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:
+33
-35
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user