From d0519bb8e9f0aac525d1c6838d670a730ff14d55 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Mon, 23 Mar 2026 14:45:35 +0000 Subject: [PATCH] add spinners for all commands (including interactive mode). pass send_command(interpret=True) for commands that return long responses. note, it strips the first `"` from cvar responses which causes CVAR_REGEX to fail. --- src/q3rcon_cli/cli.py | 55 +++++++++++++++----------- src/q3rcon_cli/commands/fastrestart.py | 10 +++-- src/q3rcon_cli/commands/gametype.py | 25 +++++++----- src/q3rcon_cli/commands/hostname.py | 16 ++++---- src/q3rcon_cli/commands/map.py | 13 +++--- src/q3rcon_cli/commands/mapname.py | 10 +++-- src/q3rcon_cli/commands/maprestart.py | 7 ++-- src/q3rcon_cli/commands/maprotate.py | 11 ++++-- src/q3rcon_cli/commands/plugins.py | 10 +++-- src/q3rcon_cli/commands/status.py | 7 ++-- src/q3rcon_cli/console.py | 2 +- 11 files changed, 99 insertions(+), 67 deletions(-) diff --git a/src/q3rcon_cli/cli.py b/src/q3rcon_cli/cli.py index b0ed1ce..5f66d48 100644 --- a/src/q3rcon_cli/cli.py +++ b/src/q3rcon_cli/cli.py @@ -1,6 +1,6 @@ import clypi from aioq3rcon import Client, IncorrectPasswordError -from clypi import Command, arg +from clypi import Command, Spinner, arg from typing_extensions import override from . import console @@ -72,35 +72,42 @@ class Q3rconCli(Command): clypi.style('to quit.', fg='blue'), ) + DEFAULT_TIMEOUT = 2 DEFAULT_FRAGMENT_READ_TIMEOUT = 0.25 while command := input(clypi.style('cmd: ', fg='green')): if command.lower() == 'q': break - fragment_read_timeout = None - if command in ( - 'status', - 'fast_restart', - 'map_restart', - 'map', - 'map_rotate', - ): - fragment_read_timeout = 1 + TIMEOUTS = { + 'status': (2, 1, False), + 'fast_restart': (3, 1, True), + 'map_restart': (3, 1, True), + 'map': (3, 1, True), + 'map_rotate': (3, 1, True), + } + timeout, fragment_read_timeout, interpret = TIMEOUTS.get( + command.split()[0].lower(), + (DEFAULT_TIMEOUT, DEFAULT_FRAGMENT_READ_TIMEOUT, False), + ) - async with Client( - self.host, - self.port, - self.password, - fragment_read_timeout=fragment_read_timeout - or DEFAULT_FRAGMENT_READ_TIMEOUT, - ) as client: - try: - if response := await client.send_command(command): - console.out.print_response(response) - except TimeoutError: - console.err.print( - f"Timeout waiting for response for command: '{command}'" - ) + async with Spinner(f"Sending command: '{command}'", suffix='...'): + async with Client( + self.host, + self.port, + self.password, + timeout=timeout or DEFAULT_TIMEOUT, + fragment_read_timeout=fragment_read_timeout + or DEFAULT_FRAGMENT_READ_TIMEOUT, + ) as client: + try: + if response := await client.send_command( + command, interpret=interpret + ): + console.out.print_response(response) + except TimeoutError: + console.err.print( + f"Timeout waiting for response for command: '{command}'" + ) def main(): diff --git a/src/q3rcon_cli/commands/fastrestart.py b/src/q3rcon_cli/commands/fastrestart.py index f7be628..4554b60 100644 --- a/src/q3rcon_cli/commands/fastrestart.py +++ b/src/q3rcon_cli/commands/fastrestart.py @@ -1,5 +1,5 @@ from aioq3rcon import Client -from clypi import Command, arg +from clypi import Command, Spinner, arg from typing_extensions import override from q3rcon_cli import console @@ -14,6 +14,8 @@ class Fastrestart(Command): @override async def run(self): - async with Client(self.host, self.port, self.password) as client: - if response := await client.send_command('fast_restart'): - console.out.print_response(response) + async with Spinner('Executing fast restart', suffix='...'): + async with Client(self.host, self.port, self.password) as client: + response = await client.send_command('fast_restart', interpret=True) + + console.out.print_response(response) diff --git a/src/q3rcon_cli/commands/gametype.py b/src/q3rcon_cli/commands/gametype.py index be9d590..0579e62 100644 --- a/src/q3rcon_cli/commands/gametype.py +++ b/src/q3rcon_cli/commands/gametype.py @@ -1,5 +1,5 @@ from aioq3rcon import Client -from clypi import Command, Positional, Spinner, arg, cprint +from clypi import Command, Positional, Spinner, arg from typing_extensions import override from q3rcon_cli import console @@ -24,16 +24,23 @@ class Gametype(Command): @override async def run(self): if not self.new_gametype: - async with Client(self.host, self.port, self.password) as client: - if response := await client.send_command('g_gametype'): - console.out.print_cvar(response) + async with Spinner('Fetching current gametype', suffix='...'): + async with Client(self.host, self.port, self.password) as client: + response = await client.send_command('g_gametype') + console.out.print_cvar(response) return - async with Client(self.host, self.port, self.password) as client: - await client.send_command(f'g_gametype {self.new_gametype}') - if self.force: - async with Spinner('Forcing gametype change', suffix='...'): + async with Spinner(f'Changing gametype to {self.new_gametype}', suffix='...'): + async with Client(self.host, self.port, self.password) as client: + await client.send_command(f'g_gametype {self.new_gametype}') + + if self.force: + async with Spinner('Forcing gametype change', suffix='...'): + async with Client(self.host, self.port, self.password) as client: + client.timeout = 3 client.fragment_read_timeout = 1 await client.send_command('map_restart') - cprint(f'Gametype changed successfully to {self.new_gametype}.', fg='green') + console.out.print( + f'Gametype changed successfully to {self.new_gametype}.', style='green' + ) diff --git a/src/q3rcon_cli/commands/hostname.py b/src/q3rcon_cli/commands/hostname.py index bcd5eeb..d709342 100644 --- a/src/q3rcon_cli/commands/hostname.py +++ b/src/q3rcon_cli/commands/hostname.py @@ -1,5 +1,5 @@ from aioq3rcon import Client -from clypi import Command, Positional, arg, cprint +from clypi import Command, Positional, Spinner, arg from typing_extensions import override from q3rcon_cli import console @@ -19,12 +19,14 @@ class Hostname(Command): @override async def run(self): if not self.new_hostname: - async with Client(self.host, self.port, self.password) as client: - if response := await client.send_command('sv_hostname'): - console.out.print_cvar(response) + async with Spinner('Fetching current hostname', suffix='...'): + async with Client(self.host, self.port, self.password) as client: + response = await client.send_command('sv_hostname') + console.out.print_cvar(response) return - async with Client(self.host, self.port, self.password) as client: - await client.send_command(f'sv_hostname {self.new_hostname}') + async with Spinner(f'Changing hostname to {self.new_hostname}', suffix='...'): + async with Client(self.host, self.port, self.password) as client: + await client.send_command(f'sv_hostname {self.new_hostname}') - cprint(f'Hostname changed to: {self.new_hostname}', fg='green') + console.out.print(f'Hostname changed to: {self.new_hostname}', style='green') diff --git a/src/q3rcon_cli/commands/map.py b/src/q3rcon_cli/commands/map.py index ea20d0e..2cb6efd 100644 --- a/src/q3rcon_cli/commands/map.py +++ b/src/q3rcon_cli/commands/map.py @@ -1,5 +1,5 @@ from aioq3rcon import Client -from clypi import Command, Positional, Spinner, arg, cprint +from clypi import Command, Positional, Spinner, arg from typing_extensions import override from q3rcon_cli import console @@ -19,9 +19,10 @@ class Map(Command): @override async def run(self): if not self.new_map: - async with Client(self.host, self.port, self.password) as client: - if response := await client.send_command('mapname'): - console.out.print_cvar(response) + async with Spinner('Getting current map', suffix='...'): + async with Client(self.host, self.port, self.password) as client: + response = await client.send_command('mapname') + console.out.print_cvar(response) return async with Spinner('Changing map', suffix='...'): @@ -30,4 +31,6 @@ class Map(Command): ) as client: await client.send_command(f'map mp_{self.new_map.removeprefix("mp_")}') - cprint(f'Map changed to {self.new_map.removeprefix("mp_")}', fg='green') + console.out.print( + f'Map changed to {self.new_map.removeprefix("mp_")}', style='green' + ) diff --git a/src/q3rcon_cli/commands/mapname.py b/src/q3rcon_cli/commands/mapname.py index c0bd6cb..f679238 100644 --- a/src/q3rcon_cli/commands/mapname.py +++ b/src/q3rcon_cli/commands/mapname.py @@ -1,5 +1,5 @@ from aioq3rcon import Client -from clypi import Command, arg +from clypi import Command, Spinner, arg from typing_extensions import override from q3rcon_cli import console @@ -14,6 +14,8 @@ class Mapname(Command): @override async def run(self): - async with Client(self.host, self.port, self.password) as client: - if response := await client.send_command('mapname'): - console.out.print_cvar(response) + async with Spinner('Getting map name...'): + async with Client(self.host, self.port, self.password) as client: + response = await client.send_command('mapname') + + console.out.print_cvar(response) diff --git a/src/q3rcon_cli/commands/maprestart.py b/src/q3rcon_cli/commands/maprestart.py index 9694616..38c36c1 100644 --- a/src/q3rcon_cli/commands/maprestart.py +++ b/src/q3rcon_cli/commands/maprestart.py @@ -16,7 +16,8 @@ class Maprestart(Command): async def run(self): async with Spinner('Restarting map', suffix='...'): async with Client( - self.host, self.port, self.password, fragment_read_timeout=1 + self.host, self.port, self.password, timeout=3, fragment_read_timeout=1 ) as client: - if response := await client.send_command('map_restart'): - console.out.print_response(response) + response = await client.send_command('map_restart', interpret=True) + + console.out.print_response(response) diff --git a/src/q3rcon_cli/commands/maprotate.py b/src/q3rcon_cli/commands/maprotate.py index 8b5b167..fb1c9d7 100644 --- a/src/q3rcon_cli/commands/maprotate.py +++ b/src/q3rcon_cli/commands/maprotate.py @@ -16,7 +16,12 @@ class Maprotate(Command): async def run(self): async with Spinner('Rotating map', suffix='...'): async with Client( - self.host, self.port, self.password, fragment_read_timeout=1 + self.host, + self.port, + self.password, + timeout=3, + fragment_read_timeout=1, ) as client: - if response := await client.send_command('map_rotate'): - console.out.print_response(response) + response = await client.send_command('map_rotate', interpret=True) + + console.out.print_response(response) diff --git a/src/q3rcon_cli/commands/plugins.py b/src/q3rcon_cli/commands/plugins.py index ed48997..7b6e272 100644 --- a/src/q3rcon_cli/commands/plugins.py +++ b/src/q3rcon_cli/commands/plugins.py @@ -1,5 +1,5 @@ from aioq3rcon import Client -from clypi import Command, arg +from clypi import Command, Spinner, arg from typing_extensions import override from q3rcon_cli import console @@ -14,6 +14,8 @@ class Plugins(Command): @override async def run(self): - async with Client(self.host, self.port, self.password) as client: - if response := await client.send_command('plugins'): - console.out.print_response(response) + async with Spinner('Fetching plugins...'): + async with Client(self.host, self.port, self.password) as client: + response = await client.send_command('plugins') + + console.out.print_response(response) diff --git a/src/q3rcon_cli/commands/status.py b/src/q3rcon_cli/commands/status.py index 0fb7791..3f33415 100644 --- a/src/q3rcon_cli/commands/status.py +++ b/src/q3rcon_cli/commands/status.py @@ -16,7 +16,8 @@ class Status(Command): async def run(self): async with Spinner('Fetching status', suffix='...'): async with Client( - self.host, self.port, self.password, fragment_read_timeout=0.5 + self.host, self.port, self.password, fragment_read_timeout=1 ) as client: - if response := await client.send_command('status'): - console.out.print_status(response) + response = await client.send_command('status') + + console.out.print_status(response) diff --git a/src/q3rcon_cli/console.py b/src/q3rcon_cli/console.py index 9864d50..d94ef8e 100644 --- a/src/q3rcon_cli/console.py +++ b/src/q3rcon_cli/console.py @@ -72,7 +72,7 @@ class OutConsole(Console): _names.append(self._remove_colour_codes(m.group('name'))) _ips.append(m.group('ip')) elif m := OutConsole.STATUS_MAP_REGEX.match(line): - cprint(f'\nCurrent map: {m.group("mapname")}', fg=self.style) + cprint(f'Current map: {m.group("mapname")}', fg=self.style) if not _slots: cprint('No players connected.', fg='red')