feat(tui-v3): migrate reveal and QR logic with confirmation support

Implemented _resolve_sensitive_payload and _show_sensitive_view in v3 app.
Added double-press confirmation logic for high-risk entry kinds. Updated
BaseBoard and all specialized boards to reactively render revealed content
or QR codes. Enabled sensitive actions within the MaximizedInspectorScreen.
Unified board update logic via _update_board_sensitive.
This commit is contained in:
thePR0M3TH3AN committed 2026-03-02 23:18:14 -05:00
1 parent 0c59c20c64
commit 66a89de13f
3 files changed
+214 -13

No files matched your search

+119 -4
View File
@@ -136,6 +136,9 @@ class SeedPassTuiV3(App[None]):
selected_entry_id = reactive[int | None](None)
session_locked = reactive(True)
# Internal state for sensitive actions
_pending_sensitive_confirm: tuple[str, int, float] | None = None
def __init__(
self,
fingerprint: str | None = None,
@@ -245,14 +248,126 @@ class SeedPassTuiV3(App[None]):
return
self.push_screen(MaximizedInspectorScreen())
def action_reveal_selected(self) -> None:
self.notify("Reveal Coming Soon in V3")
def action_reveal_selected(self, confirm: bool = False) -> None:
"""Handle reveal shortcut (v)."""
if self.session_locked:
self.notify("Vault is locked", severity="error")
return
if self.selected_entry_id is None:
return
# Check confirmation
if not confirm:
confirm = self._consume_confirm("reveal_selected", self.selected_entry_id)
self._show_sensitive_view(include_qr=False, confirm=confirm)
def action_show_qr(self) -> None:
self.notify("QR Coming Soon in V3")
def action_show_qr(self, mode: str = "default", confirm: bool = False) -> None:
"""Handle QR shortcut (g)."""
if self.session_locked:
self.notify("Vault is locked", severity="error")
return
if self.selected_entry_id is None:
return
if not confirm:
confirm = self._consume_confirm("show_qr", self.selected_entry_id)
self._show_sensitive_view(include_qr=True, qr_mode=mode, confirm=confirm)
def action_toggle_archive(self) -> None:
self.notify("Archive toggle Coming Soon in V3")
def action_copy_selected(self) -> None:
self.notify("Copy Coming Soon in V3")
def _consume_confirm(self, action: str, eid: int) -> bool:
if self._pending_sensitive_confirm is None: return False
p_action, p_eid, p_ts = self._pending_sensitive_confirm
now = time.time()
if p_action == action and p_eid == eid and (now - p_ts) <= 8.0:
self._pending_sensitive_confirm = None
return True
self._pending_sensitive_confirm = None
return False
def _show_sensitive_view(self, include_qr: bool, qr_mode: str = "default", confirm: bool = False) -> None:
try:
payload = self._resolve_sensitive_payload(qr_mode=qr_mode)
title, body, qr_data, secret, kind = payload
except Exception as e:
self.notify(f"Reveal failed: {e}", severity="error")
return
# Check if confirmation is required
requires = False
if include_qr:
requires = (kind == "nostr" and qr_mode == "private")
else:
requires = kind in {"seed", "managed_account", "ssh", "pgp"}
if requires and not confirm:
key = "g" if include_qr else "v"
self._pending_sensitive_confirm = ("show_qr" if include_qr else "reveal_selected", self.selected_entry_id, time.time())
# Update the board with confirmation prompt
self._update_board_sensitive(prompt=f"CONFIRMATION REQUIRED\n\nHigh-risk action for '{kind}'.\nPress '{key}' again within 8s to proceed.")
self.notify(f"Press '{key}' again to confirm")
return
# Success - Update Board
if include_qr:
try:
qr_rendered = render_qr_ascii(qr_data)
self._update_board_sensitive(content=qr_rendered, title=title)
except Exception as e:
self.notify(f"QR Render failed: {e}", severity="error")
else:
self._update_board_sensitive(content=body, title=title)
def _resolve_sensitive_payload(self, qr_mode="default"):
if "entry" not in self.services: raise ValueError("Service offline")
eid = self.selected_entry_id
entry = self.services["entry"].retrieve_entry(eid)
if not entry: raise ValueError("Entry not found")
kind = str(entry.get("kind") or entry.get("type") or "").lower()
label = entry.get("label", "")
if kind == "password":
val = self.services["entry"].generate_password(int(entry.get("length", 16)), eid)
return ("Password Revealed", f"Label: {label}\nPassword: {val}", None, val, kind)
if kind == "totp":
secret = self.services["entry"].get_totp_secret(eid)
from seedpass.core.totp import TotpManager
uri = TotpManager.make_otpauth_uri(label, secret)
return ("TOTP Secret Revealed", f"Label: {label}\nSecret: {secret}", uri, secret, kind)
if kind in {"seed", "managed_account"}:
parent_seed = self.services["vault"]._manager.parent_seed
if kind == "seed":
phrase = self.services["entry"].get_seed_phrase(eid, parent_seed)
else:
phrase = self.services["entry"].get_managed_account_seed(eid, parent_seed)
from seedpass.core.seedqr import encode_seedqr
return ("Seed Words Revealed", f"Label: {label}\nSeed: {phrase}", encode_seedqr(phrase), phrase, kind)
# Fallback
return ("Data Revealed", f"Label: {label}\nDetails: {entry}", None, None, kind)
def _update_board_sensitive(self, content: str = None, title: str = None, prompt: str = None):
"""Push sensitive data to the currently active board or screen."""
data = {"content": content, "title": title, "prompt": prompt}
# 1. Update full-screen inspector if active
if isinstance(self.screen, MaximizedInspectorScreen):
self.screen.reveal_data = data
return
# 2. Update standard inspector board
try:
board_cont = self.query_one("#board-container")
if board_cont.children:
board = board_cont.children[0]
if hasattr(board, "reveal_data"):
board.reveal_data = data
except Exception:
pass
+22 -4
View File
@@ -4,12 +4,17 @@ from textual.app import ComposeResult
from textual.screen import Screen
from textual.widgets import Static, Footer
from textual.containers import Vertical, Horizontal
from textual.reactive import reactive
class MaximizedInspectorScreen(Screen):
"""
Full-screen detailed view for a single entry.
Optimized for long documents, SSH keys, or PGP blocks.
"""
reveal_data = reactive[dict[str, Any]]({})
def watch_reveal_data(self, old, new):
self._refresh_detail()
BINDINGS = [
("escape", "app.pop_screen", "Back to Vault"),
@@ -61,10 +66,20 @@ class MaximizedInspectorScreen(Screen):
def _refresh_detail(self) -> None:
app = self.app
eid = app.selected_entry_id
r = self.reveal_data
if eid is None:
self.query_one("#max-inspector-content", Static).update("No entry selected.")
return
if r.get("prompt"):
self.query_one("#max-inspector-content", Static).update(f"[b]SECURITY ALERT[/b]\n\n{r['prompt']}")
return
if r.get("content") and "##" in r["content"]:
self.query_one("#max-inspector-content", Static).update(f"[b]SECURE QR DISPLAY[/b]\n\n{r['content']}")
return
if "entry" not in app.services:
return
@@ -88,15 +103,18 @@ class MaximizedInspectorScreen(Screen):
f"Modified : {entry.get('modified_at', '(unknown)')}",
]
# Content (if note/doc)
# Handle revealed content
content = entry.get('content', '')
content_block = f"\n[b]CONTENT / NOTES:[/b]\n{divider}{content}\n" if content else ""
if r.get("content"):
content = f"[b][cyan]{r['content']}[/b]"
content_block = f"\n[b]CONTENT / SECURE DATA:[/b]\n{divider}{content}\n" if content else ""
final_text = header + divider + "\n".join(meta_lines) + content_block
self.query_one("#max-inspector-content", Static).update(final_text)
def action_reveal(self) -> None:
self.app.notify("Reveal triggered (Coming Soon in v3)")
self.app.action_reveal_selected()
def action_qr(self) -> None:
self.app.notify("QR triggered (Coming Soon in v3)")
self.app.action_show_qr()
+73 -5
View File
@@ -8,6 +8,10 @@ from textual.reactive import reactive
class BaseBoard(Static):
"""Base class for all specialized entry boards."""
entry_data = reactive[dict[str, Any]]({})
reveal_data = reactive[dict[str, Any]]({})
def watch_reveal_data(self, old, new):
self.refresh()
def _render_card(self, title: str, rows: list[str]) -> str:
"""Utility to render ASCII cards matching mockup style."""
@@ -35,11 +39,22 @@ class PasswordBoard(BaseBoard):
"""Matches 'Password Board.png' mockup."""
def render(self) -> str:
d = self.entry_data
r = self.reveal_data
# If we have a direct prompt (confirmation required), show ONLY that
if r.get("prompt"):
return self._render_card("SECURITY ALERT", r["prompt"].splitlines())
# If we have revealed content, show it
password_val = "[HIDDEN] (v to reveal)"
if r.get("content"):
password_val = f"[b]{r['content']}[/b]"
cred_rows = [
f"Label : {d.get('label', '')}",
f"Username : {d.get('username', '')}",
f"URL : {d.get('url', '')}",
"Password : [HIDDEN]",
f"Password : {password_val}",
]
action_rows = [
"▣ Reveal (v) ▣ QR (g)",
@@ -56,7 +71,15 @@ class TotpBoard(BaseBoard):
"""Matches '2FA Board.png' mockup."""
def render(self) -> str:
d = self.entry_data
r = self.reveal_data
app = self.app
if r.get("prompt"):
return self._render_card("SECURITY ALERT", r["prompt"].splitlines())
if r.get("content") and "##" in r["content"]:
return self._render_card("TOTP QR CODE", r["content"].splitlines())
code = "------"
if app.services.get("entry") and not app.session_locked:
try:
@@ -70,10 +93,13 @@ class TotpBoard(BaseBoard):
filled = int((remaining / 30) * bar_width)
bar = "█" * filled + "░" * (bar_width - filled)
secret_val = r.get("content") if r.get("content") else "[HIDDEN] (v to reveal)"
cred_rows = [
f"Label : {d.get('label', '')}",
f"Code : [b][cyan]{code}[/b] ({remaining}s)",
f"Timer : {bar}",
f"Secret : {secret_val}",
f"Period : {d.get('period', 30)}s",
f"Digits : {d.get('digits', 6)}",
]
@@ -92,12 +118,24 @@ class SeedBoard(BaseBoard):
"""Matches 'BIP-39 Seed Board.png' mockup."""
def render(self) -> str:
d = self.entry_data
r = self.reveal_data
if r.get("prompt"):
return self._render_card("SECURITY ALERT", r["prompt"].splitlines())
if r.get("content") and "##" in r["content"]:
return self._render_card("SEED QR CODE", r["content"].splitlines())
phrase_val = "[HIDDEN] (vv to reveal)"
if r.get("content"):
phrase_val = f"[b]{r['content']}[/b]"
cred_rows = [
f"Label : {d.get('label', '')}",
f"Fingerprint: {d.get('fingerprint', 'Pending...')}",
f"Index : {d.get('index', 0)}",
f"Word Count : {d.get('word_count', 24)}",
"Seed Phrase: [HIDDEN] (vv to reveal)",
f"Seed Phrase: {phrase_val}",
]
action_rows = [
"▣ Reveal Words (vv) ▣ Show SeedQR (gg)",
@@ -114,12 +152,21 @@ class SshBoard(BaseBoard):
"""Matches 'SSH Board.png' mockup."""
def render(self) -> str:
d = self.entry_data
r = self.reveal_data
if r.get("prompt"):
return self._render_card("SECURITY ALERT", r["prompt"].splitlines())
priv_val = "[HIDDEN] (vv to reveal)"
if r.get("content"):
priv_val = f"[b]{r['content']}[/b]"
cred_rows = [
f"Label : {d.get('label', '')}",
f"Key Type : {d.get('key_type', 'RSA/Ed25519')}",
f"Index : {d.get('index', 0)}",
"Pub Key : [PREVIEW AVAILABLE]",
"Priv Key : [HIDDEN] (vv to reveal)",
f"Priv Key : {priv_val}",
]
action_rows = [
"▣ Copy Pub (c) ▣ Reveal Priv (vv)",
@@ -136,11 +183,20 @@ class PgpBoard(BaseBoard):
"""Matches 'PGP Board.png' mockup."""
def render(self) -> str:
d = self.entry_data
r = self.reveal_data
if r.get("prompt"):
return self._render_card("SECURITY ALERT", r["prompt"].splitlines())
priv_val = "[HIDDEN] (vv to reveal)"
if r.get("content"):
priv_val = f"[b]{r['content']}[/b]"
cred_rows = [
f"Label : {d.get('label', '')}",
f"Identity : {d.get('user_id', '')}",
f"Fingerpr : {d.get('fingerprint', '')}",
"Priv Key : [HIDDEN] (vv to reveal)",
f"Priv Key : {priv_val}",
]
action_rows = [
"▣ Copy Pub (c) ▣ Reveal Priv (vv)",
@@ -157,10 +213,22 @@ class NostrBoard(BaseBoard):
"""Matches 'Nostr Board.png' mockup."""
def render(self) -> str:
d = self.entry_data
r = self.reveal_data
if r.get("prompt"):
return self._render_card("SECURITY ALERT", r["prompt"].splitlines())
nsec_val = "[HIDDEN] (v to reveal)"
if r.get("content") and "##" not in r["content"]:
nsec_val = f"[b]{r['content']}[/b]"
if r.get("content") and "##" in r["content"]:
return self._render_card("NOSTR QR CODE", r["content"].splitlines())
cred_rows = [
f"Label : {d.get('label', '')}",
f"npub : {d.get('npub', '')[:20]}...",
"nsec : [HIDDEN] (v to reveal)",
f"nsec : {nsec_val}",
]
action_rows = [
"▣ Reveal (v) ▣ QR (g)",