initial commit

This commit is contained in:
2026-03-20 08:55:47 +00:00
commit 17889e56bd
11 changed files with 889 additions and 0 deletions

0
src/vban_tui/__init__.py Normal file
View File

29
src/vban_tui/hybrid.py Normal file
View File

@@ -0,0 +1,29 @@
"""module for hybrid widgets"""
from textual.app import ComposeResult
from textual.containers import Container, Vertical
from textual.widgets import Input, Label
class LabelInput(Container):
"""A label and input field container for configuration settings."""
def __init__(
self,
label: str,
placeholder: str = '',
id: str = '',
label_id: str = '',
input_id: str = '',
):
super().__init__(id=id)
self.label = label
self.placeholder = placeholder
self.label_id = label_id
self.input_id = input_id
def compose(self) -> ComposeResult:
"""Create child widgets for the label and input."""
with Vertical():
yield Label(self.label, id=self.label_id)
yield Input(placeholder=self.placeholder, id=self.input_id)

59
src/vban_tui/settings.py Normal file
View File

@@ -0,0 +1,59 @@
from pathlib import Path
from typing import Annotated, Type
from pydantic import AfterValidator
from pydantic_settings import BaseSettings, CliSettingsSource, SettingsConfigDict
def is_valid_host(value: str) -> str:
if not value:
raise ValueError('Host cannot be empty')
return value
def is_valid_port(value: int) -> int:
if not (0 < value < 65536):
raise ValueError('Port must be between 1 and 65535')
return value
def is_valid_streamname(value: str) -> str:
if len(value) > 16:
raise ValueError('Stream name cannot be longer than 16 characters')
return value
class Settings(BaseSettings):
host: Annotated[str, AfterValidator(is_valid_host)] = 'localhost'
port: Annotated[int, AfterValidator(is_valid_port)] = 6980
streamname: Annotated[str, AfterValidator(is_valid_streamname)] = ''
model_config = SettingsConfigDict(
env_file=(
'.env',
Path.home() / '.config' / 'vban-tui' / 'config.env',
),
env_file_encoding='utf-8',
env_prefix='VBAN_TUI_',
cli_prefix='',
cli_parse_args=True,
cli_implicit_flags=True,
validate_assignment=True,
frozen=False,
)
@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,
)

89
src/vban_tui/tui.py Normal file
View File

@@ -0,0 +1,89 @@
import vban_cmd
from textual.app import App, ComposeResult
from textual.containers import Grid
from textual.widgets import RichLog
from .hybrid import LabelInput
from .settings import Settings
class VbanTui(App):
"""A Textual App to display VBAN data."""
CSS_PATH = 'tui.tcss'
def __init__(self):
super().__init__()
self._settings = Settings()
def compose(self) -> ComposeResult:
"""Create child widgets for the app."""
yield Grid(
LabelInput(
'VBAN Host:',
'localhost',
id='host-labelinput',
label_id='host-label',
input_id='host-input',
),
LabelInput(
'VBAN Port:',
'6980',
id='port-labelinput',
label_id='port-label',
input_id='port-input',
),
LabelInput(
'VBAN Stream Name:',
'Command1',
id='streamname-labelinput',
label_id='streamname-label',
input_id='streamname-input',
),
LabelInput(
'VBAN Command:',
'Enter request',
id='request-labelinput',
label_id='request-label',
input_id='request-input',
),
RichLog(id='response-log'),
id='main-grid',
)
def on_mount(self):
"""Focus the request input on mount."""
self.query_one('#host-input').value = self._settings.host
self.query_one('#port-input').value = str(self._settings.port)
self.query_one('#streamname-input').value = self._settings.streamname
self.query_one('#request-input').focus()
def on_key(self, event):
"""Handle key events."""
match event.key:
case 'q':
self.exit()
case 'enter' if self.query_one('#request-input').has_focus:
self.query_one('#response-log').clear()
request_input = self.query_one('#request-input')
request_input.add_class('request-sent')
self.send_request()
self.set_timer(0.5, lambda: request_input.remove_class('request-sent'))
def send_request(self):
with vban_cmd.api(
'potato',
host=self.query_one('#host-input').value,
port=int(self.query_one('#port-input').value),
streamname=self.query_one('#streamname-input').value,
disable_rt_listeners=True,
) as vban:
if response := vban.sendtext(self.query_one('#request-input').value):
self.query_one('#response-log').write(response)
def main():
"""Run the VBAN TUI application."""
app = VbanTui()
app.run()

100
src/vban_tui/tui.tcss Normal file
View File

@@ -0,0 +1,100 @@
#request-input.request-sent {
border: solid #a6e3a1;
background: #313244;
color: #a6e3a1;
transition: border-color 0.2s, background 0.2s, color 0.2s;
}
#main-grid {
height: 30;
margin: 1 2;
background: #1e1e2e;
color: #f5e0dc;
border: heavy #313244;
border-title-color: #b4befe;
border-title-style: bold;
padding: 1 1;
grid-size: 3 3;
grid-gutter: 1 1;
grid-rows: 25% 22% 57%;
align: center middle;
}
#host-labelinput {
height: auto;
width: 1fr;
content-align: left middle;
text-align: left;
background: #2a273f;
border: solid #313244;
border-title-color: #f5c2e7;
padding: 0 1;
margin: 0 1;
}
#host-label {
padding: 0 1;
}
#port-labelinput {
height: auto;
width: 1fr;
content-align: left middle;
text-align: left;
background: #2a273f;
border: solid #313244;
border-title-color: #b4befe;
padding: 0 1;
margin: 0 1;
}
#port-label {
padding: 0 1;
}
#streamname-labelinput {
height: auto;
width: 1fr;
content-align: left middle;
text-align: left;
background: #2a273f;
border: solid #313244;
border-title-color: #c6a0f6;
padding: 0 1;
margin: 0 1;
}
#streamname-label {
padding: 0 1;
}
#request-labelinput {
column-span: 3;
height: auto;
width: 1fr;
content-align: left middle;
text-align: left;
background: #2a273f;
border: solid #313244;
border-title-color: #b4befe;
padding: 0 1;
margin: 0 1;
}
#request-label {
padding: 0 1;
}
#response-log {
column-span: 3;
height: 0.8fr;
background: #181825;
color: #f5e0dc;
border: solid #313244;
border-title-color: #b4befe;
padding: 1;
margin: 0 1;
overflow-y: auto;
scrollbar-background: #2a273f;
scrollbar-color: #c6a0f6;
scrollbar-size: 1 1;
}