chore: format codebase and harden v3 CLI/CI gates

This commit is contained in:
thePR0M3TH3AN committed 2026-03-03 21:22:19 -05:00
1 parent 95e5a14b57
commit 538ce42338
27 files changed
+1309 -459

No files matched your search

+32 -1
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import importlib
import importlib.util
import importlib.metadata
import json
import subprocess
import sys
@@ -73,6 +74,22 @@ app.add_typer(semantic.app, name="semantic")
register_capabilities_command(app)
def _get_cli_version() -> str:
"""Return installed package version, falling back to a dev label."""
try:
return importlib.metadata.version("seedpass")
except importlib.metadata.PackageNotFoundError:
return "dev"
def _version_callback(value: bool) -> None:
"""Print version and exit early when --version is provided."""
if not value:
return
typer.echo(f"SeedPass {_get_cli_version()}")
raise typer.Exit()
def run() -> None:
"""Invoke the CLI, handling SeedPass errors gracefully."""
try:
@@ -128,12 +145,25 @@ def _prime_tui2_service(
def _launch_legacy_tui(*, fingerprint: Optional[str]) -> typer.Exit:
"""Launch legacy interactive TUI and return Typer exit."""
tui = importlib.import_module("main")
return typer.Exit(tui.main(fingerprint=fingerprint))
legacy_main = getattr(tui, "main")
# Ensure fallback from newer subcommands (e.g., tui2/tui3) does not leak
# unsupported argv into legacy argparse.
try:
return typer.Exit(legacy_main(argv=[], fingerprint=fingerprint))
except TypeError:
return typer.Exit(legacy_main(fingerprint=fingerprint))
@app.callback(invoke_without_command=True)
def main(
ctx: typer.Context,
version: bool = typer.Option(
False,
"--version",
callback=_version_callback,
is_eager=True,
help="Show SeedPass version and exit",
),
fingerprint: Optional[str] = fingerprint_option,
no_clipboard: bool = no_clipboard_option,
deterministic_totp: bool = deterministic_totp_option,
@@ -147,6 +177,7 @@ def main(
and security feature discovery.
"""
ctx.obj = {
"version": version,
"fingerprint": fingerprint,
"no_clipboard": no_clipboard,
"deterministic_totp": deterministic_totp,
+2
View File
@@ -18,6 +18,8 @@ def build_capabilities() -> dict[str, Any]:
"lock",
"gui",
"tui2",
"tui3",
"legacy",
"capabilities",
"entry",
"vault",
+4 -2
View File
@@ -1157,7 +1157,7 @@ class PasswordManager:
entry = self.entry_manager.retrieve_entry(index)
if not entry:
raise ValueError(f"Entry #{index} not found")
kind = str(entry.get("kind") or entry.get("type") or "").strip().lower()
if kind == EntryType.MANAGED_ACCOUNT.value:
seed = self.entry_manager.get_managed_account_seed(index, self.parent_seed)
@@ -3876,7 +3876,9 @@ class PasswordManager:
color_text(f" Derivation Index: {entry.get('index', index)}", "index")
)
try:
_priv, _pub, pgp_fp = self.entry_manager.get_pgp_key(index, self.parent_seed)
_priv, _pub, pgp_fp = self.entry_manager.get_pgp_key(
index, self.parent_seed
)
if pgp_fp:
print(color_text(f" Fingerprint: {pgp_fp}", "index"))
except Exception as pgp_err: # pragma: no cover - best effort logging
+42 -27
View File
@@ -127,6 +127,7 @@ def launch_tui2(
class SettingsScreen(Screen):
"""Full-screen settings management."""
BINDINGS = [("escape", "app.pop_screen", "Back")]
def compose(self) -> ComposeResult:
@@ -142,7 +143,9 @@ def launch_tui2(
# 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")
self.query_one("#settings-content", Static).update(
"Config service unavailable"
)
return
def get_val(key, default=""):
@@ -158,7 +161,7 @@ def launch_tui2(
f"KDF Mode : {get_val('kdf_mode', 'argon2id')} (setting-kdf-mode <mode>)",
f"Lock Timeout : {get_val('inactivity_timeout', 300)}s (setting-timeout <s>)",
]
backup_rows = [
f"Backup Path : {get_val('additional_backup_path', '(none)')} (db-export <path>)",
f"Backup Interval: {get_val('backup_interval', 3600)}s",
@@ -176,10 +179,13 @@ def launch_tui2(
"",
*app._board_card("Connectivity & Nostr", nostr_rows),
]
self.query_one("#settings-content", Static).update("\n".join(rendered_lines))
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"),
@@ -200,7 +206,7 @@ def launch_tui2(
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)
@@ -919,11 +925,11 @@ def launch_tui2(
nodes.append({"kind": "profile", "fingerprint": fp})
# Auto-expand the active profile if not explicitly tracked
if fp not in self._profile_tree_expanded:
self._profile_tree_expanded[fp] = (fp == current_fp)
self._profile_tree_expanded[fp] = fp == current_fp
if not self._profile_tree_expanded.get(fp, False):
continue
# Only show child nodes for the ACTIVE profile branch for now
# (since searching across all profiles is expensive)
is_active_branch = fp == current_fp or (
@@ -1034,9 +1040,7 @@ def launch_tui2(
elif kind_l == "nostr":
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) ▣ Maximize (z) ▣ doc-export"
)
context = "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) ▣ Maximize (z) ▣ notes/tags"
else:
@@ -1187,7 +1191,9 @@ def launch_tui2(
def _refresh_settings_board(self) -> None:
if self._config_service is None:
self.query_one("#settings-board", Static).update("Config service unavailable")
self.query_one("#settings-board", Static).update(
"Config service unavailable"
)
return
def get_val(key, default=""):
@@ -1203,7 +1209,7 @@ def launch_tui2(
f"KDF Mode: {get_val('kdf_mode', 'argon2id')} (setting-kdf-mode <mode>)",
f"Lock Timeout: {get_val('inactivity_timeout', 300)}s (setting-timeout <s>)",
]
backup_rows = [
f"Backup Path: {get_val('additional_backup_path', '(none)')} (db-export <path>)",
f"Backup Interval: {get_val('backup_interval', 3600)}s",
@@ -1224,25 +1230,27 @@ def launch_tui2(
self.query_one("#settings-board", Static).update("\n".join(rendered_lines))
def _handle_action_strip_click(self, column: int, row: int) -> bool:
adj_row = row - 1
adj_col = column - 1
# This handler receives zero-based coordinates.
adj_row = row
adj_col = column
if adj_row not in {0, 1} or adj_col < 0:
return False
rendered = str(self.query_one("#action-strip", Static).render())
lines = rendered.splitlines()
if not lines or adj_row >= len(lines):
return False
active_line = lines[adj_row]
if not active_line:
return False
import re
if adj_row == 0:
segments = list(re.finditer(r'\S+.*?(?=\s{2,}|$)', active_line))
segments = list(re.finditer(r"\S+.*?(?=\s{2,}|$)", active_line))
else:
segments = list(re.finditer(r'[^▣]+', active_line))
segments = list(re.finditer(r"[^▣]+", active_line))
for match in segments:
start, end = match.span()
@@ -1670,11 +1678,11 @@ def launch_tui2(
label = item if item != "(default)" else "default"
fp_short = item if len(item) <= 12 else f"{item[:9]}..."
active_branch = item == current_fp
# Add expand/collapse indicator
expanded = self._profile_tree_expanded.get(item, False)
exp_ind = "[-] " if expanded else "[+] "
branch_suffix = ""
if active_branch:
managed_count = len(
@@ -1684,7 +1692,7 @@ def launch_tui2(
[n for n in nodes if str(n.get("kind")) == "agent"]
)
branch_suffix = f" M:{managed_count} A:{agent_count}"
tree_lines.append(
f"{cursor} {marker} {exp_ind}{label[:12]:<12} | Seed: {fp_short}{branch_suffix}"
)
@@ -3126,8 +3134,10 @@ def launch_tui2(
f"To proceed, press '{action_key}' again within 8s."
)
self._set_secret_panel(prompt, state="HIDDEN")
self._set_status(f"Confirmation required (press '{action_key}' again)")
self._set_status(
f"Sensitive action requires confirmation (press '{action_key}' again)"
)
self._pending_sensitive_confirm = (
"show_qr" if include_qr else "reveal_selected",
int(self._selected_entry_id),
@@ -3454,14 +3464,18 @@ def launch_tui2(
raise ValueError("copy field unsupported for ssh: public|private")
if kind == "pgp":
private_key, public_key, fingerprint_text = self._service.get_pgp_key(entry_id)
private_key, public_key, fingerprint_text = self._service.get_pgp_key(
entry_id
)
if key in {"private", "private_key"}:
return str(private_key), True, "private_key"
if key in {"public", "public_key"}:
return str(public_key), False, "public_key"
if key in {"fingerprint", "fpr"}:
return str(fingerprint_text), False, "fingerprint"
raise ValueError("copy field unsupported for pgp: private|public|fingerprint")
raise ValueError(
"copy field unsupported for pgp: private|public|fingerprint"
)
if kind == "nostr":
npub, nsec = self._service.get_nostr_key_pair(entry_id)
@@ -4563,6 +4577,7 @@ def launch_tui2(
)
self._active_profile_fp = fp
self._root_profile_fp = self._active_profile_key()
self._profile_tree_expanded[fp] = True
self._managed_session_stack = []
self._managed_session_entry_id = None
self._restore_filter_for_active_profile(default_filter="all")
@@ -5618,7 +5633,7 @@ def launch_tui2(
else ""
)
if kind not in {"managed_account", "seed"}:
self._set_status("Selected entry is not a loadable profile (managed_account or seed)")
self._set_status("Selected entry is not a managed account or seed")
return
managed_fp = (
str(entry.get("fingerprint") or "").strip()
@@ -6098,7 +6113,7 @@ def launch_tui2(
self._set_status(status)
def action_shortcut_settings(self) -> None:
self.action_toggle_settings()
self._open_palette_with_prefix("setting-", "Settings shortcut opened")
def action_toggle_settings(self) -> None:
if self._session_locked:
+5 -3
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import importlib.util
from typing import Any
def check_tui3_runtime() -> dict[str, Any]:
"""Return runtime capability diagnostics for TUI v3."""
textual_available = importlib.util.find_spec("textual") is not None
@@ -16,6 +17,7 @@ def check_tui3_runtime() -> dict[str, Any]:
),
}
def launch_tui3(
*,
fingerprint: str | None = None,
@@ -35,7 +37,7 @@ def launch_tui3(
return False
from .app import SeedPassTuiV3
app = SeedPassTuiV3(
fingerprint=fingerprint,
entry_service_factory=entry_service_factory,
@@ -47,10 +49,10 @@ def launch_tui3(
vault_service_factory=vault_service_factory,
semantic_service_factory=semantic_service_factory,
)
if callable(app_hook):
app_hook(app)
return True
app.run()
return True
+126 -45
View File
@@ -36,18 +36,21 @@ def render_qr_ascii(data: str) -> str:
class CommandProcessor:
"""Handles logic for palette commands in TUI v3."""
def __init__(self, app: SeedPassTuiV3):
self.app = app
def execute(self, raw: str) -> None:
import shlex
try:
parts = shlex.split(raw)
except Exception as e:
self.app.notify(f"Parse error: {e}", severity="error")
return
if not parts: return
if not parts:
return
cmd = parts[0].lower()
args = parts[1:]
@@ -77,7 +80,9 @@ class CommandProcessor:
self.app.action_search(query)
elif cmd == "search-mode":
if not args:
self.app.notify("Usage: search-mode <keyword|hybrid|semantic>", severity="warning")
self.app.notify(
"Usage: search-mode <keyword|hybrid|semantic>", severity="warning"
)
return
mode = args[0].lower()
if mode in {"keyword", "hybrid", "semantic"}:
@@ -87,7 +92,9 @@ class CommandProcessor:
self.app.notify(f"Invalid search mode: {mode}", severity="error")
elif cmd == "filter":
if not args:
self.app.notify("Usage: filter <all|secrets|docs|keys|2fa>", severity="warning")
self.app.notify(
"Usage: filter <all|secrets|docs|keys|2fa>", severity="warning"
)
return
self.app.action_set_kind_filter(args[0])
elif cmd == "archived":
@@ -135,8 +142,10 @@ class CommandProcessor:
else:
self.app.notify(f"Unknown v3 command: {cmd}", severity="warning")
class BrandFingerprint(Static):
"""Placeholder for the top left fingerprint block matching mockups."""
def render(self) -> str:
app = self.app
fp = app.active_breadcrumb or "No Profile"
@@ -145,6 +154,7 @@ class BrandFingerprint(Static):
def on_mount(self) -> None:
self.watch(self.app, "active_breadcrumb", self.refresh)
class MainScreen(Screen):
def compose(self) -> ComposeResult:
yield CommandPalette(id="palette")
@@ -160,10 +170,12 @@ class MainScreen(Screen):
yield BoardContainer(id="board-container")
yield ActionBar(id="action-bar")
class SeedPassTuiV3(App[None]):
"""
SeedPass TUI v3 - Rebuilt from scratch for modularity and mockup fidelity.
"""
CSS = """
#brand-fingerprint {
background: #000000;
@@ -187,7 +199,7 @@ class SeedPassTuiV3(App[None]):
color: #3ce79c;
}
"""
BINDINGS = [
Binding("q", "quit", "Quit", show=True),
Binding("ctrl+p", "open_palette", "Palette", show=True),
@@ -254,13 +266,15 @@ class SeedPassTuiV3(App[None]):
self.services[name] = factory()
except Exception as e:
self.log(f"Failed to init service {name}: {e}")
self.active_fingerprint = self._initial_fingerprint or ""
self.push_screen(MainScreen())
# Global UI Heartbeat (for 2FA ticking etc)
self.set_interval(1.0, self.action_refresh_ui_quiet)
def on_command_palette_command_executed(self, message: CommandPalette.CommandExecuted) -> None:
def on_command_palette_command_executed(
self, message: CommandPalette.CommandExecuted
) -> None:
"""Handle command from palette."""
self.processor.execute(message.command)
@@ -288,19 +302,23 @@ class SeedPassTuiV3(App[None]):
"""Refresh components when the profile changes."""
if not new_fp:
return
try:
mgr = self.services["vault"]._manager
paths = []
for fp, _, _ in getattr(mgr, "profile_stack", []):
paths.append(fp[:8])
current = mgr.current_fingerprint[:8] if mgr.current_fingerprint else "???"
if paths:
paths.append(current)
self.active_breadcrumb = " > ".join(paths)
else:
self.active_breadcrumb = mgr.current_fingerprint[:24] if mgr.current_fingerprint else "No Profile"
self.active_breadcrumb = (
mgr.current_fingerprint[:24]
if mgr.current_fingerprint
else "No Profile"
)
except Exception:
self.active_breadcrumb = new_fp[:24]
@@ -365,6 +383,7 @@ class SeedPassTuiV3(App[None]):
self.notify("Vault is locked", severity="error")
return
from .screens.add import AddEntryScreen
self.push_screen(AddEntryScreen())
def action_seed_plus(self) -> None:
@@ -373,6 +392,7 @@ class SeedPassTuiV3(App[None]):
self.notify("Vault is locked", severity="error")
return
from .screens.add import SeedPlusScreen
self.push_screen(SeedPlusScreen())
def action_maximize_inspector(self) -> None:
@@ -433,11 +453,11 @@ class SeedPassTuiV3(App[None]):
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, mode: str = "default", confirm: bool = False) -> None:
@@ -460,18 +480,18 @@ class SeedPassTuiV3(App[None]):
return
if self.selected_entry_id is None:
return
try:
entry = self.services["entry"].retrieve_entry(self.selected_entry_id)
is_archived = entry.get("archived", False)
if is_archived:
self.services["entry"].restore_entry(self.selected_entry_id)
self.notify(f"Restored Entry #{self.selected_entry_id}")
else:
self.services["entry"].archive_entry(self.selected_entry_id)
self.notify(f"Archived Entry #{self.selected_entry_id}")
# Refresh UI
self.action_refresh()
except Exception as e:
@@ -507,17 +527,21 @@ class SeedPassTuiV3(App[None]):
return
if self.selected_entry_id is None:
return
try:
entry = self.services["entry"].retrieve_entry(self.selected_entry_id)
kind = str(entry.get("kind") or entry.get("type") or "").lower()
if kind not in {"managed_account", "seed"}:
self.notify("Selected entry is not a loadable profile", severity="warning")
self.notify(
"Selected entry is not a loadable profile", severity="warning"
)
return
self.services["entry"].load_managed_account(self.selected_entry_id)
# Update reactive state to trigger UI refresh
self.active_fingerprint = self.services["vault"]._manager.current_fingerprint
self.active_fingerprint = self.services[
"vault"
]._manager.current_fingerprint
self.notify(f"Loaded session: {self.active_fingerprint[:8]}...")
self.action_refresh()
except Exception as e:
@@ -528,7 +552,9 @@ class SeedPassTuiV3(App[None]):
try:
self.services["entry"].exit_managed_account()
# Update reactive state
self.active_fingerprint = self.services["vault"]._manager.current_fingerprint
self.active_fingerprint = self.services[
"vault"
]._manager.current_fingerprint
self.notify(f"Exited session. Back to: {self.active_fingerprint[:8]}...")
self.action_refresh()
except Exception as e:
@@ -541,9 +567,10 @@ class SeedPassTuiV3(App[None]):
return
if self.selected_entry_id is None:
return
try:
from .screens.edit import EditEntryScreen
self.push_screen(EditEntryScreen(self.selected_entry_id))
except Exception as e:
self.notify(f"Edit failed: {e}", severity="error")
@@ -561,7 +588,9 @@ class SeedPassTuiV3(App[None]):
kind = str(entry.get("kind") or entry.get("type") or "").lower()
if kind in {"document", "note"}:
path = self.services["entry"].export_document_file(self.selected_entry_id)
path = self.services["entry"].export_document_file(
self.selected_entry_id
)
self.notify(f"Document exported to: {path}")
elif kind in {"ssh", "pgp", "nostr"}:
payload = self._resolve_sensitive_payload()
@@ -569,8 +598,14 @@ class SeedPassTuiV3(App[None]):
return
# format: [copy_val, label, public_prefix, sec, pub_prefix, pub]
_, label, _, sec, _, pub = payload
safe_label = "".join(c for c in str(label) if c.isalnum() or c in ('-', '_')).strip() or f"entry_{self.selected_entry_id}"
safe_label = (
"".join(
c for c in str(label) if c.isalnum() or c in ("-", "_")
).strip()
or f"entry_{self.selected_entry_id}"
)
from pathlib import Path
base_path = Path.cwd() / f"{safe_label}_{kind}"
Path(f"{base_path}_pub.txt").write_text(str(pub), encoding="utf-8")
Path(f"{base_path}_sec.txt").write_text(str(sec), encoding="utf-8")
@@ -581,13 +616,21 @@ class SeedPassTuiV3(App[None]):
return
# format: [copy_val, label, pub_p, secret, sec_p, uri]
_, label, _, secret, _, uri = payload
safe_label = "".join(c for c in str(label) if c.isalnum() or c in ('-', '_')).strip() or f"entry_{self.selected_entry_id}"
safe_label = (
"".join(
c for c in str(label) if c.isalnum() or c in ("-", "_")
).strip()
or f"entry_{self.selected_entry_id}"
)
from pathlib import Path
path = Path.cwd() / f"{safe_label}_totp.txt"
path.write_text(f"Secret: {secret}\nURI: {uri}", encoding="utf-8")
self.notify(f"Exported TOTP info to {path.name}")
else:
self.notify(f"Export not supported for kind '{kind}'", severity="warning")
self.notify(
f"Export not supported for kind '{kind}'", severity="warning"
)
except Exception as e:
self.notify(f"Export failed: {e}", severity="error")
@@ -598,6 +641,7 @@ class SeedPassTuiV3(App[None]):
return
try:
from seedpass.core.api import VaultExportRequest
req = VaultExportRequest(path=path)
self.services["vault"].export_vault(req)
self.notify(f"Exported DB to {path}")
@@ -611,14 +655,17 @@ class SeedPassTuiV3(App[None]):
return
try:
from seedpass.core.api import VaultImportRequest
req = VaultImportRequest(path=path)
self.services["vault"].import_vault(req)
self.notify(f"Imported DB from {path}")
self.action_refresh()
except Exception as e:
self.notify(f"Import DB failed: {e}", severity="error")
def _consume_confirm(self, action: str, eid: int) -> bool:
if self._pending_sensitive_confirm is None: return False
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:
@@ -627,7 +674,9 @@ class SeedPassTuiV3(App[None]):
self._pending_sensitive_confirm = None
return False
def _show_sensitive_view(self, include_qr: bool, qr_mode: str = "default", confirm: bool = False) -> None:
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
@@ -638,15 +687,25 @@ class SeedPassTuiV3(App[None]):
# Check if confirmation is required
requires = False
if include_qr:
requires = kind in {"seed", "managed_account", "nostr"} # for nostr qr we often show nsec if mode is private
requires = kind in {
"seed",
"managed_account",
"nostr",
} # for nostr qr we often show nsec if mode is private
else:
requires = kind in {"seed", "managed_account", "ssh", "pgp", "nostr"}
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())
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._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
@@ -661,48 +720,68 @@ class SeedPassTuiV3(App[None]):
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")
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")
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)
val = self.services["entry"].generate_password(
int(entry.get("length", 16)), eid
)
return ("Password Revealed", 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", 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)
try:
phrase = self.services["entry"].get_seed_phrase(eid, parent_seed)
except TypeError:
phrase = self.services["entry"].get_seed_phrase(eid)
else:
phrase = self.services["entry"].get_managed_account_seed(eid, parent_seed)
try:
phrase = self.services["entry"].get_managed_account_seed(
eid, parent_seed
)
except TypeError:
phrase = self.services["entry"].get_managed_account_seed(eid)
from seedpass.core.seedqr import encode_seedqr
return ("Seed Words Revealed", phrase, encode_seedqr(phrase), phrase, kind)
if kind == "ssh":
priv, pub = self.services["entry"].get_ssh_key_pair(eid)
return ("SSH Private Key Revealed", priv, pub, pub, kind)
if kind == "pgp":
priv, pub, fp = self.services["entry"].get_pgp_key(eid)
pgp_payload = self.services["entry"].get_pgp_key(eid)
if isinstance(pgp_payload, tuple) and len(pgp_payload) == 3:
priv, pub, _fp = pgp_payload
elif isinstance(pgp_payload, tuple) and len(pgp_payload) == 2:
priv, pub = pgp_payload
else:
raise ValueError("Invalid PGP payload from entry service")
return ("PGP Private Key Revealed", priv, pub, pub, kind)
if kind == "nostr":
npub, nsec = self.services["entry"].get_nostr_key_pair(eid)
qr_data = nsec if qr_mode == "private" else f"nostr:{npub}"
return ("Nostr Secret Revealed", nsec, qr_data, nsec, kind)
if kind == "key_value":
val = entry.get("value", "")
return ("Key-Value Revealed", val, None, val, kind)
if kind in {"document", "note"}:
content = entry.get("content", "")
return ("Document Content", content, None, content, kind)
@@ -710,10 +789,12 @@ class SeedPassTuiV3(App[None]):
# 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):
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
+72 -29
View File
@@ -2,22 +2,32 @@ from __future__ import annotations
from typing import Any
from textual.app import ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Input, Label, Button, Select, TextArea, Checkbox
from textual.widgets import (
Header,
Footer,
Input,
Label,
Button,
Select,
TextArea,
Checkbox,
)
from textual.containers import Vertical, Horizontal, Container, Grid
from textual.binding import Binding
from seedpass.core.entry_types import EntryType
class AddEntryScreen(Screen):
"""
A unified wizard-style screen for adding new entries.
Supports selecting entry kind and filling relevant fields.
"""
BINDINGS = [
Binding("ctrl+s", "save", "Save", show=True),
Binding("escape", "app.pop_screen", "Cancel", show=True),
]
CSS = """
AddEntryScreen {
background: #999999;
@@ -69,35 +79,51 @@ class AddEntryScreen(Screen):
yield Header()
with Container(id="add-container"):
yield Label("ADD NEW ENTRY", id="screen-title")
yield Label("Kind", classes="field-label")
kinds = [(k.value.title(), k.value) for k in EntryType]
# Seed/managed-account derivations are handled by SeedPlusScreen.
supported_kinds = (
EntryType.PASSWORD,
EntryType.TOTP,
EntryType.DOCUMENT,
EntryType.KEY_VALUE,
EntryType.SSH,
EntryType.PGP,
EntryType.NOSTR,
)
kinds = [(k.value.title(), k.value) for k in supported_kinds]
yield Select(kinds, value="password", id="kind-select")
yield Label(
"Use `seed-plus` for Seed / Managed Account derivations.",
id="seedplus-hint",
)
yield Label("Label", classes="field-label")
yield Input(placeholder="Label (e.g. Gmail)", id="entry-label")
# Common fields
with Vertical(id="fields-common"):
yield Label("Username / Email", classes="field-label", id="lbl-username")
yield Label(
"Username / Email", classes="field-label", id="lbl-username"
)
yield Input(placeholder="Username", id="entry-username")
yield Label("URL", classes="field-label", id="lbl-url")
yield Input(placeholder="https://...", id="entry-url")
# Specialized fields (initially hidden/shown based on kind)
with Vertical(id="fields-password", classes="field-group"):
yield Label("Password Length", classes="field-label")
yield Input(value="16", id="entry-length")
with Vertical(id="fields-totp", classes="field-group hidden"):
yield Label("TOTP Secret (Base32)", classes="field-label")
yield Input(placeholder="JBSWY3DPEHPK3PXP", id="entry-totp-secret")
with Vertical(id="fields-document", classes="field-group hidden"):
yield Label("Content", classes="field-label")
yield TextArea(id="entry-content")
with Vertical(id="fields-keyvalue", classes="field-group hidden"):
yield Label("Key", classes="field-label")
yield Input(placeholder="Key", id="entry-key")
@@ -106,7 +132,7 @@ class AddEntryScreen(Screen):
yield Label("Tags (comma separated)", classes="field-label")
yield Input(placeholder="work, personal", id="entry-tags")
with Horizontal(id="action-row"):
yield Button("Cancel (Esc)", id="btn-cancel", variant="error")
yield Button("Save (Ctrl+S)", id="btn-save", variant="success")
@@ -120,7 +146,7 @@ class AddEntryScreen(Screen):
# Hide all groups first
for group in self.query(".field-group"):
group.add_class("hidden")
# Show specific ones
if kind == "password":
self.query_one("#fields-password").remove_class("hidden")
@@ -130,7 +156,7 @@ class AddEntryScreen(Screen):
self.query_one("#fields-document").remove_class("hidden")
elif kind == "key_value":
self.query_one("#fields-keyvalue").remove_class("hidden")
# Toggle common fields visibility if needed
common = self.query_one("#fields-common")
if kind in {"ssh", "pgp", "nostr", "seed", "managed_account"}:
@@ -149,7 +175,7 @@ class AddEntryScreen(Screen):
label = self.query_one("#entry-label", Input).value
tags_raw = self.query_one("#entry-tags", Input).value
tags = [t.strip() for t in tags_raw.split(",") if t.strip()]
if not label:
self.app.notify("Label is required", severity="error")
return
@@ -160,11 +186,15 @@ class AddEntryScreen(Screen):
length = int(self.query_one("#entry-length", Input).value or 16)
username = self.query_one("#entry-username", Input).value
url = self.query_one("#entry-url", Input).value
service.add_entry(label=label, username=username, url=url, length=length, tags=tags)
service.add_entry(
label=label, username=username, url=url, length=length, tags=tags
)
elif kind == "totp":
secret = self.query_one("#entry-totp-secret", Input).value
username = self.query_one("#entry-username", Input).value
service.add_totp(label=label, secret=secret, username=username, tags=tags)
service.add_totp(
label=label, secret=secret, username=username, tags=tags
)
elif kind == "ssh":
service.add_ssh_key(label=label, tags=tags)
elif kind == "pgp":
@@ -179,7 +209,10 @@ class AddEntryScreen(Screen):
content = self.query_one("#entry-content", TextArea).text
service.add_document(label=label, content=content, tags=tags)
else:
self.app.notify(f"Adding '{kind}' not yet implemented in v3 screen", severity="warning")
self.app.notify(
f"Adding '{kind}' not yet implemented in v3 screen",
severity="warning",
)
return
self.app.notify(f"Added {kind}: {label}")
@@ -188,10 +221,12 @@ class AddEntryScreen(Screen):
except Exception as e:
self.app.notify(f"Failed to add entry: {e}", severity="error")
class SeedPlusScreen(Screen):
"""
Specialized screen for BIP-85 derivations (Seeds and Managed Accounts).
"""
BINDINGS = [
Binding("ctrl+s", "save", "Save", show=True),
Binding("escape", "app.pop_screen", "Cancel", show=True),
@@ -228,19 +263,23 @@ class SeedPlusScreen(Screen):
yield Header()
with Container(id="seed-container"):
yield Label("SEED+ / BIP-85 DERIVATION", id="screen-title")
yield Label("Derivation Type", classes="field-label")
yield Select([("New BIP-39 Seed", "seed"), ("Managed Account", "managed_account")], value="seed", id="seed-kind")
yield Select(
[("New BIP-39 Seed", "seed"), ("Managed Account", "managed_account")],
value="seed",
id="seed-kind",
)
yield Label("Label", classes="field-label")
yield Input(placeholder="Label", id="seed-label")
yield Label("Index", classes="field-label")
yield Input(value="0", id="seed-index")
yield Label("Tags", classes="field-label")
yield Input(placeholder="tags", id="seed-tags")
with Horizontal(id="action-row"):
yield Button("Cancel", id="btn-cancel", variant="error")
yield Button("Derive & Save", id="btn-save", variant="success")
@@ -260,8 +299,12 @@ class SeedPlusScreen(Screen):
except ValueError:
self.app.notify("Index must be an integer", severity="error")
return
tags = [t.strip() for t in self.query_one("#seed-tags", Input).value.split(",") if t.strip()]
tags = [
t.strip()
for t in self.query_one("#seed-tags", Input).value.split(",")
if t.strip()
]
if not label:
self.app.notify("Label is required", severity="error")
@@ -273,7 +316,7 @@ class SeedPlusScreen(Screen):
service.add_seed(label=label, index=index, tags=tags)
else:
service.add_managed_account(label=label, index=index, tags=tags)
self.app.notify(f"Derived {kind}: {label} (Index {index})")
self.app.action_refresh()
self.app.pop_screen()
+42 -24
View File
@@ -6,17 +6,18 @@ from textual.widgets import Header, Footer, Input, TextArea, Label, Button
from textual.containers import Vertical, Horizontal, Container
from textual.binding import Binding
class EditEntryScreen(Screen):
"""
Dedicated full-screen editor for all entries.
Auto-detects the entry kind and shows relative fields.
"""
BINDINGS = [
Binding("ctrl+s", "save", "Save", show=True),
Binding("escape", "app.pop_screen", "Cancel", show=True),
]
CSS = """
EditEntryScreen {
background: #999999;
@@ -67,32 +68,52 @@ class EditEntryScreen(Screen):
def on_mount(self) -> None:
if "entry" in self.app.services:
self.entry_data = self.app.services["entry"].retrieve_entry(self.entry_id)
self.kind = str(self.entry_data.get("kind") or self.entry_data.get("type") or "").lower()
self.kind = str(
self.entry_data.get("kind") or self.entry_data.get("type") or ""
).lower()
# Common fields
self.query_one("#entry-title", Input).value = self.entry_data.get("label", "")
self.query_one("#entry-tags", Input).value = ", ".join(self.entry_data.get("tags", []) or [])
self.query_one("#entry-notes", TextArea).text = self.entry_data.get("notes", "")
self.query_one("#entry-title", Input).value = self.entry_data.get(
"label", ""
)
self.query_one("#entry-tags", Input).value = ", ".join(
self.entry_data.get("tags", []) or []
)
self.query_one("#entry-notes", TextArea).text = self.entry_data.get(
"notes", ""
)
# Kind specific fields
if self.kind in {"password", "totp"}:
self.query_one("#fields-user-url").remove_class("hidden")
self.query_one("#entry-username", Input).value = self.entry_data.get("username", "")
self.query_one("#entry-url", Input).value = self.entry_data.get("url", "")
self.query_one("#entry-username", Input).value = self.entry_data.get(
"username", ""
)
self.query_one("#entry-url", Input).value = self.entry_data.get(
"url", ""
)
elif self.kind == "key_value":
self.query_one("#fields-keyvalue").remove_class("hidden")
self.query_one("#entry-key", Input).value = self.entry_data.get("key", "")
self.query_one("#entry-value", Input).value = self.entry_data.get("value", "")
self.query_one("#entry-key", Input).value = self.entry_data.get(
"key", ""
)
self.query_one("#entry-value", Input).value = self.entry_data.get(
"value", ""
)
elif self.kind in {"document", "note"}:
self.query_one("#fields-document").remove_class("hidden")
self.query_one("#doc-type", Input).value = self.entry_data.get("file_type", "txt")
self.query_one("#doc-content", TextArea).text = self.entry_data.get("content", "")
self.query_one("#doc-type", Input).value = self.entry_data.get(
"file_type", "txt"
)
self.query_one("#doc-content", TextArea).text = self.entry_data.get(
"content", ""
)
def compose(self) -> ComposeResult:
yield Header()
with Container(id="editor-container"):
yield Label(f"EDIT ENTRY #{self.entry_id}", id="screen-title")
with Horizontal(id="edit-row"):
with Vertical(id="col-left"):
yield Label("Title / Label", classes="field-label")
@@ -100,20 +121,20 @@ class EditEntryScreen(Screen):
with Vertical(id="col-right"):
yield Label("Tags (comma separated)", classes="field-label")
yield Input(placeholder="work, personal...", id="entry-tags")
# Optional fields
with Vertical(id="fields-user-url", classes="hidden"):
yield Label("Username / Email", classes="field-label")
yield Input(placeholder="Username", id="entry-username")
yield Label("URL", classes="field-label")
yield Input(placeholder="https://...", id="entry-url")
with Vertical(id="fields-keyvalue", classes="hidden"):
yield Label("Key", classes="field-label")
yield Input(placeholder="Key", id="entry-key")
yield Label("Value", classes="field-label")
yield Input(placeholder="Value", id="entry-value")
with Vertical(id="fields-document", classes="hidden"):
yield Label("File Type", classes="field-label")
yield Input(placeholder="txt", id="doc-type")
@@ -122,7 +143,7 @@ class EditEntryScreen(Screen):
yield Label("Notes", classes="field-label")
yield TextArea(id="entry-notes")
with Horizontal(id="action-row"):
yield Button("Cancel (Esc)", id="btn-cancel", variant="error")
yield Button("Save (Ctrl+S)", id="btn-save", variant="success")
@@ -139,7 +160,7 @@ class EditEntryScreen(Screen):
tags_raw = self.query_one("#entry-tags", Input).value
notes = self.query_one("#entry-notes", TextArea).text
tags = [t.strip() for t in tags_raw.split(",") if t.strip()]
update_args = {
"label": title,
"tags": tags,
@@ -156,12 +177,9 @@ class EditEntryScreen(Screen):
elif self.kind in {"document", "note"}:
update_args["file_type"] = self.query_one("#doc-type", Input).value
update_args["content"] = self.query_one("#doc-content", TextArea).text
try:
self.app.services["entry"].modify_entry(
self.entry_id,
**update_args
)
self.app.services["entry"].modify_entry(self.entry_id, **update_args)
self.app.notify(f"Entry #{self.entry_id} saved successfully")
self.app.action_refresh()
self.app.pop_screen()
+23 -13
View File
@@ -6,16 +6,18 @@ 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"),
("v", "reveal", "Reveal Secret"),
@@ -71,15 +73,21 @@ class MaximizedInspectorScreen(Screen):
r = self.reveal_data
if eid is None:
self.query_one("#max-inspector-content", Static).update("No entry selected.")
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']}")
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']}")
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:
@@ -90,13 +98,13 @@ class MaximizedInspectorScreen(Screen):
return
# Simple render for the full screen view
label = entry.get('label', 'Unknown')
kind = entry.get('kind', 'Unknown')
label = entry.get("label", "Unknown")
kind = entry.get("kind", "Unknown")
# Build a large detail view
header = f"[b]ENTRY #{eid} - {label.upper()} ({kind})[/b]\n"
divider = "=" * 60 + "\n"
# Meta info
meta_lines = [
f"Username : {entry.get('username', '(none)')}",
@@ -104,14 +112,16 @@ class MaximizedInspectorScreen(Screen):
f"Created : {entry.get('created_at', '(unknown)')}",
f"Modified : {entry.get('modified_at', '(unknown)')}",
]
# Handle revealed content
content = entry.get('content', '')
content = entry.get("content", "")
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 ""
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)
+21 -10
View File
@@ -5,11 +5,12 @@ from textual.screen import Screen
from textual.widgets import Static, Footer, DataTable, Input, Button
from textual.containers import Vertical, Horizontal
class RelaysScreen(Screen):
"""
A dedicated screen for managing Nostr relays and triggering synchronization.
"""
BINDINGS = [
("escape", "app.pop_screen", "Back to Vault"),
("r", "refresh_relays", "Refresh"),
@@ -83,7 +84,10 @@ class RelaysScreen(Screen):
with Horizontal(id="relays-controls"):
yield Input(placeholder="wss://relay.example.com", id="relay-input")
yield Button("Add Relay", id="add-button")
yield Static("ESC: Exit | R: Refresh | D: Delete Selected | S: Sync Now", id="relays-footer")
yield Static(
"ESC: Exit | R: Refresh | D: Delete Selected | S: Sync Now",
id="relays-footer",
)
def on_mount(self) -> None:
table = self.query_one(DataTable)
@@ -107,7 +111,7 @@ class RelaysScreen(Screen):
table.clear()
for idx, url in enumerate(relays):
table.add_row("🟢 Active", url, key=str(idx))
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "add-button":
self._add_relay()
@@ -121,21 +125,23 @@ class RelaysScreen(Screen):
url = inp.value.strip()
if not url:
return
if not url.startswith("ws://") and not url.startswith("wss://"):
self.app.notify("Relay URL must start with ws:// or wss://", severity="error")
self.app.notify(
"Relay URL must start with ws:// or wss://", severity="error"
)
return
service = self.app.services.get("nostr")
if not service:
return
try:
relays = service.list_relays()
if url in relays:
self.app.notify("Relay already exists", severity="warning")
return
service.add_relay(url)
inp.value = ""
self.action_refresh_relays()
@@ -160,7 +166,7 @@ class RelaysScreen(Screen):
service = self.app.services.get("nostr")
if not service:
return
try:
service.remove_relay(idx)
self.action_refresh_relays()
@@ -181,8 +187,13 @@ class RelaysScreen(Screen):
sync_service = self.app.services.get("sync")
try:
import traceback
sync_service.sync()
self.app.call_from_thread(self.app.notify, "Synchronization complete", severity="information")
self.app.call_from_thread(
self.app.notify, "Synchronization complete", severity="information"
)
self.app.call_from_thread(self.app.action_refresh)
except Exception as e:
self.app.call_from_thread(self.app.notify, f"Sync failed: {e}", severity="error")
self.app.call_from_thread(
self.app.notify, f"Sync failed: {e}", severity="error"
)
+12 -6
View File
@@ -5,12 +5,13 @@ from textual.screen import Screen
from textual.widgets import Static, Footer
from textual.containers import Vertical
class SettingsScreen(Screen):
"""
A dedicated, full-screen settings management interface.
Categorizes configuration into Security, Storage, and Connectivity.
"""
BINDINGS = [
("escape", "app.pop_screen", "Back to Vault"),
("r", "refresh", "Refresh Settings"),
@@ -54,7 +55,10 @@ class SettingsScreen(Screen):
yield Static("SeedPass ◈ System Configuration", id="settings-title")
with Vertical(id="settings-container"):
yield Static("Loading configuration...", id="settings-content")
yield Static("ESC: Exit Settings | R: Reload | Use Palette (Ctrl+P) to change values", id="settings-footer")
yield Static(
"ESC: Exit Settings | R: Reload | Use Palette (Ctrl+P) to change values",
id="settings-footer",
)
def on_mount(self) -> None:
self.action_refresh()
@@ -63,11 +67,13 @@ class SettingsScreen(Screen):
"""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]")
self.query_one("#settings-content", Static).update(
"[red]Config Service Offline[/red]"
)
return
service = app.services["config"]
def get_val(key, default=""):
try:
val = service.get(key)
@@ -83,7 +89,7 @@ class SettingsScreen(Screen):
f"KDF Mode : {get_val('kdf_mode', 'argon2id')} [dim](setting-kdf-mode <mode>)[/dim]",
f"Lock Timeout : {get_val('inactivity_timeout', 300)}s [dim](setting-timeout <s>)[/dim]",
]
backup_rows = [
f"Backup Path : {get_val('additional_backup_path', '(none)')} [dim](db-export <path>)[/dim]",
f"Backup Interval: {get_val('backup_interval', 3600)}s",
@@ -113,6 +119,6 @@ class SettingsScreen(Screen):
"",
render_card("CONNECTIVITY & NOSTR SYNC", nostr_rows),
]
self.query_one("#settings-content", Static).update("\n".join(rendered_lines))
self.app.notify("Settings reloaded")
+26 -16
View File
@@ -4,12 +4,13 @@ from textual.app import ComposeResult
from textual.widgets import Static
from textual.reactive import reactive
class ActionBar(Static):
"""
Bottom action bar showing global shortcuts and context actions.
Matches the 'Settings (S) Add (A) ...' layout.
"""
DEFAULT_CSS = """
ActionBar {
height: auto;
@@ -25,22 +26,29 @@ class ActionBar(Static):
def render(self) -> str:
app = self.app
selected_id = app.selected_entry_id
# Global Row - mockup format
is_managed = False
try:
is_managed = len(getattr(app.services.get("vault")._manager, "profile_stack", [])) > 0
is_managed = (
len(getattr(app.services.get("vault")._manager, "profile_stack", []))
> 0
)
except:
pass
exit_hint = " [@click='app.managed_exit'][b][M][/b]anaged Exit[/]" if is_managed else ""
exit_hint = (
" [@click='app.managed_exit'][b][M][/b]anaged Exit[/]"
if is_managed
else ""
)
global_row = (
f"[@click='app.toggle_settings'][b][S][/b]ettings[/] [@click='app.add_entry'][b][A][/b]dd New Entry[/] "
f"[@click='app.seed_plus'][b][C][/b]reate New Seed[/] "
f"[@click='app.open_palette'][b][R][/b]emove Seed[/] "
f"[@click='app.action_db_export'][b][E][/b]xport Data[/] [@click='app.action_db_import'][b][I][/b]mport Data[/] [@click='app.open_palette'][b][B][/b]ackup Data[/]{exit_hint}"
)
# Context Row (added from previous phase parity)
if selected_id is None:
context = "Select an entry to view actions."
@@ -48,24 +56,26 @@ class ActionBar(Static):
try:
entry = app.services["entry"].retrieve_entry(selected_id)
kind = str(entry.get("kind") or entry.get("type") or "").lower()
actions = [
"[@click='app.reveal_selected'][b][v][/b] Reveal[/]",
"[@click='app.show_qr'][b][g][/b] QR[/]",
"[@click='app.edit_selected'][b][e][/b] Edit[/]",
"[@click='app.toggle_archive'][b][a][/b] Archive[/]",
"[@click='app.copy_selected'][b][c][/b] Copy[/]",
"[@click='app.maximize_inspector'][b][z][/b] Maximize[/]"
"[@click='app.reveal_selected'][b][v][/b] Reveal[/]",
"[@click='app.show_qr'][b][g][/b] QR[/]",
"[@click='app.edit_selected'][b][e][/b] Edit[/]",
"[@click='app.toggle_archive'][b][a][/b] Archive[/]",
"[@click='app.copy_selected'][b][c][/b] Copy[/]",
"[@click='app.maximize_inspector'][b][z][/b] Maximize[/]",
]
if kind in {"managed_account", "seed"}:
actions.insert(4, "[@click='app.managed_load'][b][m][/b] Load[/]")
if kind in {"document", "note"}:
actions.insert(5, "[@click='app.export_selected'][b][x][/b] Export[/]")
actions.insert(
5, "[@click='app.export_selected'][b][x][/b] Export[/]"
)
context = f" Context ({kind}): " + " ▣ ".join(actions)
except Exception:
context = f" Error fetching context details for Entry #{selected_id}"
return f"{global_row}\n{context}"
def on_mount(self) -> None:
+60 -28
View File
@@ -4,11 +4,13 @@ from textual.app import ComposeResult
from textual.widgets import Static, DataTable
from textual.reactive import reactive
class GridMetrics(Static):
"""
Displays pagination, row counts, and search mode state.
Matches the 'Entry Grid | Pg X/X ...' line in the mockups.
"""
DEFAULT_CSS = """
GridMetrics {
height: 1;
@@ -17,7 +19,7 @@ class GridMetrics(Static):
padding: 0 1;
}
"""
def render(self) -> str:
app = self.app
# These would eventually come from App reactives
@@ -26,21 +28,23 @@ class GridMetrics(Static):
try:
table = app.screen.query_one("#entry-data-table")
if table:
rows = f"{len(table.rows)}/{len(table.rows)}" # TODO: handle actual total
rows = (
f"{len(table.rows)}/{len(table.rows)}" # TODO: handle actual total
)
except Exception:
pass
density = "compact"
# Search mode highlighting
m = app.search_mode
keyword = f"[b](KEYWORD)[/b]" if m == "keyword" else "keyword"
hybrid = f"[b](HYBRID)[/b]" if m == "hybrid" else "hybrid"
semantic = f"[b](SEMANTIC)[/b]" if m == "semantic" else "semantic"
search = f"{keyword} {hybrid} {semantic}"
filter_text = f"Filter: [b]{app.filter_kind}[/b]"
arch_text = " | [reverse] ARCHIVED [/reverse]" if app.show_archived else ""
return f"Entry Grid | Pg {pg} Rows {rows} Density {density} {filter_text}{arch_text} Search {search}"
def on_mount(self) -> None:
@@ -49,15 +53,15 @@ class GridMetrics(Static):
self.watch(self.app, "filter_kind", self.refresh)
self.watch(self.app, "show_archived", self.refresh)
class EntryDataTable(DataTable):
"""
High-fidelity data table for entry browsing.
"""
def on_mount(self) -> None:
self.cursor_type = "row"
self.add_columns(
"Sel", "Id", "Entry#", "Label", "Kind", "Meta", "Arch"
)
self.add_columns("Sel", "Id", "Entry#", "Label", "Kind", "Meta", "Arch")
self._refresh_data()
def on_focus(self) -> None:
@@ -75,13 +79,21 @@ class EntryDataTable(DataTable):
# Map filter presets to kind lists
kind_map = {
"secrets": ["password", "totp", "ssh", "pgp", "nostr", "seed", "managed_account"],
"secrets": [
"password",
"totp",
"ssh",
"pgp",
"nostr",
"seed",
"managed_account",
],
"docs": ["document", "note"],
"keys": ["ssh", "pgp", "nostr", "seed", "managed_account"],
"2fa": ["totp"],
}
kinds = kind_map.get(app.filter_kind)
entries = []
# Use semantic search if requested and available
if app.search_mode != "keyword" and "semantic" in app.services and query:
@@ -93,50 +105,68 @@ class EntryDataTable(DataTable):
# Fetch full entry to match table columns
entry = app.services["entry"].retrieve_entry(eid)
if entry:
kind_val = str(entry.get("kind") or entry.get("type") or "").lower()
kind_val = str(
entry.get("kind") or entry.get("type") or ""
).lower()
# Filtering for semantic results (best effort)
if kinds and kind_val not in kinds: continue
if kinds and kind_val not in kinds:
continue
is_arch = bool(entry.get("archived", False))
if app.show_archived and not is_arch: continue
if not app.show_archived and is_arch: continue
if app.show_archived and not is_arch:
continue
if not app.show_archived and is_arch:
continue
from seedpass.core.entry_types import EntryType
try:
etype = EntryType(kind_val)
except ValueError:
etype = kind_val
entries.append((
eid,
entry.get("label", ""),
entry.get("username"),
entry.get("url"),
is_arch,
etype
))
entries.append(
(
eid,
entry.get("label", ""),
entry.get("username"),
entry.get("url"),
is_arch,
etype,
)
)
except Exception as e:
app.notify(f"Semantic search failed: {e}", severity="error")
# Fallback to lexical
entries = app.services["entry"].search_entries(query, kinds=kinds, include_archived=app.show_archived, archived_only=app.show_archived)
entries = app.services["entry"].search_entries(
query,
kinds=kinds,
include_archived=app.show_archived,
archived_only=app.show_archived,
)
else:
# Fetch filtered data from standard service
entries = app.services["entry"].search_entries(query, kinds=kinds, include_archived=app.show_archived, archived_only=app.show_archived)
entries = app.services["entry"].search_entries(
query,
kinds=kinds,
include_archived=app.show_archived,
archived_only=app.show_archived,
)
for i, (eid, label, user, url, arch, kind) in enumerate(entries):
marker = "▶" if eid == app.selected_entry_id else " "
arch_status = "🔒" if arch else " "
# 'Meta' column logic from mockup
meta = user or url or ""
self.add_row(
marker,
str(i+1),
str(i + 1),
f"#{eid}",
label,
kind.value,
meta,
arch_status,
key=str(eid)
key=str(eid),
)
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
@@ -159,10 +189,12 @@ class EntryDataTable(DataTable):
except ValueError:
pass
class GridContainer(Static):
"""
The central workspace container.
"""
def compose(self) -> ComposeResult:
yield GridMetrics()
# Header divider
+13 -10
View File
@@ -3,14 +3,15 @@ from textual.app import ComposeResult
from textual.widgets import Static
from textual.reactive import reactive
class RibbonHeader(Static):
"""
A high-density status ribbon that displays active profile,
A high-density status ribbon that displays active profile,
session state, and vault metrics.
Matches the 'UI Board' mockup top ribbon layout.
"""
DEFAULT_CSS = """
RibbonHeader {
height: 1;
@@ -24,14 +25,16 @@ class RibbonHeader(Static):
def render(self) -> str:
app = self.app
# Gather metrics from actual services
managed = 0
try:
managed = len(getattr(app.services.get("vault")._manager, "profile_stack", []))
managed = len(
getattr(app.services.get("vault")._manager, "profile_stack", [])
)
except:
pass
entries = 0
try:
table = app.screen.query_one("#entry-data-table")
@@ -39,17 +42,17 @@ class RibbonHeader(Static):
entries = len(table.rows)
except:
pass
sync = "YYY-MM-DDThh:mm:ssTZD"
# Build the exact string parts from the mockup
left = f"Managed Users: {managed} Entries: {entries} ◀ ▶ Kind = {app.filter_kind.capitalize()}"
right = f"Last Sync: {sync}"
# Calculate spacing to push sync to the right edge
available_width = self.size.width - len(left) - len(right) - 2
spacer = " " * max(1, available_width)
return f"{left}{spacer}{right}"
def on_mount(self) -> None:
+135 -64
View File
@@ -5,8 +5,10 @@ from textual.widgets import Static, Label
from textual.containers import Vertical, Horizontal
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]]({})
@@ -19,8 +21,10 @@ class BaseBoard(Static):
def update_board(self) -> None:
pass
class InspectorHeader(Static):
"""Reusable high-fidelity header matching the UI Board mockups."""
DEFAULT_CSS = """
InspectorHeader {
height: 3;
@@ -39,24 +43,29 @@ class InspectorHeader(Static):
.edit-btn { background: #ffffff; color: #000000; padding: 0 2; height: 1; margin-top: 1; margin-right: 1; text-style: bold; }
"""
def compose(self) -> ComposeResult:
with Vertical(classes="title-block"):
yield Label("Title", id="hdr-title", classes="title-text")
yield Label("Type", id="hdr-type", classes="type-text")
with Horizontal(classes="meta-block"):
yield Label("[b]Date Modified[/b]\nYYYY-MM-DD", id="hdr-date", classes="meta-item")
yield Label(
"[b]Date Modified[/b]\nYYYY-MM-DD", id="hdr-date", classes="meta-item"
)
yield Label("[b]Black Listed? No[/b]", id="hdr-bl", classes="meta-item")
yield Label("[b]Index Num*:[/b]\n0", id="hdr-idx", classes="meta-item")
yield Label("[b]Entry Num:[/b]\n1", id="hdr-enum", classes="meta-item")
yield Label("[@click=app.edit_selected]Edit[/]", classes="edit-btn")
def update_data(self, entry: dict, kind_label: str):
if not self.is_mounted: return
if not self.is_mounted:
return
try:
self.query_one("#hdr-title", Label).update(f"[b]{entry.get('label', 'Untitled')}[/b]")
self.query_one("#hdr-title", Label).update(
f"[b]{entry.get('label', 'Untitled')}[/b]"
)
self.query_one("#hdr-type", Label).update(kind_label)
idx = str(entry.get('id', 'N/A'))
idx = str(entry.get("id", "N/A"))
self.query_one("#hdr-idx", Label).update(f"[b]Index Num*:[/b]\n{idx}")
self.query_one("#hdr-enum", Label).update(f"[b]Entry Num:[/b]\n{idx}")
except Exception:
@@ -65,12 +74,17 @@ class InspectorHeader(Static):
class IdleBoard(BaseBoard):
"""Shown when no entry is selected."""
def compose(self) -> ComposeResult:
yield Label("\n\n[b]Inspector Idle[/b]\n\nSelect an entry from the grid\nto view details.", id="idle-text")
yield Label(
"\n\n[b]Inspector Idle[/b]\n\nSelect an entry from the grid\nto view details.",
id="idle-text",
)
class PasswordBoard(BaseBoard):
"""Matches 'Password Board.png' mockup."""
DEFAULT_CSS = """
PasswordBoard {
background: #000000;
@@ -97,9 +111,15 @@ class PasswordBoard(BaseBoard):
with Vertical(classes="field-box highlight"):
yield Label("Password*", classes="field-title")
with Horizontal(classes="field-row"):
yield Label("**********", id="fld-pass-val", classes="field-val")
yield Label("[@click=app.copy_selected]Copy[/]", classes="action-btn")
yield Label("[@click=app.add_entry]Create New[/]", classes="action-btn")
yield Label(
"**********", id="fld-pass-val", classes="field-val"
)
yield Label(
"[@click=app.copy_selected]Copy[/]", classes="action-btn"
)
yield Label(
"[@click=app.add_entry]Create New[/]", classes="action-btn"
)
with Vertical(classes="field-box"):
yield Label("Username*", classes="field-title")
yield Label("-", id="fld-user-val")
@@ -114,26 +134,29 @@ class PasswordBoard(BaseBoard):
yield Label("", id="fld-notes-val")
def update_board(self) -> None:
if not self.is_mounted: return
if not self.is_mounted:
return
d = self.entry_data
r = self.reveal_data
try:
self.query_one("#password-header", InspectorHeader).update_data(d, "Password")
self.query_one("#password-header", InspectorHeader).update_data(
d, "Password"
)
pwd = "[b]**********[/b] (v to reveal)"
if r.get("content"):
pwd = r.get("content")
self.query_one("#fld-pass-val", Label).update(pwd)
self.query_one("#fld-user-val", Label).update(d.get("username", "-"))
self.query_one("#fld-url-val", Label).update(d.get("url", "-"))
tags = d.get('tags', [])
tags = d.get("tags", [])
tag_str = ", ".join(tags) if isinstance(tags, list) else str(tags)
self.query_one("#fld-tags-val", Label).update(tag_str or "-")
notes = d.get('notes', '')
notes = d.get("notes", "")
self.query_one("#fld-notes-val", Label).update(notes)
except Exception:
pass
@@ -141,6 +164,7 @@ class PasswordBoard(BaseBoard):
class TotpBoard(BaseBoard):
"""Matches '2FA Board.png' mockup."""
DEFAULT_CSS = """
TotpBoard { background: #000000; color: #ffffff; padding: 0 1; height: 1fr; }
.board-body { layout: horizontal; height: 1fr; }
@@ -162,8 +186,12 @@ class TotpBoard(BaseBoard):
with Vertical(classes="field-box highlight"):
yield Label("2FA Code*", classes="field-title")
with Horizontal(classes="field-row"):
yield Label("------ (30s left)", id="fld-code-val", classes="field-val")
yield Label("[@click=app.copy_selected]Copy[/]", classes="action-btn")
yield Label(
"------ (30s left)", id="fld-code-val", classes="field-val"
)
yield Label(
"[@click=app.copy_selected]Copy[/]", classes="action-btn"
)
with Vertical(classes="field-box"):
yield Label("Secret", classes="field-title")
yield Label("-", id="fld-secret-val")
@@ -174,14 +202,15 @@ class TotpBoard(BaseBoard):
yield Label("QR Code (Press g)", id="fld-qr-val", classes="qr-text")
def update_board(self) -> None:
if not self.is_mounted: return
if not self.is_mounted:
return
d = self.entry_data
r = self.reveal_data
app = self.app
try:
self.query_one("#totp-header", InspectorHeader).update_data(d, "2FA")
code = "------"
if app.services.get("entry") and not app.session_locked:
try:
@@ -189,27 +218,33 @@ class TotpBoard(BaseBoard):
except:
pass
import time
remaining = 30 - (int(time.time()) % 30)
self.query_one("#fld-code-val", Label).update(f"[b]{code}[/b] ({remaining}s left)")
self.query_one("#fld-code-val", Label).update(
f"[b]{code}[/b] ({remaining}s left)"
)
secret = "[b]**********[/b] (v to reveal)"
if r.get("content") and "##" not in r.get("content"):
secret = r.get("content")
self.query_one("#fld-secret-val", Label).update(secret)
self.query_one("#fld-algo-val", Label).update(d.get("algorithm", "SHA1").upper())
self.query_one("#fld-algo-val", Label).update(
d.get("algorithm", "SHA1").upper()
)
qr_content = "QR Code (Press g)"
if r.get("content") and "##" in r.get("content"):
qr_content = r.get("content")
self.query_one("#fld-qr-val", Label).update(qr_content)
except Exception:
pass
class NoteBoard(BaseBoard):
"""Matches 'Note Board.png' mockup."""
DEFAULT_CSS = """
NoteBoard { background: #000000; color: #ffffff; padding: 0 1; height: 1fr; }
.board-body { layout: horizontal; height: 1fr; border: solid #ffffff; padding: 1; }
@@ -221,18 +256,20 @@ class NoteBoard(BaseBoard):
yield Label("Content", id="fld-content-val")
def update_board(self) -> None:
if not self.is_mounted: return
if not self.is_mounted:
return
d = self.entry_data
try:
kind_label = "Note" if d.get('kind', '') == 'note' else "Document"
kind_label = "Note" if d.get("kind", "") == "note" else "Document"
self.query_one("#note-header", InspectorHeader).update_data(d, kind_label)
self.query_one("#fld-content-val", Label).update(d.get('content', ''))
self.query_one("#fld-content-val", Label).update(d.get("content", ""))
except Exception:
pass
class SeedBoard(BaseBoard):
"""Matches 'BIP-39 Seed Board.png' mockup."""
DEFAULT_CSS = """
SeedBoard { background: #000000; color: #ffffff; padding: 0 1; height: 1fr; }
.board-body { layout: horizontal; height: 1fr; }
@@ -254,8 +291,12 @@ class SeedBoard(BaseBoard):
with Vertical(classes="field-box highlight"):
yield Label("Seed Phrase*", classes="field-title")
with Horizontal(classes="field-row"):
yield Label("[HIDDEN]", id="fld-phrase-val", classes="field-val")
yield Label("[@click=app.copy_selected]Copy[/]", classes="action-btn")
yield Label(
"[HIDDEN]", id="fld-phrase-val", classes="field-val"
)
yield Label(
"[@click=app.copy_selected]Copy[/]", classes="action-btn"
)
with Vertical(classes="field-box"):
yield Label("Fingerprint", classes="field-title")
yield Label("-", id="fld-fp-val")
@@ -266,17 +307,24 @@ class SeedBoard(BaseBoard):
yield Label("QR Code (Press gg)", id="fld-qr-val", classes="qr-text")
def update_board(self) -> None:
if not self.is_mounted: return
if not self.is_mounted:
return
d = self.entry_data
r = self.reveal_data
try:
kind_label = "BIP-39" if d.get('kind', '') in {'seed', 'managed_account'} else d.get('kind', 'Seed')
self.query_one("#seed-header", InspectorHeader).update_data(d, kind_label.capitalize())
kind_label = (
"BIP-39"
if d.get("kind", "") in {"seed", "managed_account"}
else d.get("kind", "Seed")
)
self.query_one("#seed-header", InspectorHeader).update_data(
d, kind_label.capitalize()
)
phrase = "[b]**********[/b] (vv to reveal)"
if r.get("content") and "##" not in r.get("content"):
phrase = r.get("content")
self.query_one("#fld-phrase-val", Label).update(phrase)
self.query_one("#fld-fp-val", Label).update(d.get("fingerprint", "-"))
self.query_one("#fld-wc-val", Label).update(str(d.get("word_count", 24)))
@@ -291,6 +339,7 @@ class SeedBoard(BaseBoard):
class SshBoard(BaseBoard):
"""Matches 'SSH Board.png' mockup."""
DEFAULT_CSS = """
SshBoard { background: #000000; color: #ffffff; padding: 0 1; height: 1fr; }
.board-body { layout: horizontal; height: 1fr; }
@@ -312,12 +361,16 @@ class SshBoard(BaseBoard):
yield Label("Private Key*", classes="field-title")
with Horizontal(classes="field-row"):
yield Label("[HIDDEN]", id="fld-priv-val", classes="field-val")
yield Label("[@click=app.copy_selected]Copy[/]", classes="action-btn")
yield Label(
"[@click=app.copy_selected]Copy[/]", classes="action-btn"
)
with Vertical(classes="field-box"):
yield Label("Public Key", classes="field-title")
with Horizontal(classes="field-row"):
yield Label("[PREVIEW]", id="fld-pub-val", classes="field-val")
yield Label("[@click=app.copy_selected]Copy[/]", classes="action-btn")
yield Label(
"[@click=app.copy_selected]Copy[/]", classes="action-btn"
)
with Vertical(classes="field-box"):
yield Label("Key Type", classes="field-title")
yield Label("Ed25519", id="fld-type-val")
@@ -326,7 +379,8 @@ class SshBoard(BaseBoard):
yield Label("", id="fld-notes-val")
def update_board(self) -> None:
if not self.is_mounted: return
if not self.is_mounted:
return
d = self.entry_data
r = self.reveal_data
try:
@@ -336,12 +390,15 @@ class SshBoard(BaseBoard):
if r.get("content"):
priv = r.get("content")
self.query_one("#fld-priv-val", Label).update(priv)
pub = d.get('public_key', '[PREVIEW]')
if len(pub) > 30: pub = pub[:30] + "..."
pub = d.get("public_key", "[PREVIEW]")
if len(pub) > 30:
pub = pub[:30] + "..."
self.query_one("#fld-pub-val", Label).update(pub)
self.query_one("#fld-type-val", Label).update(d.get("key_type", "RSA/Ed25519"))
self.query_one("#fld-type-val", Label).update(
d.get("key_type", "RSA/Ed25519")
)
self.query_one("#fld-notes-val", Label).update(d.get("notes", ""))
except Exception:
pass
@@ -349,6 +406,7 @@ class SshBoard(BaseBoard):
class PgpBoard(BaseBoard):
"""Matches 'PGP Board.png' mockup."""
DEFAULT_CSS = SshBoard.DEFAULT_CSS.replace("SshBoard", "PgpBoard")
def compose(self) -> ComposeResult:
@@ -359,7 +417,9 @@ class PgpBoard(BaseBoard):
yield Label("Private Key*", classes="field-title")
with Horizontal(classes="field-row"):
yield Label("[HIDDEN]", id="fld-priv-val", classes="field-val")
yield Label("[@click=app.copy_selected]Copy[/]", classes="action-btn")
yield Label(
"[@click=app.copy_selected]Copy[/]", classes="action-btn"
)
with Vertical(classes="field-box"):
yield Label("Fingerprint / ID", classes="field-title")
yield Label("-", id="fld-fp-val")
@@ -371,7 +431,8 @@ class PgpBoard(BaseBoard):
yield Label("", id="fld-notes-val")
def update_board(self) -> None:
if not self.is_mounted: return
if not self.is_mounted:
return
d = self.entry_data
r = self.reveal_data
try:
@@ -381,7 +442,7 @@ class PgpBoard(BaseBoard):
if r.get("content"):
priv = r.get("content")
self.query_one("#fld-priv-val", Label).update(priv)
self.query_one("#fld-fp-val", Label).update(d.get("fingerprint", "-"))
self.query_one("#fld-ident-val", Label).update(d.get("user_id", "-"))
self.query_one("#fld-notes-val", Label).update(d.get("notes", ""))
@@ -391,6 +452,7 @@ class PgpBoard(BaseBoard):
class NostrBoard(BaseBoard):
"""Matches 'Nostr Board.png' mockup."""
DEFAULT_CSS = TotpBoard.DEFAULT_CSS.replace("TotpBoard", "NostrBoard")
def compose(self) -> ComposeResult:
@@ -401,12 +463,16 @@ class NostrBoard(BaseBoard):
yield Label("nsec (Secret)*", classes="field-title")
with Horizontal(classes="field-row"):
yield Label("[HIDDEN]", id="fld-nsec-val", classes="field-val")
yield Label("[@click=app.copy_selected]Copy[/]", classes="action-btn")
yield Label(
"[@click=app.copy_selected]Copy[/]", classes="action-btn"
)
with Vertical(classes="field-box"):
yield Label("npub (Public)", classes="field-title")
with Horizontal(classes="field-row"):
yield Label("-", id="fld-npub-val", classes="field-val")
yield Label("[@click=app.copy_selected]Copy[/]", classes="action-btn")
yield Label(
"[@click=app.copy_selected]Copy[/]", classes="action-btn"
)
with Vertical(classes="field-box"):
yield Label("Hex Key", classes="field-title")
yield Label("-", id="fld-hex-val")
@@ -414,7 +480,8 @@ class NostrBoard(BaseBoard):
yield Label("QR Code (Press g)", id="fld-qr-val", classes="qr-text")
def update_board(self) -> None:
if not self.is_mounted: return
if not self.is_mounted:
return
d = self.entry_data
r = self.reveal_data
try:
@@ -424,11 +491,12 @@ class NostrBoard(BaseBoard):
if r.get("content") and "##" not in r.get("content"):
nsec = r.get("content")
self.query_one("#fld-nsec-val", Label).update(nsec)
npub = d.get('npub', '-')
if len(npub) > 30: npub = npub[:30] + "..."
npub = d.get("npub", "-")
if len(npub) > 30:
npub = npub[:30] + "..."
self.query_one("#fld-npub-val", Label).update(npub)
self.query_one("#fld-hex-val", Label).update(d.get("hex", "-"))
qr_content = "QR Code (Press g)"
@@ -441,6 +509,7 @@ class NostrBoard(BaseBoard):
class GenericBoard(BaseBoard):
"""Fallback high-fidelity board."""
DEFAULT_CSS = """
GenericBoard { background: #000000; color: #ffffff; padding: 0 1; height: 1fr; }
.board-body { layout: horizontal; height: 1fr; }
@@ -466,27 +535,28 @@ class GenericBoard(BaseBoard):
yield Label("", id="fld-notes-val")
def update_board(self) -> None:
if not self.is_mounted: return
if not self.is_mounted:
return
d = self.entry_data
r = self.reveal_data
kind = str(d.get('kind', d.get('type', 'Unknown'))).capitalize()
kind = str(d.get("kind", d.get("type", "Unknown"))).capitalize()
try:
self.query_one("#generic-header", InspectorHeader).update_data(d, kind)
secret = "[HIDDEN] (v to reveal)"
if r.get("content"):
secret = rf"{r.get('content')}"
self.query_one("#fld-secret-val", Label).update(secret)
details = []
for k, v in d.items():
if k not in ('label', 'notes', 'tags', 'kind', 'id', 'type', 'content'):
if k not in ("label", "notes", "tags", "kind", "id", "type", "content"):
details.append(f"[b]{k.capitalize()}:[/b] {v}")
self.query_one("#fld-details-val", Label).update("\n".join(details) or "-")
self.query_one("#fld-notes-val", Label).update(d.get('notes', ''))
self.query_one("#fld-notes-val", Label).update(d.get("notes", ""))
except Exception:
pass
@@ -495,6 +565,7 @@ class BoardContainer(Vertical):
"""
Reactive container that swaps specialized boards based on entry kind.
"""
DEFAULT_CSS = """
BoardContainer {
height: 1fr;
@@ -523,7 +594,7 @@ class BoardContainer(Vertical):
return
kind = str(entry.get("kind") or entry.get("type") or "").lower()
if kind in {"password", "stored_password"}:
board = self._show_board("password")
elif kind == "totp":
@@ -540,7 +611,7 @@ class BoardContainer(Vertical):
board = self._show_board("nostr")
else:
board = self._show_board("generic")
board.entry_data = entry
def _show_board(self, board_type: str) -> Any:
+4 -2
View File
@@ -5,12 +5,13 @@ from textual.widgets import Input, Static
from textual.containers import Vertical
from textual.message import Message
class CommandPalette(Vertical):
"""
The TUI v3 Command Palette.
Docks at the top and provides a unified entry point for all actions.
"""
DEFAULT_CSS = """
CommandPalette {
dock: top;
@@ -61,10 +62,11 @@ class CommandPalette(Vertical):
if cmd:
self.app.post_message(self.CommandExecuted(cmd))
self.input.value = ""
self.toggle() # Close after execution
self.toggle() # Close after execution
class CommandExecuted(Message):
"""Sent when a command is submitted."""
def __init__(self, command: str) -> None:
self.command = command
super().__init__()
+9 -6
View File
@@ -4,10 +4,11 @@ from textual.app import ComposeResult
from textual.widgets import Tree, Static
from textual.widgets.tree import TreeNode
class ProfileTree(Tree):
"""
A hierarchical sidebar that manages Seed Profiles, Managed Accounts, and Agents.
Structure:
Root (Hidden)
├── Fingerprint (Parent Seed)
@@ -16,7 +17,7 @@ class ProfileTree(Tree):
│ └── Managed Account 2
└── External Fingerprint
"""
def on_mount(self) -> None:
self.root.expand()
self._refresh_tree()
@@ -31,14 +32,14 @@ class ProfileTree(Tree):
service = app.services["profile"]
entry_service = app.services.get("entry")
profiles = service.list_profiles()
for fp in profiles:
is_active = fp == app.active_fingerprint
icon = "■ " if is_active else "□ "
label = f"{icon}{fp[:12]}..." if len(fp) > 12 else f"{icon}{fp}"
node = self.root.add(label, data=fp, expand=is_active)
# If this is the active profile or we have the entry service,
# If this is the active profile or we have the entry service,
# we can fetch nested managed accounts.
if entry_service and is_active:
# Find managed accounts in the current profile
@@ -46,7 +47,7 @@ class ProfileTree(Tree):
managed = entry_service.search_entries("", kinds=["managed_account"])
for mid, mlabel, _, _, _, _ in managed:
node.add_leaf(f"├─ {mlabel}", data=f"managed:{mid}")
# Find agents (nostr)
agents = entry_service.search_entries("", kinds=["nostr"])
for aid, alabel, _, _, _, _ in agents:
@@ -72,8 +73,10 @@ class ProfileTree(Tree):
self.app.active_fingerprint = value
self.app.notify(f"Switched to profile: {value[:8]}")
class SidebarContainer(Static):
"""Container for the sidebar tree and toggle."""
def compose(self) -> ComposeResult:
yield ProfileTree("Profiles", id="profile-tree")