From ebe7437974895424110bd78d29ee459ddb9d726e Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Fri, 20 Feb 2026 00:26:39 +0000 Subject: [PATCH] add a config popup window for updating connection settings from within the TUI. --- src/q3rcon_tui/rcon_tui.tcss | 93 ++++++++++++++++++++++++++++++++++-- src/q3rcon_tui/tui.py | 87 +++++++++++++++++++++++++++++++-- 2 files changed, 172 insertions(+), 8 deletions(-) diff --git a/src/q3rcon_tui/rcon_tui.tcss b/src/q3rcon_tui/rcon_tui.tcss index 9e245f7..cccccb0 100644 --- a/src/q3rcon_tui/rcon_tui.tcss +++ b/src/q3rcon_tui/rcon_tui.tcss @@ -7,14 +7,14 @@ border-title-color: #88c0d0; border-title-style: bold; padding: 1 3; - grid-size: 2 3; + grid-size: 3 3; grid-gutter: 1 2; grid-rows: auto 1fr auto; align: center middle; } #command { - column-span: 2; + column-span: 3; height: auto; width: 1fr; content-align: center middle; @@ -31,7 +31,7 @@ } #response { - column-span: 2; + column-span: 3; height: 1fr; background: #2e3440; border: solid #4c566a; @@ -57,6 +57,24 @@ Button:hover { text-style: bold; } +Button.success { + background: #a3be8c; +} + +Button.success:hover { + background: #8fbcbb; +} + +Button.warning { + background: #ebcb8b; + color: #2e3440; +} + +Button.warning:hover { + background: #d08770; + color: #2e3440; +} + Button.error { background: #bf616a; } @@ -79,6 +97,75 @@ Button.error:hover { background: #a3be8c; } +#config { + background: #ebcb8b; + color: #2e3440; +} + +#config:hover { + background: #d08770; + color: #2e3440; +} + #send:hover { background: #8fbcbb; +} + +/* Configuration Dialog Styles */ +#config-dialog { + background: #2e3440; + border: heavy #4c566a; + border-title-color: #ebcb8b; + border-title-style: bold; + padding: 1 2; + width: 60; + height: 30; + align: center middle; +} + +#config-title { + content-align: center middle; + text-style: bold; + color: #88c0d0; + height: 3; + width: 100%; + background: #3b4252; + margin-bottom: 1; +} + +#config-form { + height: auto; + width: 100%; + margin-bottom: 1; +} + +#config-form Label { + margin-bottom: 1; + color: #d8dee9; + text-style: bold; +} + +#config-form Input { + height: 3; + width: 100%; + margin-bottom: 1; + background: #3b4252; + border: solid #4c566a; + padding: 0 1; +} + +#config-form Input:focus { + border: solid #88c0d0; + background: #434c5e; +} + +#config-buttons { + height: 3; + width: 100%; + align: center middle; +} + +#config-buttons Button { + width: 1fr; + margin: 0 1; } \ No newline at end of file diff --git a/src/q3rcon_tui/tui.py b/src/q3rcon_tui/tui.py index 3e94b03..506357c 100644 --- a/src/q3rcon_tui/tui.py +++ b/src/q3rcon_tui/tui.py @@ -7,8 +7,9 @@ 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 textual.containers import Grid, Horizontal, Vertical +from textual.screen import ModalScreen +from textual.widgets import Button, Input, Label, RichLog, Static from .__about__ import __version__ as version @@ -43,6 +44,9 @@ class Settings(BaseSettings): cli_prefix='', cli_parse_args=True, cli_implicit_flags=True, + # Allow field assignment for runtime updates + validate_assignment=False, + frozen=False, ) @classmethod @@ -69,6 +73,64 @@ except ValueError as e: raise SystemExit(1) +class ConfigScreen(ModalScreen[bool]): + """Modal dialog for configuring connection settings.""" + + def __init__(self, current_host: str, current_port: int, current_password: str): + super().__init__() + self.current_host = current_host + self.current_port = current_port + self.current_password = current_password + + def compose(self) -> ComposeResult: + with Vertical(id='config-dialog'): + yield Static('Connection Configuration', id='config-title') + with Vertical(id='config-form'): + yield Label('Host:') + yield Input( + value=self.current_host, placeholder='localhost', id='host-input' + ) + yield Label('Port:') + yield Input( + value=str(self.current_port), placeholder='28960', id='port-input' + ) + yield Label('Password:') + yield Input( + value=self.current_password, + placeholder='Enter password', + password=True, + id='password-input', + ) + with Horizontal(id='config-buttons'): + yield Button('Save', variant='success', id='config-save') + yield Button('Cancel', variant='error', id='config-cancel') + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == 'config-save': + try: + new_host = ( + self.query_one('#host-input', Input).value.strip() or 'localhost' + ) + new_port = int(self.query_one('#port-input', Input).value or '28960') + new_password = self.query_one('#password-input', Input).value + + if new_port < 1 or new_port > 65535: + raise ValueError('Port must be between 1 and 65535') + + if len(new_password) < 8: + raise ValueError('Password must be at least 8 characters long') + + settings.host = new_host + settings.port = new_port + settings.password = new_password + + self.dismiss(True) + except ValueError: + self.app.bell() + elif event.button.id == 'config-cancel': + self.dismiss(False) + + class RconApp(App): RE_COLOR_CODES = re.compile(r'\^[0-9]') CSS_PATH = 'rcon_tui.tcss' @@ -77,19 +139,34 @@ class RconApp(App): yield Grid( Input('status', placeholder='Enter a rcon command', id='command'), RichLog(id='response'), - Button('Send', variant='error', id='send'), + Button('Send', variant='success', id='send'), + Button('Config', variant='warning', id='config'), 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: + match event.key: + case 'enter' if self.query_one('#command', Input).has_focus: self.query_one('#send', Button).press() + case 'f2': + self.query_one('#config', Button).press() async def on_button_pressed(self, event: Button.Pressed) -> None: if event.button.id == 'quit': self.app.exit() + elif event.button.id == 'config': + result = await self.push_screen( + ConfigScreen(settings.host, settings.port, settings.password) + ) + if result: + self.query_one('#response', RichLog).write( + f'Configuration updated: {settings.host}:{settings.port}' + ) + return + + if event.button.id != 'send': + return if settings.refresh_output: self.query_one('#response', RichLog).clear()