mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-04 03:42:04 +00:00
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:
1 parent
ca31abf33e
commit
02c4ef9e24
3 files changed
+141
-23
No files matched your search
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user