feat(tui): keyboard-only ASCII redesign of config TUI

Remove all Button widgets — saves and plugin toggles are keyboard-only
(ctrl+s, e, d). Replace Header with a plain _TitleBar Static. Apply a
dark monochrome ASCII theme: +---+ borders on inputs, DataTable, and
tab panes; #0d0d0d background; grey/white palette. Disable mouse at the
driver level via run(mouse=False). Update save test to drive via ctrl+s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-18 23:28:59 +02:00
parent 1412ced7a8
commit ee6c32b035
2 changed files with 35 additions and 37 deletions
+33 -35
View File
@@ -7,7 +7,7 @@ from textual.binding import Binding
from textual.containers import Horizontal, VerticalScroll
from textual.coordinate import Coordinate
from textual.widget import Widget
from textual.widgets import Button, DataTable, Footer, Header, Input, Label, Switch, TabbedContent, TabPane
from textual.widgets import DataTable, Footer, Input, Label, Static, Switch, TabbedContent, TabPane
from pyra.config.manager import load_config, save_config
from pyra.plugins.base import BasePlugin, ConfigField
@@ -65,6 +65,21 @@ def _pfid(plugin_name: str, key: str) -> str:
return f"pf-{plugin_name}-{key}"
# ── Shared widgets ────────────────────────────────────────────────────────────
class _TitleBar(Static):
DEFAULT_CSS = """
_TitleBar {
height: 1;
background: #1a1a1a;
color: #ffffff;
text-style: bold;
padding: 0 2;
border-bottom: ascii #444444;
}
"""
# ── Tab widgets ───────────────────────────────────────────────────────────────
class _GeneralTab(VerticalScroll):
@@ -80,18 +95,10 @@ class _GeneralTab(VerticalScroll):
yield Switch(value=bool(current), id=_fid(f.path))
else:
yield Input(value=str(current), id=_fid(f.path))
with Horizontal(classes="actions"):
yield Button("Save", id="save-general", variant="primary")
def action_save(self) -> None:
self._do_save()
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id != "save-general":
return
self._do_save()
event.stop()
def _do_save(self) -> None:
cfg = load_config()
for f in GENERAL_FIELDS:
@@ -126,9 +133,6 @@ class _PluginsTab(Widget):
manifest.get("description", ""),
)
yield table
with Horizontal(classes="actions"):
yield Button("Enable [e]", id="btn-enable", variant="success")
yield Button("Disable [d]", id="btn-disable")
def action_enable_plugin(self) -> None:
self._toggle_plugin("enable")
@@ -136,14 +140,6 @@ class _PluginsTab(Widget):
def action_disable_plugin(self) -> None:
self._toggle_plugin("disable")
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn-enable":
self._toggle_plugin("enable")
event.stop()
elif event.button.id == "btn-disable":
self._toggle_plugin("disable")
event.stop()
def _toggle_plugin(self, action: str) -> None:
table = self.query_one("#plugins-table", DataTable)
if table.row_count == 0:
@@ -193,18 +189,10 @@ class _PluginConfigTab(VerticalScroll):
yield Input(value=str(current), id=_pfid(self._name, f.key))
if f.description:
yield Label(f.description, classes="hint")
with Horizontal(classes="actions"):
yield Button("Save", id=f"save-{self._name}", variant="primary")
def action_save(self) -> None:
self._do_save()
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id != f"save-{self._name}":
return
self._do_save()
event.stop()
def _do_save(self) -> None:
cfg = load_config()
settings: dict[str, Any] = dict(cfg.plugin_settings.get(self._name, {}))
@@ -230,16 +218,26 @@ class ConfigApp(App):
Binding("ctrl+left", "prev_tab", "Prev tab"),
]
CSS = """
Screen { background: $surface; }
Screen { background: #0d0d0d; color: #c8c8c8; }
TabbedContent, TabPane { background: #0d0d0d; border: ascii #444444; }
Tabs { background: #111111; border-bottom: ascii #444444; }
Tab { color: #666666; padding: 0 2; }
Tab.-active { color: #ffffff; text-style: bold; background: #1a1a1a; }
Input { border: ascii #444444; background: #111111; color: #ffffff; }
Input:focus { border: ascii #888888; }
Switch { background: #111111; }
DataTable { border: ascii #444444; height: 1fr; background: #0d0d0d; }
DataTable > .datatable--header { text-style: bold; color: #aaaaaa; background: #1a1a1a; }
DataTable > .datatable--cursor { background: #2a2a2a; color: #ffffff; }
Footer { background: #111111; color: #888888; }
Footer > .footer--key { background: #2a2a2a; color: #ffffff; }
.row { height: 3; margin: 0 2; }
.row Label { width: 26; content-align: left middle; }
.hint { color: $foreground 50%; margin: 0 2 1 28; }
.actions { height: 3; align: right middle; margin: 1 2; }
DataTable { height: 1fr; }
.row Label { width: 26; content-align: left middle; color: #aaaaaa; }
.hint { color: #555555; margin: 0 2 1 28; }
"""
def compose(self) -> ComposeResult:
yield Header()
yield _TitleBar("PYRA CONFIGURATION")
plugins = _installed_plugins()
with TabbedContent():
with TabPane("General"):
@@ -273,4 +271,4 @@ class ConfigApp(App):
def launch_config_tui() -> None:
"""Open the configuration TUI. Blocks until the user quits (q / Escape)."""
ConfigApp().run()
ConfigApp().run(mouse=False)
+2 -2
View File
@@ -107,8 +107,8 @@ async def test_general_tab_save_persists_new_value(tmp_pyra_home):
async with _TestApp().run_test() as pilot:
widget = pilot.app.query_one(f"#{_fid('general.user_name')}", Input)
widget.value = "Alice"
await pilot.pause() # flush reactive update before click
await pilot.click("#save-general")
await pilot.pause() # flush reactive update before key press
await pilot.press("ctrl+s")
assert saved, "save_config was not called"
assert saved[-1].general.user_name == "Alice"