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.containers import Horizontal, VerticalScroll
from textual.coordinate import Coordinate from textual.coordinate import Coordinate
from textual.widget import Widget 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.config.manager import load_config, save_config
from pyra.plugins.base import BasePlugin, ConfigField 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}" 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 ─────────────────────────────────────────────────────────────── # ── Tab widgets ───────────────────────────────────────────────────────────────
class _GeneralTab(VerticalScroll): class _GeneralTab(VerticalScroll):
@@ -80,18 +95,10 @@ class _GeneralTab(VerticalScroll):
yield Switch(value=bool(current), id=_fid(f.path)) yield Switch(value=bool(current), id=_fid(f.path))
else: else:
yield Input(value=str(current), id=_fid(f.path)) 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: def action_save(self) -> None:
self._do_save() 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: def _do_save(self) -> None:
cfg = load_config() cfg = load_config()
for f in GENERAL_FIELDS: for f in GENERAL_FIELDS:
@@ -126,9 +133,6 @@ class _PluginsTab(Widget):
manifest.get("description", ""), manifest.get("description", ""),
) )
yield table 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: def action_enable_plugin(self) -> None:
self._toggle_plugin("enable") self._toggle_plugin("enable")
@@ -136,14 +140,6 @@ class _PluginsTab(Widget):
def action_disable_plugin(self) -> None: def action_disable_plugin(self) -> None:
self._toggle_plugin("disable") 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: def _toggle_plugin(self, action: str) -> None:
table = self.query_one("#plugins-table", DataTable) table = self.query_one("#plugins-table", DataTable)
if table.row_count == 0: if table.row_count == 0:
@@ -193,18 +189,10 @@ class _PluginConfigTab(VerticalScroll):
yield Input(value=str(current), id=_pfid(self._name, f.key)) yield Input(value=str(current), id=_pfid(self._name, f.key))
if f.description: if f.description:
yield Label(f.description, classes="hint") 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: def action_save(self) -> None:
self._do_save() 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: def _do_save(self) -> None:
cfg = load_config() cfg = load_config()
settings: dict[str, Any] = dict(cfg.plugin_settings.get(self._name, {})) 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"), Binding("ctrl+left", "prev_tab", "Prev tab"),
] ]
CSS = """ 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 { height: 3; margin: 0 2; }
.row Label { width: 26; content-align: left middle; } .row Label { width: 26; content-align: left middle; color: #aaaaaa; }
.hint { color: $foreground 50%; margin: 0 2 1 28; } .hint { color: #555555; margin: 0 2 1 28; }
.actions { height: 3; align: right middle; margin: 1 2; }
DataTable { height: 1fr; }
""" """
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header() yield _TitleBar("PYRA CONFIGURATION")
plugins = _installed_plugins() plugins = _installed_plugins()
with TabbedContent(): with TabbedContent():
with TabPane("General"): with TabPane("General"):
@@ -273,4 +271,4 @@ class ConfigApp(App):
def launch_config_tui() -> None: def launch_config_tui() -> None:
"""Open the configuration TUI. Blocks until the user quits (q / Escape).""" """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: async with _TestApp().run_test() as pilot:
widget = pilot.app.query_one(f"#{_fid('general.user_name')}", Input) widget = pilot.app.query_one(f"#{_fid('general.user_name')}", Input)
widget.value = "Alice" widget.value = "Alice"
await pilot.pause() # flush reactive update before click await pilot.pause() # flush reactive update before key press
await pilot.click("#save-general") await pilot.press("ctrl+s")
assert saved, "save_config was not called" assert saved, "save_config was not called"
assert saved[-1].general.user_name == "Alice" assert saved[-1].general.user_name == "Alice"