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.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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user