first commit

This commit is contained in:
2026-02-19 22:29:15 +00:00
commit 7682f4080e
13 changed files with 680 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026-present onyx-and-iris <code@onyxandiris.online>
#
# SPDX-License-Identifier: MIT
__version__ = '0.1.0'

View File

@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026-present onyx-and-iris <code@onyxandiris.online>
#
# SPDX-License-Identifier: MIT

View File

@@ -0,0 +1,84 @@
#dialog {
height: 30;
margin: 4 8;
background: #1e1e2f;
color: #e2e8f0;
border: heavy #3b4252;
border-title-color: #88c0d0;
border-title-style: bold;
padding: 1 3;
grid-size: 2 3;
grid-gutter: 1 2;
grid-rows: auto 1fr auto;
align: center middle;
}
#command {
column-span: 2;
height: auto;
width: 1fr;
content-align: center middle;
background: #2e3440;
border: solid #4c566a;
border-title-color: #81a1c1;
padding: 0 1;
margin: 0 1;
}
#command:focus {
border: solid #88c0d0;
background: #3b4252;
}
#response {
column-span: 2;
height: 1fr;
background: #2e3440;
border: solid #4c566a;
border-title-color: #a3be8c;
padding: 1;
margin: 0 1;
scrollbar-background: #3b4252;
scrollbar-color: #5e81ac;
}
Button {
width: 100%;
height: 3;
margin: 0 1;
background: #5e81ac;
color: #eceff4;
border: none;
text-style: bold;
}
Button:hover {
background: #81a1c1;
text-style: bold;
}
Button.error {
background: #bf616a;
}
Button.error:hover {
background: #d08770;
}
#quit {
background: #bf616a;
border: solid #bf616a;
}
#quit:hover {
background: #d08770;
border: solid #ebcb8b;
}
#send {
background: #a3be8c;
}
#send:hover {
background: #8fbcbb;
}

114
src/q3rcon_tui/tui.py Normal file
View File

@@ -0,0 +1,114 @@
import re
from pathlib import Path
from typing import Annotated, Type
from aioq3rcon import Client
from loguru import logger
from pydantic import AfterValidator, BeforeValidator
from pydantic_settings import BaseSettings, CliSettingsSource, SettingsConfigDict
from textual.app import App, ComposeResult
from textual.containers import Grid
from textual.widgets import Button, Input, RichLog
from .__about__ import __version__ as version
def is_valid_password(password: str) -> str | None:
if len(password) < 8:
raise ValueError('Password must be at least 8 characters long')
return password
def version_callback(value: bool) -> bool | None:
if value:
print(f'q3rcon-tui version: {version}')
raise SystemExit(0)
return False
class Settings(BaseSettings):
host: str = 'localhost'
port: int = 28960
password: Annotated[str, AfterValidator(is_valid_password)] = ''
refresh_output: bool = False
version: Annotated[bool, BeforeValidator(version_callback)] = False
model_config = SettingsConfigDict(
env_file=(
'.env',
Path.home() / '.config' / 'rcon-tui' / 'config.env',
),
env_file_encoding='utf-8',
env_prefix='Q3RCON_TUI_',
cli_prefix='',
cli_parse_args=True,
cli_implicit_flags=True,
)
@classmethod
def settings_customise_sources(
cls,
settings_cls: Type[BaseSettings],
init_settings: ...,
env_settings: ...,
dotenv_settings: ...,
file_secret_settings: ...,
) -> tuple:
return (
CliSettingsSource(settings_cls),
env_settings,
dotenv_settings,
init_settings,
)
try:
settings = Settings()
except ValueError as e:
logger.error(e)
raise SystemExit(1)
class RconApp(App):
RE_COLOR_CODES = re.compile(r'\^[0-9]')
CSS_PATH = 'rcon_tui.tcss'
def compose(self) -> ComposeResult:
yield Grid(
Input('status', placeholder='Enter a rcon command', id='command'),
RichLog(id='response'),
Button('Send', variant='error', id='send'),
Button('Quit', variant='primary', id='quit'),
id='dialog',
)
async def on_key(self, event) -> None:
if event.key == 'enter':
if self.query_one('#command', Input).has_focus:
self.query_one('#send', Button).press()
async def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == 'quit':
self.app.exit()
if settings.refresh_output:
self.query_one('#response', RichLog).clear()
async with Client(settings.host, settings.port, settings.password) as client:
response = await client.send_command(
self.query_one('#command', Input).value
)
self.query_one('#response', RichLog).write(
self.remove_color_codes(response.removeprefix('print\n'))
)
self.query_one('#command', Input).value = ''
@staticmethod
def remove_color_codes(s: str) -> str:
return RconApp.RE_COLOR_CODES.sub('', s)
def main():
app = RconApp()
app.run()