feat(tui-v3): add settings utility section and inspector kind-specific hints

Settings screen now renders a UTILITY & MAINTENANCE card listing stats,
db-export, db-import, checksum-verify/update, totp-export, and offline
mode commands. Renders gracefully when config service is offline (no
early return — all sections including utility always display).

Inspector adds UtilityHintsBar widget that persists across board swaps
via a #board-slot Vertical. Hints update to show kind-appropriate
palette commands (copy, reveal keybinds, QR, npub, managed-load, etc.).
BoardContainer now routes managed_account to its own SeedBoard hint set.

Two new smoke tests verify the utility section renders and that hints
update correctly for password, nostr, and idle states.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Sonnet 4.6 committed 2026-03-06 13:35:49 -05:00
1 parent ca31abf33e
commit 02c4ef9e24
3 files changed
+141 -23

No files matched your search

+23 -12
View File
@@ -66,20 +66,20 @@ class SettingsScreen(Screen):
def action_refresh(self) -> None:
"""Fetch live settings and render them into the content panel."""
app = self.app
if "config" not in app.services:
self.query_one("#settings-content", Static).update(
"[red]Config Service Offline[/red]"
)
return
config_online = "config" in app.services
service = app.services["config"]
if config_online:
service = app.services["config"]
def get_val(key, default=""):
try:
val = service.get(key)
return val if val is not None else default
except Exception:
return "[red](error)[/red]"
def get_val(key, default=""):
try:
val = service.get(key)
return val if val is not None else default
except Exception:
return "[red](error)[/red]"
else:
def get_val(key, default=""): # type: ignore[misc]
return "[dim](offline)[/dim]"
# Categorized Rows
security_rows = [
@@ -106,6 +106,15 @@ class SettingsScreen(Screen):
"Fresh Namespace: [dim](nostr-fresh-namespace)[/dim]",
]
utility_rows = [
"Statistics : [dim](stats)[/dim]",
"DB Export : [dim](db-export <path>)[/dim]",
"DB Import : [dim](db-import <path>)[/dim]",
"Checksum : [dim](checksum-verify)[/dim] | [dim](checksum-update)[/dim]",
"TOTP Export : [dim](totp-export)[/dim]",
"Offline Mode : [dim](setting-offline on|off)[/dim]",
]
# Use the app's card rendering utility if available, or fallback
# In v3, we keep card logic in a central place or widget.
# For now, we'll implement a clean local version.
@@ -124,6 +133,8 @@ class SettingsScreen(Screen):
render_card("STORAGE & BACKUP POLICY", backup_rows),
"",
render_card("CONNECTIVITY & NOSTR SYNC", nostr_rows),
"",
render_card("UTILITY & MAINTENANCE", utility_rows),
]
self.query_one("#settings-content", Static).update("\n".join(rendered_lines))
+73 -11
View File
@@ -561,6 +561,43 @@ class GenericBoard(BaseBoard):
pass
class UtilityHintsBar(Static):
"""Shows kind-specific palette command hints at the bottom of the inspector."""
DEFAULT_CSS = """
UtilityHintsBar {
height: auto;
min-height: 2;
background: #111111;
color: #888888;
border-top: solid #333333;
padding: 0 1;
}
"""
_HINTS: dict[str, list[str]] = {
"idle": [],
"password": ["copy", "v: reveal", "edit", "export", "archive"],
"totp": ["copy (2FA code)", "v: reveal secret", "g: QR", "archive"],
"note": ["copy (content)", "edit", "archive"],
"document": ["copy (content)", "edit", "archive"],
"seed": ["vv: reveal phrase", "gg: QR", "archive"],
"managed_account": ["ml: managed-load", "vv: reveal phrase", "archive"],
"ssh": ["copy (private key)", "vv: reveal", "archive"],
"pgp": ["copy (private key)", "vv: reveal", "archive"],
"nostr": ["copy (nsec/npub)", "v: reveal nsec", "g: QR", "npub", "archive"],
"generic": ["copy", "v: reveal", "archive"],
}
def update_kind(self, board_type: str) -> None:
hints = self._HINTS.get(board_type, self._HINTS["generic"])
if not hints:
self.update("")
return
hint_str = " | ".join(hints)
self.update(f"[b]Actions:[/b] {hint_str} [dim](Ctrl+P: palette)[/dim]")
class BoardContainer(Vertical):
"""
Reactive container that swaps specialized boards based on entry kind.
@@ -574,14 +611,23 @@ class BoardContainer(Vertical):
background: #000000;
overflow: auto;
}
#board-slot {
height: 1fr;
overflow: auto;
}
"""
def compose(self) -> ComposeResult:
yield Vertical(id="board-slot")
yield UtilityHintsBar(id="utility-hints-bar")
def on_mount(self) -> None:
self._show_board("idle")
def update_entry(self, entry_id: int | None) -> None:
if entry_id is None:
self._show_board("idle")
self._update_hints("idle")
return
app = self.app
@@ -591,37 +637,53 @@ class BoardContainer(Vertical):
entry = app.services["entry"].retrieve_entry(entry_id)
if not entry:
self._show_board("idle")
self._update_hints("idle")
return
kind = str(entry.get("kind") or entry.get("type") or "").lower()
if kind in {"password", "stored_password"}:
board = self._show_board("password")
board_type = "password"
elif kind == "totp":
board = self._show_board("totp")
board_type = "totp"
elif kind in {"document", "note"}:
board = self._show_board("note")
elif kind in {"seed", "managed_account"}:
board = self._show_board("seed")
board_type = "note"
elif kind == "managed_account":
board_type = "managed_account"
elif kind == "seed":
board_type = "seed"
elif kind == "ssh":
board = self._show_board("ssh")
board_type = "ssh"
elif kind == "pgp":
board = self._show_board("pgp")
board_type = "pgp"
elif kind == "nostr":
board = self._show_board("nostr")
board_type = "nostr"
else:
board = self._show_board("generic")
board_type = "generic"
board = self._show_board(board_type)
board.entry_data = entry
self._update_hints(board_type)
def _update_hints(self, board_type: str) -> None:
try:
self.query_one("#utility-hints-bar", UtilityHintsBar).update_kind(board_type)
except Exception:
pass
def _show_board(self, board_type: str) -> Any:
self.remove_children()
try:
slot = self.query_one("#board-slot", Vertical)
except Exception:
slot = self
slot.remove_children()
mapping = {
"idle": IdleBoard,
"password": PasswordBoard,
"totp": TotpBoard,
"note": NoteBoard,
"seed": SeedBoard,
"managed_account": SeedBoard,
"ssh": SshBoard,
"pgp": PgpBoard,
"nostr": NostrBoard,
@@ -629,7 +691,7 @@ class BoardContainer(Vertical):
}
board_cls = mapping.get(board_type, GenericBoard)
new_board = board_cls()
self.mount(new_board)
slot.mount(new_board)
return new_board
+45
View File
@@ -22,6 +22,8 @@ from seedpass.tui_v3.screens.atlas import AtlasWayfinderScreen
from seedpass.tui_v3.screens.pubkey import NostrPubkeyScreen
from seedpass.tui_v3.screens.relays import RelaysScreen
from seedpass.tui_v3.screens.security import ChangePasswordScreen
from seedpass.tui_v3.screens.settings import SettingsScreen
from seedpass.tui_v3.widgets.inspector import UtilityHintsBar
class V3EntryService:
@@ -983,3 +985,46 @@ async def test_tui3_word_entry_flags_invalid_bip39_word() -> None:
progress = str(app.screen.query_one("#seed-words-progress").render())
assert "not in the BIP-39 wordlist" in status
assert "invalid: 1" in progress
@pytest.mark.anyio
async def test_tui3_settings_screen_shows_utility_maintenance_section() -> None:
app, _entry, _vault, _search = _build_app()
async with app.run_test() as pilot:
await pilot.pause()
app.action_toggle_settings()
await pilot.pause()
assert isinstance(app.screen, SettingsScreen)
content = str(app.screen.query_one("#settings-content", Static).render())
assert "UTILITY & MAINTENANCE" in content
assert "db-export" in content
assert "db-import" in content
assert "checksum-verify" in content
assert "stats" in content
@pytest.mark.anyio
async def test_tui3_inspector_utility_hints_update_by_entry_kind() -> None:
app, _entry, _vault, _search = _build_app()
async with app.run_test() as pilot:
await pilot.pause()
# Select password entry (id=1)
app.selected_entry_id = 1
await pilot.pause()
hints_bar = app.screen.query_one("#utility-hints-bar", UtilityHintsBar)
hints_text = str(hints_bar.render())
assert "reveal" in hints_text.lower()
assert "copy" in hints_text.lower()
# Select nostr entry (id=3) — should show npub hint
app.selected_entry_id = 3
await pilot.pause()
hints_text = str(hints_bar.render())
assert "npub" in hints_text.lower()
# Deselect — hints bar should be empty
app.selected_entry_id = None
await pilot.pause()
hints_text = str(hints_bar.render())
assert "Actions" not in hints_text