diff --git a/src/pyra/config/tui.py b/src/pyra/config/tui.py index 94113f9..80d8cfb 100644 --- a/src/pyra/config/tui.py +++ b/src/pyra/config/tui.py @@ -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) diff --git a/tests/unit/test_config_tui.py b/tests/unit/test_config_tui.py index f2b8025..8b38201 100644 --- a/tests/unit/test_config_tui.py +++ b/tests/unit/test_config_tui.py @@ -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"