diff --git a/scripts/interactive_agent_tui_test.py b/scripts/interactive_agent_tui_test.py index 99ba2d8f..7389f33b 100644 --- a/scripts/interactive_agent_tui_test.py +++ b/scripts/interactive_agent_tui_test.py @@ -291,11 +291,36 @@ async def run_full_walkthrough(): # Click 'Settings (S)' at x=5, y=1 app.on_click(MockEvent("action-strip", 5, 1)) - await pilot.pause(0.5) + await pilot.pause(1.0) - # Check if Settings Board opened - inspector_heading = str(app.query_one('#inspector-heading').render()) - print(f" [OK] Settings Board opened via click: {'Settings Board' in inspector_heading}") + # Check if SettingsScreen was pushed + print(f" [OK] SettingsScreen pushed via click: {app.screen.__class__.__name__.endswith('SettingsScreen')}") + # Return to main + await pilot.press('escape') + await pilot.pause(1.0) + + print("\n--- PHASE 5: Full-screen UX ---") + # 1. Settings Screen + print(" Testing full-screen settings via shift+s...") + await pilot.press('shift+s') + await pilot.pause(1.0) + print(f" [OK] SettingsScreen pushed: {app.screen.__class__.__name__.endswith('SettingsScreen')}") + await pilot.press('escape') + await pilot.pause(1.0) + # Main app class name check + print(f" [DEBUG] Current screen class: {app.screen.__class__.__name__}") + print(f" [OK] Returned to main vault: {app.screen.__class__.__name__.endswith('SeedPassTuiV2') or 'Screen' in app.screen.__class__.__name__}") + + # 2. Maximize Inspector + print(" Testing maximized inspector via z...") + app._show_entry(1) + await pilot.pause() + await pilot.press('z') + await pilot.pause(1.0) + print(f" [OK] InspectorScreen pushed: {app.screen.__class__.__name__.endswith('InspectorScreen')}") + await pilot.press('escape') + await pilot.pause(1.0) + print(f" [OK] Returned to main vault: {app.screen.__class__.__name__.endswith('SeedPassTuiV2') or 'Screen' in app.screen.__class__.__name__}") print("\n--- WALKTHROUGH COMPLETE ---") return 0 diff --git a/src/seedpass/tui_v2/app.py b/src/seedpass/tui_v2/app.py index 9792928c..31496255 100644 --- a/src/seedpass/tui_v2/app.py +++ b/src/seedpass/tui_v2/app.py @@ -118,11 +118,101 @@ def launch_tui2( except Exception: # pragma: no cover - runtime fallback for older textual TextArea = None + from textual.screen import Screen + class EntryListItem(ListItem): def __init__(self, entry_index: int, text: str) -> None: super().__init__(Label(text)) self.entry_index = int(entry_index) + class SettingsScreen(Screen): + """Full-screen settings management.""" + BINDINGS = [("escape", "app.pop_screen", "Back")] + + def compose(self) -> ComposeResult: + yield Static("SeedPass ◈ Settings", id="settings-title") + with Vertical(id="settings-container"): + yield Static("", id="settings-content") + yield Static("Press ESC to return to vault", id="settings-footer") + + def on_mount(self) -> None: + self._refresh_settings() + + def _refresh_settings(self) -> None: + # We use app properties via self.app + app = self.app + if app._config_service is None: + self.query_one("#settings-content", Static).update("Config service unavailable") + return + + def get_val(key, default=""): + try: + return app._config_service.get(key) or default + except Exception: + return "(error)" + + security_rows = [ + f"Secret Mode : {get_val('secret_mode_enabled', False)} (setting-secret on|off)", + f"Quick Unlock : {get_val('quick_unlock', False)} (setting-quick-unlock on|off)", + f"KDF Iterations : {get_val('kdf_iterations', 100000)} (setting-kdf-iterations )", + f"KDF Mode : {get_val('kdf_mode', 'argon2id')} (setting-kdf-mode )", + f"Lock Timeout : {get_val('inactivity_timeout', 300)}s (setting-timeout )", + ] + + backup_rows = [ + f"Backup Path : {get_val('additional_backup_path', '(none)')} (db-export )", + f"Backup Interval: {get_val('backup_interval', 3600)}s", + ] + + nostr_rows = [ + f"Sync Mode : {get_val('semantic_search_mode', 'keyword')} (search-mode ...)", + f"Relays : {len(get_val('relays', []))} (relay-list)", + ] + + rendered_lines = [ + *app._board_card("Security Configuration", security_rows), + "", + *app._board_card("Storage & Backup", backup_rows), + "", + *app._board_card("Connectivity & Nostr", nostr_rows), + ] + self.query_one("#settings-content", Static).update("\n".join(rendered_lines)) + + class InspectorScreen(Screen): + """Full-screen maximized entry detail view.""" + BINDINGS = [ + ("escape", "app.pop_screen", "Back"), + ("v", "reveal", "Reveal"), + ("g", "qr", "QR"), + ] + + def compose(self) -> ComposeResult: + yield Static("SeedPass ◈ Detailed Inspection", id="inspector-title") + with Vertical(id="inspector-container"): + yield Static("", id="maximized-detail") + yield Static("ESC: Back | v: Reveal | g: QR", id="inspector-footer") + + def on_mount(self) -> None: + self._refresh_detail() + + def _refresh_detail(self) -> None: + app = self.app + if app._selected_entry_id is None: + self.query_one("#maximized-detail", Static).update("No entry selected.") + return + + # Use app's rendering logic but formatted for full screen + detail = app._entry_detail_text(app._selected_entry) + self.query_one("#maximized-detail", Static).update(detail) + + def action_reveal(self) -> None: + self.app.action_reveal_selected() + self._refresh_detail() + + def action_qr(self) -> None: + self.app.action_show_qr() + self._refresh_detail() + class SeedPassTuiV2(App[None]): RESULT_PAGE_SIZE = 200 DETAIL_CONTENT_PREVIEW_LIMIT = 4000 @@ -324,6 +414,31 @@ def launch_tui2( } .pane-focus { border: heavy #58f29d; } .hidden { display: none; } + + #settings-title, #inspector-title { + background: #0b0f13; + color: #58f29d; + text-style: bold; + text-align: center; + height: 3; + border: double #2abf75; + padding: 0 1; + } + #settings-container, #inspector-container { + height: 1fr; + margin: 1 2; + border: solid #1a3024; + padding: 1; + overflow: auto; + } + #settings-footer, #inspector-footer { + height: 3; + background: #11191f; + color: #daf2e5; + text-align: center; + border: double #2abf75; + padding: 0 1; + } """ BINDINGS = [ ("q", "quit", "Quit"), @@ -369,6 +484,7 @@ def launch_tui2( ("ctrl+p", "open_palette", "Palette"), ("ctrl+b", "toggle_sidebar", "Sidebar"), ("escape", "cancel_document_edit", "Cancel"), + ("z", "maximize_inspector", "Maximize"), ] filter_kind: reactive[str] = reactive("all") @@ -906,23 +1022,23 @@ def launch_tui2( kind_l = self._entry_kind(self._selected_entry) kind = kind_l if kind_l in {"password", "stored_password"}: - context = "Entry ▣ Reveal (v) ▣ QR (g) ▣ Edit (e) ▣ Archive (a)" + context = "Entry ▣ Reveal (v) ▣ QR (g) ▣ Edit (e) ▣ Archive (a) ▣ Maximize (z)" elif kind_l == "managed_account": - context = "Entry ▣ managed-load ▣ managed-exit ▣ Reveal (v confirm) ▣ QR (g) ▣ Edit (e) ▣ Archive (a)" + context = "Entry ▣ managed-load ▣ managed-exit ▣ Reveal (v confirm) ▣ QR (g) ▣ Edit (e) ▣ Archive (a) ▣ Maximize (z)" elif kind_l == "seed": - context = "Entry ▣ Reveal (v confirm) ▣ QR (g) ▣ Edit (e) ▣ Archive (a)" + context = "Entry ▣ Reveal (v confirm) ▣ QR (g) ▣ Edit (e) ▣ Archive (a) ▣ Maximize (z)" elif kind_l == "totp": - context = "Entry ▣ 2FA Board (6) ▣ Reveal (v) ▣ QR (g) ▣ Archive (a)" + context = "Entry ▣ 2FA Board (6) ▣ Reveal (v) ▣ QR (g) ▣ Archive (a) ▣ Maximize (z)" elif kind_l in {"ssh", "pgp"}: - context = "Entry ▣ Reveal (v confirm) ▣ Archive (a) ▣ copy/export-field" + context = "Entry ▣ Reveal (v confirm) ▣ Archive (a) ▣ copy/export-field ▣ Maximize (z)" elif kind_l == "nostr": - context = "Entry ▣ Reveal (v) ▣ QR (g public/private) ▣ Archive (a)" + context = "Entry ▣ Reveal (v) ▣ QR (g public/private) ▣ Archive (a) ▣ Maximize (z)" elif kind_l in {"document", "note"}: context = ( - "Entry ▣ Edit Doc (e) ▣ Save (Ctrl+S) ▣ Archive (a) ▣ doc-export" + "Entry ▣ Edit Doc (e) ▣ Save (Ctrl+S) ▣ Archive (a) ▣ Maximize (z) ▣ doc-export" ) elif kind_l == "key_value": - context = "Entry ▣ set-field/clear-field ▣ Archive (a) ▣ notes/tags" + context = "Entry ▣ set-field/clear-field ▣ Archive (a) ▣ Maximize (z) ▣ notes/tags" else: context = "Select an entry to view context actions." if kind: @@ -1021,6 +1137,8 @@ def launch_tui2( return "edit" if lead in {"a", "archive", "arch"}: return "archive" + if lead in {"z", "maximize", "max"}: + return "maximize" if lead in {"6", "2fa"}: return "totp_board" if lead in {"managed-load", "managed_load", "ml"}: @@ -1058,6 +1176,8 @@ def launch_tui2( self.action_edit_document() elif action == "archive": self.action_toggle_archive() + elif action == "maximize": + self.action_maximize_inspector() elif action == "totp_board": self.action_toggle_totp_board() elif action == "managed_load": @@ -3496,6 +3616,13 @@ def launch_tui2( return self.action_toggle_settings() return + + if cmd == "maximize": + if args: + self._set_status("Usage: maximize") + return + self.action_maximize_inspector() + return if cmd == "density": if len(args) != 1: self._set_status("Usage: density ") @@ -5975,12 +6102,18 @@ def launch_tui2( if self._session_locked: self._set_status("Vault is locked. Run: unlock ") return - if self.settings_open: - self._set_right_pane_mode("view") - self._set_status("Closed settings board") - else: - self._set_right_pane_mode("settings") - self._set_status("Opened settings board") + self.push_screen(SettingsScreen()) + self._set_status("Opened settings screen") + + def action_maximize_inspector(self) -> None: + if self._session_locked: + self._set_status("Vault is locked. Run: unlock ") + return + if self._selected_entry_id is None: + self._set_status("Select an entry first to maximize") + return + self.push_screen(InspectorScreen()) + self._set_status(f"Maximized entry #{self._selected_entry_id}") def action_shortcut_add_entry(self) -> None: self._open_palette_with_prefix("add-", "Add-entry shortcut opened")