mirror of
https://github.com/onyx-and-iris/vban-tui.git
synced 2026-04-13 04:23:37 +00:00
initial commit
This commit is contained in:
0
src/vban_tui/__init__.py
Normal file
0
src/vban_tui/__init__.py
Normal file
29
src/vban_tui/hybrid.py
Normal file
29
src/vban_tui/hybrid.py
Normal 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
59
src/vban_tui/settings.py
Normal 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
89
src/vban_tui/tui.py
Normal 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
100
src/vban_tui/tui.tcss
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user