mirror of
https://github.com/onyx-and-iris/q3rcon-tui.git
synced 2026-04-08 21:23:30 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02ded9fb46 | |||
| fee3678f40 | |||
| ba91b2d8be | |||
| 437b76ab13 | |||
| 004f1d0880 | |||
| 71401c32f9 | |||
| 1cee478197 | |||
| fcc91b7e34 | |||
| 9b3ae629f3 |
@@ -43,6 +43,8 @@ q3rcon-tui --host=localhost --port=28960 --password=rconpassword
|
||||
Additional flags:
|
||||
|
||||
- `--raw`: Boolean flag, if set the RichLog will print raw responses without rendering tables.
|
||||
- `--min-status`: Boolean flag, if set the status command will print a minified table.
|
||||
- note, this will have no effect if in *raw* mode.
|
||||
- `--append`: Boolean flag, if set the RichLog output will append each response continuously.
|
||||
- `--version`: Print the version of the TUI.
|
||||
- `--help`: Print the help message.
|
||||
@@ -59,6 +61,7 @@ example .env:
|
||||
Q3RCON_TUI_HOST=localhost
|
||||
Q3RCON_TUI_PORT=28960
|
||||
Q3RCON_TUI_PASSWORD=rconpassword
|
||||
Q3RCON_TUI_MIN_STATUS=false
|
||||
Q3RCON_TUI_RAW=false
|
||||
Q3RCON_TUI_APPEND=false
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2026-present onyx-and-iris <code@onyxandiris.online>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
__version__ = '0.5.0'
|
||||
__version__ = '0.6.0'
|
||||
|
||||
@@ -2,7 +2,12 @@ from pathlib import Path
|
||||
from typing import Annotated, Type
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import AfterValidator, BeforeValidator
|
||||
from pydantic import (
|
||||
AfterValidator,
|
||||
AliasChoices,
|
||||
BeforeValidator,
|
||||
Field,
|
||||
)
|
||||
from pydantic_settings import BaseSettings, CliSettingsSource, SettingsConfigDict
|
||||
|
||||
from .__about__ import __version__ as version
|
||||
@@ -26,6 +31,14 @@ class Settings(BaseSettings):
|
||||
port: int = 28960
|
||||
password: Annotated[str, AfterValidator(is_valid_password)] = ''
|
||||
append: bool = False
|
||||
min_status: bool = Field(
|
||||
default=False,
|
||||
alias='min-status',
|
||||
validation_alias=AliasChoices(
|
||||
'min-status',
|
||||
'Q3RCON_TUI_MIN_STATUS',
|
||||
),
|
||||
)
|
||||
raw: bool = False
|
||||
version: Annotated[bool, BeforeValidator(version_callback)] = False
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class RconApp(App):
|
||||
)
|
||||
except RCONError:
|
||||
output = (
|
||||
'Unable to execute command.',
|
||||
f'Unable to execute command {cmd}.',
|
||||
'It may be due to a map change or a server restart.',
|
||||
'If the problem persists, please check your connection settings and ensure the server is running.',
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ Renderable = Text | Table | str
|
||||
|
||||
class Writable:
|
||||
RE_COLOR_CODES = re.compile(r'\^[0-9]')
|
||||
RE_MAP_FROM_STATUS = re.compile(r'^map: (?P<mapname>mp_[a-z_]+)$')
|
||||
RE_PLAYER_FROM_STATUS = re.compile(
|
||||
r'^\s*(?P<slot>[0-9]+)\s+'
|
||||
r'(?P<score>[0-9-]+)\s+'
|
||||
@@ -26,11 +27,11 @@ class Writable:
|
||||
)
|
||||
RE_CVAR = re.compile(
|
||||
r'^["](?P<name>[a-z_]+)["]\sis[:]\s'
|
||||
r'["](?P<value>.*?)\^7["]\s'
|
||||
r'["](?P<value>.*?)["]\s'
|
||||
r'default[:]\s'
|
||||
r'["](?P<default>.*?)\^7["]\s'
|
||||
r'["](?P<default>.*?)["]\s'
|
||||
r'info[:]\s'
|
||||
r'["](?P<info>.*?)\^7["]$'
|
||||
r'["](?P<info>.*?)["]$'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -38,7 +39,7 @@ class Writable:
|
||||
return Writable.RE_COLOR_CODES.sub('', s)
|
||||
|
||||
def parse(self, cmd, response: str, style=None) -> Renderable:
|
||||
response = response.removeprefix('print\n')
|
||||
response = self.remove_color_codes(response.removeprefix('print\n'))
|
||||
if settings.raw:
|
||||
return Text(response, style=style)
|
||||
|
||||
@@ -49,58 +50,83 @@ class Writable:
|
||||
if m := self.RE_CVAR.match(response):
|
||||
return self.cvar_table(m)
|
||||
else:
|
||||
return Text(self.remove_color_codes(response), style=style)
|
||||
return Text(response, style=style)
|
||||
|
||||
def error(self, message: str) -> Text:
|
||||
return Text(message, style='#c73d4b')
|
||||
|
||||
def status_table(self, status_response: str) -> Table | str:
|
||||
table = Table(show_header=True, header_style='bold #88c0d0')
|
||||
table.add_column('Slot', justify='center')
|
||||
table.add_column('Score', justify='center')
|
||||
table.add_column('Ping', justify='center')
|
||||
table.add_column('GUID', justify='center')
|
||||
table.add_column('Name', justify='center')
|
||||
columns = [
|
||||
('Slot', 'center'),
|
||||
('Score', 'center'),
|
||||
('Ping', 'center'),
|
||||
('GUID', 'center'),
|
||||
('Name', 'center'),
|
||||
]
|
||||
for column, justify in columns:
|
||||
table.add_column(column, justify=justify)
|
||||
if not settings.min_status:
|
||||
table.add_column('Last', justify='center')
|
||||
if settings.min_status:
|
||||
table.add_column('IP', justify='center')
|
||||
table.add_column('Port', justify='center')
|
||||
table.add_column('QPort', justify='center')
|
||||
table.add_column('Rate', justify='center')
|
||||
else:
|
||||
table.add_column('IP:Port', justify='center')
|
||||
columns = [
|
||||
('QPort', 'center'),
|
||||
('Rate', 'center'),
|
||||
]
|
||||
if not settings.min_status:
|
||||
for column, justify in columns:
|
||||
table.add_column(column, justify=justify)
|
||||
|
||||
mapname = 'unable to parse map name'
|
||||
for line in status_response.splitlines():
|
||||
match self.RE_PLAYER_FROM_STATUS.match(line):
|
||||
case None:
|
||||
continue
|
||||
case m:
|
||||
table.add_row(
|
||||
if m := self.RE_PLAYER_FROM_STATUS.match(line):
|
||||
name = m.group('name')
|
||||
if name == '':
|
||||
name = '[no name]'
|
||||
row = [
|
||||
m.group('slot'),
|
||||
m.group('score'),
|
||||
m.group('ping'),
|
||||
m.group('guid'),
|
||||
self.remove_color_codes(m.group('name')),
|
||||
m.group('last'),
|
||||
m.group('ip'),
|
||||
m.group('port'),
|
||||
m.group('qport'),
|
||||
m.group('rate'),
|
||||
)
|
||||
name,
|
||||
]
|
||||
if settings.min_status:
|
||||
row.append(m.group('ip'))
|
||||
else:
|
||||
row.append(f'{m.group("ip")}:{m.group("port")}')
|
||||
row.append(m.group('last'))
|
||||
row.append(m.group('qport'))
|
||||
row.append(m.group('rate'))
|
||||
table.add_row(*row)
|
||||
elif m := self.RE_MAP_FROM_STATUS.match(line):
|
||||
mapname = m.group('mapname')
|
||||
|
||||
out = Text(f'Map: {mapname}\n', style='bold #88c0d0')
|
||||
if len(table.rows) == 0:
|
||||
return 'No players connected.'
|
||||
return out.append('No players connected', style='#c73d4b')
|
||||
else:
|
||||
table.title = out
|
||||
return table
|
||||
|
||||
def cvar_table(self, m: re.Match) -> Table:
|
||||
table = Table(show_header=True, header_style='bold #88c0d0')
|
||||
table.add_column('Name', justify='center')
|
||||
table.add_column('Value', justify='center')
|
||||
table.add_column('Default', justify='center')
|
||||
table.add_column('Info', justify='center')
|
||||
columns = [
|
||||
('Name', 'center'),
|
||||
('Value', 'center'),
|
||||
('Default', 'center'),
|
||||
('Info', 'center'),
|
||||
]
|
||||
for column, justify in columns:
|
||||
table.add_column(column, justify=justify)
|
||||
|
||||
table.add_row(
|
||||
m.group('name'),
|
||||
self.remove_color_codes(m.group('value')),
|
||||
self.remove_color_codes(m.group('default')),
|
||||
self.remove_color_codes(m.group('info')),
|
||||
m.group('value'),
|
||||
m.group('default'),
|
||||
m.group('info'),
|
||||
)
|
||||
|
||||
return table
|
||||
|
||||
Reference in New Issue
Block a user