Learn cvar and command names from the server for autocomplete

Send cvarlist and cmdlist on connect and merge the answer into the
autocomplete candidates. Number the server menu from 0, keep suggesting
sibling commands while the command name is typed, mask the password in
the startup line, ignore venv/, fix README claims.
This commit is contained in:
pfl
2026-09-16 18:25:48 +02:00
parent db123993c0
commit 3d1dc19d9b
6 changed files with 227 additions and 60 deletions
+15 -4
View File
@@ -22,6 +22,7 @@ from lib.parser import EventParser
from lib.formatter import format_message, format_chat_message, format_powerup_message, strip_color_codes
from lib.ui import UIManager, select_server
from lib.settings import ConfigLoader
from lib.namelist import NameCapture
# Pre-compiled regex patterns
CVAR_RESPONSE_PATTERN = re.compile(r'"([^"]+)"\s+is:"([^"]*)"')
@@ -131,7 +132,7 @@ def handle_stats_connection(message):
password_str = match.group(2)
password_str = strip_color_codes(password_str)
stats_password = password_str.strip()
logger.info(f'Got stats password: {stats_password}')
logger.info('Got stats password')
return stats_port, stats_password
@@ -340,15 +341,17 @@ def main_loop(screen, args):
# Initialize components
game_state = GameState()
name_capture = NameCapture() # cvar/command names learned from the server for autocomplete
ui = UIManager(screen, args.title, args.max_history,
player_names_provider=lambda: game_state.player_tracker.get_player_names())
player_names_provider=lambda: game_state.player_tracker.get_player_names(),
learned_names_provider=lambda: name_capture.names)
# Setup logging to output window
log_handler = ui.setup_logging()
logger.addHandler(log_handler)
# Attach UI log handler to lib module loggers
for lib_name in ('network', 'parser', 'state'):
for lib_name in ('network', 'parser', 'state', 'namelist'):
lib_logger = logging.getLogger(lib_name)
if log_handler not in lib_logger.handlers:
lib_logger.addHandler(log_handler)
@@ -359,7 +362,8 @@ def main_loop(screen, args):
ui.print_message(f"zmq python bindings {zmq.__version__}, libzmq version {zmq.zmq_version()}\n")
# Initialize network connections
ui.print_message(f"Connecting with host={args.host} password={args.password}\n")
password_display = '***' if args.password else '(none)'
ui.print_message(f"Connecting with host={args.host} password={password_display}\n")
rcon = RconConnection(args.host, args.password, args.identity)
rcon.connect()
@@ -408,6 +412,7 @@ def main_loop(screen, args):
ui.print_message("Requesting connection info...\n")
rcon.send_command(b'zmq_stats_password')
rcon.send_command(b'net_port')
name_capture.request(rcon)
elif monitor_event and monitor_event[0] == zmq.EVENT_DISCONNECTED:
timestamp = time.strftime('%H:%M:%S')
ui.print_message(f"^3[^7{timestamp}^3] ^8^1Disconnected from server - waiting for reconnect...^0^7\n")
@@ -516,6 +521,12 @@ def main_loop(screen, args):
ui.print_message(f"^1[^7{timestamp}^1] Error: Stats connection failed: {e}^7\n")
logger.error(f'Stats connection failed: {e}')
# Hide the cvarlist/cmdlist output while learning names from it.
# Runs after the cvar handling above so the stats credentials and
# server info responses are never swallowed.
if name_capture.handle(message):
continue
# Try to parse as game event
parsed_event = event_parser.parse_event(message)
if parsed_event: