mirror of
https://github.com/onyx-and-iris/q3rcon-tui.git
synced 2026-04-08 21:23:30 +00:00
first commit
This commit is contained in:
4
src/q3rcon_tui/__about__.py
Normal file
4
src/q3rcon_tui/__about__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2026-present onyx-and-iris <code@onyxandiris.online>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
__version__ = '0.1.0'
|
||||
3
src/q3rcon_tui/__init__.py
Normal file
3
src/q3rcon_tui/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2026-present onyx-and-iris <code@onyxandiris.online>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
84
src/q3rcon_tui/rcon_tui.tcss
Normal file
84
src/q3rcon_tui/rcon_tui.tcss
Normal 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
114
src/q3rcon_tui/tui.py
Normal 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()
|
||||
Reference in New Issue
Block a user