Update TUI v3 plan and land parity fixes with smoke tests

This commit is contained in:
thePR0M3TH3AN committed 2026-03-02 23:43:07 -05:00
1 parent e7ea8a4187
commit d3920ccc4c
306 files changed
+51128 -425

No files matched your search

+89 -16
View File
@@ -18,6 +18,21 @@ from .widgets.action_bar import ActionBar
from .screens.settings import SettingsScreen
from .screens.inspector import MaximizedInspectorScreen
def render_qr_ascii(data: str) -> str:
"""Render ``data`` as an ASCII QR code."""
import qrcode
qr = qrcode.QRCode(border=1)
qr.add_data(data)
qr.make(fit=True)
matrix = qr.get_matrix()
lines: list[str] = []
for row in matrix:
lines.append("".join("##" if cell else " " for cell in row))
return "\n".join(lines)
class CommandProcessor:
"""Handles logic for palette commands in TUI v3."""
def __init__(self, app: SeedPassTuiV3):
@@ -36,18 +51,24 @@ class CommandProcessor:
args = parts[1:]
if cmd == "help":
self.app.notify("v3 commands: stats, lock, refresh, settings, maximize")
self.app.notify(
"v3 commands: help, stats, session-status, lock, unlock <password>, refresh, search <query>, open <id>, settings, maximize, copy, archive, restore"
)
elif cmd == "stats":
self.app.notify("Calculating stats...")
# We reuse the existing stats logic
if "vault" in self.app.services:
stats = self.app.services["vault"].stats()
self.app.notify(f"Total entries: {stats.get('total_entries', 0)}")
elif cmd == "session-status":
self.app.action_session_status()
elif cmd == "lock":
if "vault" in self.app.services:
self.app.services["vault"].lock()
self.app.session_locked = True
self.app.notify("Vault locked")
self.app.action_lock()
elif cmd == "unlock":
if len(args) != 1:
self.app.notify("Usage: unlock <password>", severity="warning")
return
self.app.action_unlock(args[0])
elif cmd == "refresh":
self.app.action_refresh()
elif cmd == "search":
@@ -139,7 +160,7 @@ class SeedPassTuiV3(App[None]):
# Shared Reactive State
active_fingerprint = reactive("")
selected_entry_id = reactive[int | None](None)
session_locked = reactive(True)
session_locked = reactive(False)
# Internal state for sensitive actions
_pending_sensitive_confirm: tuple[str, int, float] | None = None
@@ -194,10 +215,18 @@ class SeedPassTuiV3(App[None]):
def action_refresh_ui_quiet(self) -> None:
"""Background refresh for dynamic elements (2FA)."""
try:
# We only refresh the inspector if it's currently showing a 2FA board
board = self.query_one("#board-container")
board = self.screen.query_one("#board-container")
if board and hasattr(board, "children") and board.children:
current_board = board.children[0]
# Ensure selection/inspector stay synchronized if a selection is set
# but the board is still idle after screen transitions.
if (
self.selected_entry_id is not None
and current_board.__class__.__name__ == "IdleBoard"
):
board.update_entry(self.selected_entry_id)
return
# Refresh dynamic TOTP countdown only for the active TOTP board.
if current_board.__class__.__name__ == "TotpBoard":
current_board.refresh()
except Exception:
@@ -209,28 +238,28 @@ class SeedPassTuiV3(App[None]):
return
# Notify sidebar and grid to refresh
try:
self.query_one("#profile-tree")._refresh_tree()
self.query_one("#entry-data-table")._refresh_data()
self.screen.query_one("#profile-tree")._refresh_tree()
self.screen.query_one("#entry-data-table")._refresh_data()
except Exception:
pass
def watch_selected_entry_id(self, old_id: int | None, new_id: int | None) -> None:
"""Update inspectors when an entry is selected."""
try:
self.query_one("#board-container").update_entry(new_id)
self.screen.query_one("#board-container").update_entry(new_id)
except Exception:
pass
def action_refresh(self) -> None:
"""Force a global UI refresh."""
self.query_one("#profile-tree")._refresh_tree()
self.query_one("#entry-data-table")._refresh_data()
self.screen.query_one("#profile-tree")._refresh_tree()
self.screen.query_one("#entry-data-table")._refresh_data()
self.notify("UI Refreshed")
def action_search(self, query: str) -> None:
"""Search entries and update grid."""
try:
self.query_one("#entry-data-table")._refresh_data(query)
self.screen.query_one("#entry-data-table")._refresh_data(query)
self.notify(f"Search results for: {query}")
except Exception:
pass
@@ -238,7 +267,7 @@ class SeedPassTuiV3(App[None]):
def action_open_palette(self) -> None:
"""Toggle the command palette."""
try:
self.query_one("#palette").toggle()
self.screen.query_one("#palette").toggle()
except Exception:
pass
@@ -253,6 +282,50 @@ class SeedPassTuiV3(App[None]):
return
self.push_screen(MaximizedInspectorScreen())
def action_session_status(self) -> None:
"""Display vault lock status."""
state = "locked" if self.session_locked else "unlocked"
self.notify(f"Session status: {state}")
def action_lock(self) -> None:
"""Lock the vault if service support is available."""
vault = self.services.get("vault")
if vault is None:
self.notify("Vault service unavailable", severity="error")
return
locker = getattr(vault, "lock", None)
if not callable(locker):
self.notify("Vault service does not support lock", severity="error")
return
try:
locker()
self.session_locked = True
self.notify("Vault locked")
except Exception as e:
self.notify(f"Lock failed: {e}", severity="error")
def action_unlock(self, password: str) -> None:
"""Unlock the vault with a password."""
vault = self.services.get("vault")
if vault is None:
self.notify("Vault service unavailable", severity="error")
return
unlocker = getattr(vault, "unlock", None)
if not callable(unlocker):
self.notify("Vault service does not support unlock", severity="error")
return
try:
try:
from seedpass.core.api import UnlockRequest
unlocker(UnlockRequest(password=password))
except Exception:
unlocker(password)
self.session_locked = False
self.notify("Vault unlocked")
except Exception as e:
self.notify(f"Unlock failed: {e}", severity="error")
def action_reveal_selected(self, confirm: bool = False) -> None:
"""Handle reveal shortcut (v)."""
if self.session_locked:
@@ -410,7 +483,7 @@ class SeedPassTuiV3(App[None]):
# 2. Update standard inspector board
try:
board_cont = self.query_one("#board-container")
board_cont = self.screen.query_one("#board-container")
if board_cont.children:
board = board_cont.children[0]
if hasattr(board, "reveal_data"):
+7
View File
@@ -39,6 +39,13 @@ class EntryDataTable(DataTable):
)
self._refresh_data()
def on_focus(self) -> None:
"""Refresh table when focused so newly added entries appear immediately."""
try:
self._refresh_data()
except Exception:
pass
def _refresh_data(self, query: str = "") -> None:
self.clear()
app = self.app
+17 -4
View File
@@ -54,10 +54,23 @@ class ProfileTree(Tree):
def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
"""Handle profile selection."""
fp = event.node.data
if fp and isinstance(fp, str):
self.app.active_fingerprint = fp
self.app.notify(f"Switched to profile: {fp[:8]}")
value = event.node.data
if not value or not isinstance(value, str):
return
if value.startswith("managed:") or value.startswith("agent:"):
_, raw_id = value.split(":", 1)
try:
entry_id = int(raw_id)
except ValueError:
self.app.notify("Invalid tree entry target", severity="error")
return
self.app.selected_entry_id = entry_id
self.app.notify(f"Opened Entry #{entry_id} from profile tree")
return
self.app.active_fingerprint = value
self.app.notify(f"Switched to profile: {value[:8]}")
class SidebarContainer(Static):
"""Container for the sidebar tree and toggle."""
+167
View File
@@ -0,0 +1,167 @@
from __future__ import annotations
from types import SimpleNamespace
import pytest
pytest.importorskip("textual")
from textual.widgets import DataTable
from seedpass.tui_v3 import launch_tui3
class V3EntryService:
def __init__(self) -> None:
self._entries: dict[int, dict] = {
1: {"id": 1, "kind": "password", "label": "Email", "length": 16},
2: {"id": 2, "kind": "managed_account", "label": "Managed Ops"},
3: {"id": 3, "kind": "nostr", "label": "Agent Nostr"},
}
self.copied: list[str] = []
def search_entries(
self,
query: str,
kinds: list[str] | None = None,
*,
include_archived: bool = False,
archived_only: bool = False,
):
_ = (include_archived, archived_only)
q = (query or "").strip().lower()
rows = []
for entry_id in sorted(self._entries.keys()):
entry = self._entries[entry_id]
kind = str(entry.get("kind", "password"))
if kinds and kind not in kinds:
continue
label = str(entry.get("label", ""))
if q and q not in label.lower():
continue
rows.append(
(
entry_id,
label,
entry.get("username"),
entry.get("url"),
bool(entry.get("archived", False)),
SimpleNamespace(value=kind),
)
)
return rows
def retrieve_entry(self, entry_id: int):
return dict(self._entries.get(int(entry_id), {}))
def generate_password(self, length: int, entry_id: int) -> str:
return f"pw-{entry_id}-{length}"
def archive_entry(self, entry_id: int) -> None:
self._entries[int(entry_id)]["archived"] = True
def restore_entry(self, entry_id: int) -> None:
self._entries[int(entry_id)]["archived"] = False
def copy_to_clipboard(self, value: str) -> bool:
self.copied.append(value)
return True
class V3ProfileService:
def list_profiles(self) -> list[str]:
return ["EFBE51E70ED1B53A"]
class V3VaultService:
def __init__(self) -> None:
self.locked = False
self.last_unlock_password: str | None = None
self._manager = SimpleNamespace(parent_seed="PARENT_SEED")
def lock(self) -> None:
self.locked = True
def unlock(self, request) -> SimpleNamespace:
if hasattr(request, "password"):
self.last_unlock_password = str(request.password)
else:
self.last_unlock_password = str(request)
self.locked = False
return SimpleNamespace(status="ok", duration=0.01)
def stats(self) -> dict[str, int]:
return {"total_entries": 3}
def _build_app() -> tuple[object, V3EntryService, V3VaultService]:
holder: dict[str, object] = {}
entry = V3EntryService()
vault = V3VaultService()
profile = V3ProfileService()
def _hook(app):
holder["app"] = app
launched = launch_tui3(
fingerprint="EFBE51E70ED1B53A",
entry_service_factory=lambda: entry,
profile_service_factory=lambda: profile,
vault_service_factory=lambda: vault,
app_hook=_hook,
)
assert launched is True
app = holder.get("app")
assert app is not None
return app, entry, vault
@pytest.mark.anyio
async def test_tui3_palette_lock_unlock_and_session_status() -> None:
app, _entry, vault = _build_app()
async with app.run_test() as pilot:
await pilot.pause()
assert app.session_locked is False
app.processor.execute("session-status")
app.processor.execute("lock")
await pilot.pause()
assert app.session_locked is True
assert vault.locked is True
app.processor.execute("unlock hunter2")
await pilot.pause()
assert app.session_locked is False
assert vault.locked is False
assert vault.last_unlock_password == "hunter2"
@pytest.mark.anyio
async def test_tui3_sidebar_child_nodes_open_entries() -> None:
app, _entry, _vault = _build_app()
async with app.run_test() as pilot:
await pilot.pause()
tree = app.screen.query_one("#profile-tree")
tree.on_tree_node_selected(SimpleNamespace(node=SimpleNamespace(data="managed:2")))
await pilot.pause()
assert app.selected_entry_id == 2
assert app.active_fingerprint == "EFBE51E70ED1B53A"
tree.on_tree_node_selected(SimpleNamespace(node=SimpleNamespace(data="agent:3")))
await pilot.pause()
assert app.selected_entry_id == 3
@pytest.mark.anyio
async def test_tui3_grid_focus_refresh_enables_selection_after_runtime_add() -> None:
app, entry, _vault = _build_app()
async with app.run_test() as pilot:
await pilot.pause()
entry._entries[4] = {
"id": 4,
"kind": "password",
"label": "Runtime Added",
"length": 18,
}
table = app.screen.query_one("#entry-data-table", DataTable)
table.focus()
await pilot.pause()
table._refresh_data()
await pilot.pause()
assert table.row_count >= 4