From 6e8288e651702b4591dad104326bc2ca9f872a05 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 5 Jul 2025 18:31:59 -0400 Subject: [PATCH 01/18] Run backup after configuring additional location --- README.md | 3 ++- src/main.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6eecfaa..29859ea 100644 --- a/README.md +++ b/README.md @@ -324,7 +324,8 @@ Back in the Settings menu you can: * Select `7` to export the database to an encrypted file. * Choose `8` to import a database from a backup file. * Select `9` to export all 2FA codes. -* Choose `10` to set an additional backup location. +* Choose `10` to set an additional backup location. A backup is created + immediately after the directory is configured. * Select `11` to change the inactivity timeout. * Choose `12` to lock the vault and require re-entry of your password. * Select `13` to view seed profile stats. The summary lists counts for diff --git a/src/main.py b/src/main.py index b7cc7fd..5e77705 100644 --- a/src/main.py +++ b/src/main.py @@ -514,6 +514,8 @@ def handle_set_additional_backup_location(pm: PasswordManager) -> None: try: cfg_mgr.set_additional_backup_path(str(path)) print(colored(f"Additional backups will be copied to {path}", "green")) + if pm.backup_manager is not None: + pm.backup_manager.create_backup() except Exception as e: logging.error(f"Error saving backup path: {e}") print(colored(f"Error: {e}", "red")) From 540950d06fd230ca2dab4e82ecd0278f06105b22 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 5 Jul 2025 18:45:15 -0400 Subject: [PATCH 02/18] sync from nostr on unlock --- src/password_manager/manager.py | 24 +++++++++++++++++ .../test_password_unlock_after_change.py | 1 + src/tests/test_unlock_sync.py | 26 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 src/tests/test_unlock_sync.py diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index e969f63..cd651c0 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -187,6 +187,7 @@ class PasswordManager: self.initialize_managers() self.locked = False self.update_activity() + self.sync_index_from_nostr() def initialize_fingerprint_manager(self): """ @@ -833,6 +834,29 @@ class PasswordManager: print(colored(f"Error: Failed to initialize managers: {e}", "red")) sys.exit(1) + def sync_index_from_nostr(self) -> None: + """Always fetch the latest vault data from Nostr and update the local index.""" + try: + result = asyncio.run(self.nostr_client.fetch_latest_snapshot()) + if not result: + return + manifest, chunks = result + encrypted = gzip.decompress(b"".join(chunks)) + if manifest.delta_since: + try: + version = int(manifest.delta_since) + deltas = asyncio.run(self.nostr_client.fetch_deltas_since(version)) + if deltas: + encrypted = deltas[-1] + except ValueError: + pass + current = self.vault.get_encrypted_index() + if current != encrypted: + self.vault.decrypt_and_save_index_from_nostr(encrypted) + logger.info("Local database synchronized from Nostr.") + except Exception as e: + logger.warning(f"Unable to sync index from Nostr: {e}") + def sync_index_from_nostr_if_missing(self) -> None: """Retrieve the password database from Nostr if it doesn't exist locally.""" index_file = self.fingerprint_dir / "seedpass_entries_db.json.enc" diff --git a/src/tests/test_password_unlock_after_change.py b/src/tests/test_password_unlock_after_change.py index 22b8d6c..114b7f1 100644 --- a/src/tests/test_password_unlock_after_change.py +++ b/src/tests/test_password_unlock_after_change.py @@ -81,6 +81,7 @@ def test_password_change_and_unlock(monkeypatch): ) monkeypatch.setattr(PasswordManager, "initialize_bip85", lambda self: None) monkeypatch.setattr(PasswordManager, "initialize_managers", lambda self: None) + monkeypatch.setattr(PasswordManager, "sync_index_from_nostr", lambda self: None) pm.unlock_vault() diff --git a/src/tests/test_unlock_sync.py b/src/tests/test_unlock_sync.py new file mode 100644 index 0000000..7eef4c5 --- /dev/null +++ b/src/tests/test_unlock_sync.py @@ -0,0 +1,26 @@ +import time +from types import SimpleNamespace +from pathlib import Path +import sys + +sys.path.append(str(Path(__file__).resolve().parents[1])) + +from password_manager.manager import PasswordManager + + +def test_unlock_triggers_sync(monkeypatch, tmp_path): + pm = PasswordManager.__new__(PasswordManager) + pm.fingerprint_dir = tmp_path + pm.setup_encryption_manager = lambda *a, **k: None + pm.initialize_bip85 = lambda: None + pm.initialize_managers = lambda: None + called = {"sync": False} + + def fake_sync(self): + called["sync"] = True + + monkeypatch.setattr(PasswordManager, "sync_index_from_nostr", fake_sync) + + pm.unlock_vault() + + assert called["sync"] From e7d6b7d46e40770bf8017f3011f81c7e40608209 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 5 Jul 2025 19:06:46 -0400 Subject: [PATCH 03/18] Add color scheme helper and apply to menus and stats --- src/main.py | 105 ++++++++++++++------------- src/password_manager/manager.py | 124 ++++++++++++++++++-------------- src/utils/color_scheme.py | 32 +++++++++ 3 files changed, 157 insertions(+), 104 deletions(-) create mode 100644 src/utils/color_scheme.py diff --git a/src/main.py b/src/main.py index 5e77705..68575a5 100644 --- a/src/main.py +++ b/src/main.py @@ -12,6 +12,7 @@ import gzip import tomli from colorama import init as colorama_init from termcolor import colored +from utils.color_scheme import color_text import traceback from password_manager.manager import PasswordManager @@ -248,26 +249,28 @@ def print_matches( if data else EntryType.PASSWORD.value ) - print(colored(f"Index: {idx}", "cyan")) + print(color_text(f"Index: {idx}", "index")) if etype == EntryType.TOTP.value: - print(colored(f" Label: {data.get('label', website)}", "cyan")) - print(colored(f" Derivation Index: {data.get('index', idx)}", "cyan")) + print(color_text(f" Label: {data.get('label', website)}", "index")) + print(color_text(f" Derivation Index: {data.get('index', idx)}", "index")) elif etype == EntryType.SEED.value: - print(colored(" Type: Seed Phrase", "cyan")) + print(color_text(" Type: Seed Phrase", "index")) elif etype == EntryType.SSH.value: - print(colored(" Type: SSH Key", "cyan")) + print(color_text(" Type: SSH Key", "index")) elif etype == EntryType.PGP.value: - print(colored(" Type: PGP Key", "cyan")) + print(color_text(" Type: PGP Key", "index")) elif etype == EntryType.NOSTR.value: - print(colored(" Type: Nostr Key", "cyan")) + print(color_text(" Type: Nostr Key", "index")) else: if website: - print(colored(f" Label: {website}", "cyan")) + print(color_text(f" Label: {website}", "index")) if username: - print(colored(f" Username: {username}", "cyan")) + print(color_text(f" Username: {username}", "index")) if url: - print(colored(f" URL: {url}", "cyan")) - print(colored(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "cyan")) + print(color_text(f" URL: {url}", "index")) + print( + color_text(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "index") + ) print("-" * 40) @@ -565,12 +568,12 @@ def handle_toggle_secret_mode(pm: PasswordManager) -> None: def handle_profiles_menu(password_manager: PasswordManager) -> None: """Submenu for managing seed profiles.""" while True: - print("\nProfiles:") - print("1. Switch Seed Profile") - print("2. Add a New Seed Profile") - print("3. Remove an Existing Seed Profile") - print("4. List All Seed Profiles") - print("5. Back") + print(color_text("\nProfiles:", "menu")) + print(color_text("1. Switch Seed Profile", "menu")) + print(color_text("2. Add a New Seed Profile", "menu")) + print(color_text("3. Remove an Existing Seed Profile", "menu")) + print(color_text("4. List All Seed Profiles", "menu")) + print(color_text("5. Back", "menu")) choice = input("Select an option: ").strip() password_manager.update_activity() if choice == "1": @@ -601,15 +604,15 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None: return while True: - print("\nNostr Settings:") - print("1. Backup to Nostr") - print("2. Restore from Nostr") - print("3. View current relays") - print("4. Add a relay URL") - print("5. Remove a relay by number") - print("6. Reset to default relays") - print("7. Display Nostr Public Key") - print("8. Back") + print(color_text("\nNostr Settings:", "menu")) + print(color_text("1. Backup to Nostr", "menu")) + print(color_text("2. Restore from Nostr", "menu")) + print(color_text("3. View current relays", "menu")) + print(color_text("4. Add a relay URL", "menu")) + print(color_text("5. Remove a relay by number", "menu")) + print(color_text("6. Reset to default relays", "menu")) + print(color_text("7. Display Nostr Public Key", "menu")) + print(color_text("8. Back", "menu")) choice = input("Select an option: ").strip() password_manager.update_activity() if choice == "1": @@ -635,22 +638,22 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None: def handle_settings(password_manager: PasswordManager) -> None: """Interactive settings menu with submenus for profiles and Nostr.""" while True: - print("\nSettings:") - print("1. Profiles") - print("2. Nostr") - print("3. Change password") - print("4. Verify Script Checksum") - print("5. Generate Script Checksum") - print("6. Backup Parent Seed") - print("7. Export database") - print("8. Import database") - print("9. Export 2FA codes") - print("10. Set additional backup location") - print("11. Set inactivity timeout") - print("12. Lock Vault") - print("13. Stats") - print("14. Toggle Secret Mode") - print("15. Back") + print(color_text("\nSettings:", "menu")) + print(color_text("1. Profiles", "menu")) + print(color_text("2. Nostr", "menu")) + print(color_text("3. Change password", "menu")) + print(color_text("4. Verify Script Checksum", "menu")) + print(color_text("5. Generate Script Checksum", "menu")) + print(color_text("6. Backup Parent Seed", "menu")) + print(color_text("7. Export database", "menu")) + print(color_text("8. Import database", "menu")) + print(color_text("9. Export 2FA codes", "menu")) + print(color_text("10. Set additional backup location", "menu")) + print(color_text("11. Set inactivity timeout", "menu")) + print(color_text("12. Lock Vault", "menu")) + print(color_text("13. Stats", "menu")) + print(color_text("14. Toggle Secret Mode", "menu")) + print(color_text("15. Back", "menu")) choice = input("Select an option: ").strip() if choice == "1": handle_profiles_menu(password_manager) @@ -729,7 +732,7 @@ def display_menu( # Flush logging handlers for handler in logging.getLogger().handlers: handler.flush() - print(colored(menu, "cyan")) + print(color_text(menu, "menu")) try: choice = timed_input( "Enter your choice (1-8): ", inactivity_timeout @@ -750,14 +753,14 @@ def display_menu( continue # Re-display the menu without marking as invalid if choice == "1": while True: - print("\nAdd Entry:") - print("1. Password") - print("2. 2FA (TOTP)") - print("3. SSH Key") - print("4. Seed Phrase") - print("5. Nostr Key Pair") - print("6. PGP Key") - print("7. Back") + print(color_text("\nAdd Entry:", "menu")) + print(color_text("1. Password", "menu")) + print(color_text("2. 2FA (TOTP)", "menu")) + print(color_text("3. SSH Key", "menu")) + print(color_text("4. Seed Phrase", "menu")) + print(color_text("5. Nostr Key Pair", "menu")) + print(color_text("6. PGP Key", "menu")) + print(color_text("7. Back", "menu")) sub_choice = input("Select entry type: ").strip() password_manager.update_activity() if sub_choice == "1": diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index cd651c0..eefa161 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -20,6 +20,7 @@ import shutil import time import builtins from termcolor import colored +from utils.color_scheme import color_text from utils.input_utils import timed_input from password_manager.encryption import EncryptionManager @@ -1832,76 +1833,90 @@ class PasswordManager: return etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value)) - print(colored(f"Index: {index}", "cyan")) + print(color_text(f"Index: {index}", "index")) if etype == EntryType.TOTP.value: - print(colored(f" Label: {entry.get('label', '')}", "cyan")) - print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan")) + print(color_text(f" Label: {entry.get('label', '')}", "index")) print( - colored( + color_text(f" Derivation Index: {entry.get('index', index)}", "index") + ) + print( + color_text( f" Period: {entry.get('period', 30)}s Digits: {entry.get('digits', 6)}", - "cyan", + "index", ) ) notes = entry.get("notes", "") if notes: - print(colored(f" Notes: {notes}", "cyan")) + print(color_text(f" Notes: {notes}", "index")) elif etype == EntryType.SEED.value: - print(colored(" Type: Seed Phrase", "cyan")) - print(colored(f" Label: {entry.get('label', '')}", "cyan")) - print(colored(f" Words: {entry.get('words', 24)}", "cyan")) - print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan")) + print(color_text(" Type: Seed Phrase", "index")) + print(color_text(f" Label: {entry.get('label', '')}", "index")) + print(color_text(f" Words: {entry.get('words', 24)}", "index")) + print( + color_text(f" Derivation Index: {entry.get('index', index)}", "index") + ) notes = entry.get("notes", "") if notes: - print(colored(f" Notes: {notes}", "cyan")) + print(color_text(f" Notes: {notes}", "index")) elif etype == EntryType.SSH.value: - print(colored(" Type: SSH Key", "cyan")) - print(colored(f" Label: {entry.get('label', '')}", "cyan")) - print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan")) + print(color_text(" Type: SSH Key", "index")) + print(color_text(f" Label: {entry.get('label', '')}", "index")) + print( + color_text(f" Derivation Index: {entry.get('index', index)}", "index") + ) notes = entry.get("notes", "") if notes: - print(colored(f" Notes: {notes}", "cyan")) + print(color_text(f" Notes: {notes}", "index")) elif etype == EntryType.PGP.value: - print(colored(" Type: PGP Key", "cyan")) - print(colored(f" Label: {entry.get('label', '')}", "cyan")) - print(colored(f" Key Type: {entry.get('key_type', 'ed25519')}", "cyan")) + print(color_text(" Type: PGP Key", "index")) + print(color_text(f" Label: {entry.get('label', '')}", "index")) + print( + color_text(f" Key Type: {entry.get('key_type', 'ed25519')}", "index") + ) uid = entry.get("user_id", "") if uid: - print(colored(f" User ID: {uid}", "cyan")) - print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan")) + print(color_text(f" User ID: {uid}", "index")) + print( + color_text(f" Derivation Index: {entry.get('index', index)}", "index") + ) notes = entry.get("notes", "") if notes: - print(colored(f" Notes: {notes}", "cyan")) + print(color_text(f" Notes: {notes}", "index")) elif etype == EntryType.NOSTR.value: - print(colored(" Type: Nostr Key", "cyan")) - print(colored(f" Label: {entry.get('label', '')}", "cyan")) - print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan")) + print(color_text(" Type: Nostr Key", "index")) + print(color_text(f" Label: {entry.get('label', '')}", "index")) + print( + color_text(f" Derivation Index: {entry.get('index', index)}", "index") + ) notes = entry.get("notes", "") if notes: - print(colored(f" Notes: {notes}", "cyan")) + print(color_text(f" Notes: {notes}", "index")) else: website = entry.get("label", entry.get("website", "")) username = entry.get("username", "") url = entry.get("url", "") blacklisted = entry.get("blacklisted", False) - print(colored(f" Label: {website}", "cyan")) - print(colored(f" Username: {username or 'N/A'}", "cyan")) - print(colored(f" URL: {url or 'N/A'}", "cyan")) - print(colored(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "cyan")) + print(color_text(f" Label: {website}", "index")) + print(color_text(f" Username: {username or 'N/A'}", "index")) + print(color_text(f" URL: {url or 'N/A'}", "index")) + print( + color_text(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "index") + ) print("-" * 40) def handle_list_entries(self) -> None: """List entries and optionally show details.""" try: while True: - print("\nList Entries:") - print("1. All") - print("2. Passwords") - print("3. 2FA (TOTP)") - print("4. SSH Key") - print("5. Seed Phrase") - print("6. Nostr Key Pair") - print("7. PGP") - print("8. Back") + print(color_text("\nList Entries:", "menu")) + print(color_text("1. All", "menu")) + print(color_text("2. Passwords", "menu")) + print(color_text("3. 2FA (TOTP)", "menu")) + print(color_text("4. SSH Key", "menu")) + print(color_text("5. Seed Phrase", "menu")) + print(color_text("6. Nostr Key Pair", "menu")) + print(color_text("7. PGP", "menu")) + print(color_text("8. Back", "menu")) choice = input("Select entry type: ").strip() if choice == "1": filter_kind = None @@ -2602,34 +2617,37 @@ class PasswordManager: print(colored("No statistics available.", "red")) return - print(colored("\n=== Seed Profile Stats ===", "yellow")) - print(colored(f"Total entries: {stats['total_entries']}", "cyan")) + print(color_text("\n=== Seed Profile Stats ===", "stats")) + print(color_text(f"Total entries: {stats['total_entries']}", "stats")) for etype, count in stats["entries"].items(): - print(colored(f" {etype}: {count}", "cyan")) - print(colored(f"Relays configured: {stats['relay_count']}", "cyan")) + print(color_text(f" {etype}: {count}", "stats")) + print(color_text(f"Relays configured: {stats['relay_count']}", "stats")) print( - colored( - f"Backups: {stats['backup_count']} (dir: {stats['backup_dir']})", "cyan" + color_text( + f"Backups: {stats['backup_count']} (dir: {stats['backup_dir']})", + "stats", ) ) if stats.get("additional_backup_path"): print( - colored(f"Additional backup: {stats['additional_backup_path']}", "cyan") + color_text( + f"Additional backup: {stats['additional_backup_path']}", "stats" + ) ) - print(colored(f"Schema version: {stats['schema_version']}", "cyan")) + print(color_text(f"Schema version: {stats['schema_version']}", "stats")) print( - colored( + color_text( f"Database checksum ok: {'yes' if stats['checksum_ok'] else 'no'}", - "cyan", + "stats", ) ) print( - colored( + color_text( f"Script checksum ok: {'yes' if stats['script_checksum_ok'] else 'no'}", - "cyan", + "stats", ) ) - print(colored(f"Snapshot chunks: {stats['chunk_count']}", "cyan")) - print(colored(f"Pending deltas: {stats['pending_deltas']}", "cyan")) + print(color_text(f"Snapshot chunks: {stats['chunk_count']}", "stats")) + print(color_text(f"Pending deltas: {stats['pending_deltas']}", "stats")) if stats.get("delta_since"): - print(colored(f"Latest delta id: {stats['delta_since']}", "cyan")) + print(color_text(f"Latest delta id: {stats['delta_since']}", "stats")) diff --git a/src/utils/color_scheme.py b/src/utils/color_scheme.py new file mode 100644 index 0000000..d468074 --- /dev/null +++ b/src/utils/color_scheme.py @@ -0,0 +1,32 @@ +"""Utility functions for SeedPass CLI color scheme.""" + +from termcolor import colored + + +# ANSI escape for 256-color orange (color code 208) +_ORANGE = "\033[38;5;208m" +_RESET = "\033[0m" + + +def _apply_orange(text: str) -> str: + """Return text wrapped in ANSI codes for orange.""" + return f"{_ORANGE}{text}{_RESET}" + + +# Mapping of semantic color categories to actual colors +_COLOR_MAP = { + "deterministic": "red", + "imported": "orange", + "index": "yellow", + "menu": "blue", + "stats": "green", + "default": "white", +} + + +def color_text(text: str, category: str = "default") -> str: + """Colorize ``text`` according to the given category.""" + color = _COLOR_MAP.get(category, "white") + if color == "orange": + return _apply_orange(text) + return colored(text, color) From 1ae4d6ae59b9182ea60e519805a94bb6c0211bb2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 5 Jul 2025 19:18:58 -0400 Subject: [PATCH 04/18] Display index and label when handling seed entries --- src/password_manager/manager.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index eefa161..4dad8b3 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -1128,7 +1128,14 @@ class PasswordManager: print(colored("Seed phrase display cancelled.", "yellow")) return - print(colored(f"\n[+] Seed entry added with ID {index}.\n", "green")) + print( + colored( + f"\n[+] Seed entry '{label}' added with ID {index}.\n", + "green", + ) + ) + print(colored(f"Index: {index}", "cyan")) + print(colored(f"Label: {label}", "cyan")) if notes: print(colored(f"Notes: {notes}", "cyan")) print(colored("Seed Phrase:", "cyan")) @@ -1374,6 +1381,7 @@ class PasswordManager: try: phrase = self.entry_manager.get_seed_phrase(index, self.parent_seed) print(colored("\n[+] Retrieved Seed Phrase:\n", "green")) + print(colored(f"Index: {index}", "cyan")) if label: print(colored(f"Label: {label}", "cyan")) if notes: @@ -1407,8 +1415,6 @@ class PasswordManager: words_len=words, ) print(colored(f"Entropy: {entropy.hex()}", "cyan")) - if notes: - print(colored(f"Notes: {notes}", "cyan")) except Exception as e: logging.error(f"Error deriving seed phrase: {e}", exc_info=True) print(colored(f"Error: Failed to derive seed phrase: {e}", "red")) From eca97c0f0e65197bee44ca840189f63989a529f0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 5 Jul 2025 19:41:49 -0400 Subject: [PATCH 05/18] color deterministic secrets --- src/password_manager/manager.py | 40 +++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 4dad8b3..d9668e8 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -1007,7 +1007,7 @@ class PasswordManager: print(colored("Add this URI to your authenticator app:", "cyan")) print(colored(uri, "yellow")) TotpManager.print_qr_code(uri) - print(colored(f"Secret: {secret}\n", "cyan")) + print(color_text(f"Secret: {secret}\n", "deterministic")) try: self.sync_vault() except Exception as nostr_error: @@ -1088,9 +1088,9 @@ class PasswordManager: if notes: print(colored(f"Notes: {notes}", "cyan")) print(colored("Public Key:", "cyan")) - print(pub_pem) + print(color_text(pub_pem, "default")) print(colored("Private Key:", "cyan")) - print(priv_pem) + print(color_text(priv_pem, "deterministic")) try: self.sync_vault() except Exception as nostr_error: @@ -1139,7 +1139,7 @@ class PasswordManager: if notes: print(colored(f"Notes: {notes}", "cyan")) print(colored("Seed Phrase:", "cyan")) - print(colored(phrase, "yellow")) + print(color_text(phrase, "deterministic")) if confirm_action("Show Compact Seed QR? (Y/N): "): from password_manager.seedqr import encode_seedqr @@ -1193,7 +1193,7 @@ class PasswordManager: if notes: print(colored(f"Notes: {notes}", "cyan")) print(colored(f"Fingerprint: {fingerprint}", "cyan")) - print(priv_key) + print(color_text(priv_key, "deterministic")) try: self.sync_vault() except Exception as nostr_error: # pragma: no cover - best effort @@ -1228,7 +1228,7 @@ class PasswordManager: ) ) else: - print(colored(f"nsec: {nsec}", "cyan")) + print(color_text(f"nsec: {nsec}", "deterministic")) if confirm_action("Show QR code for npub? (Y/N): "): TotpManager.print_qr_code(f"nostr:{npub}") if confirm_action( @@ -1306,7 +1306,9 @@ class PasswordManager: else: print(colored("\n[+] Retrieved 2FA Code:\n", "green")) print(colored(f"Label: {label}", "cyan")) - print(colored(f"Code: {code}", "yellow")) + imported = "secret" in entry + category = "imported" if imported else "deterministic" + print(color_text(f"Code: {code}", category)) if notes: print(colored(f"Notes: {notes}", "cyan")) remaining = self.entry_manager.get_totp_time_remaining(index) @@ -1354,7 +1356,7 @@ class PasswordManager: if notes: print(colored(f"Notes: {notes}", "cyan")) print(colored("Public Key:", "cyan")) - print(pub_pem) + print(color_text(pub_pem, "default")) if self.secret_mode_enabled: copy_to_clipboard(priv_pem, self.clipboard_clear_delay) print( @@ -1365,7 +1367,7 @@ class PasswordManager: ) else: print(colored("Private Key:", "cyan")) - print(priv_pem) + print(color_text(priv_pem, "deterministic")) except Exception as e: logging.error(f"Error deriving SSH key pair: {e}", exc_info=True) print(colored(f"Error: Failed to derive SSH keys: {e}", "red")) @@ -1395,7 +1397,7 @@ class PasswordManager: ) ) else: - print(colored(phrase, "yellow")) + print(color_text(phrase, "deterministic")) if confirm_action("Show Compact Seed QR? (Y/N): "): from password_manager.seedqr import encode_seedqr @@ -1414,7 +1416,7 @@ class PasswordManager: app_no=39, words_len=words, ) - print(colored(f"Entropy: {entropy.hex()}", "cyan")) + print(color_text(f"Entropy: {entropy.hex()}", "deterministic")) except Exception as e: logging.error(f"Error deriving seed phrase: {e}", exc_info=True) print(colored(f"Error: Failed to derive seed phrase: {e}", "red")) @@ -1446,7 +1448,7 @@ class PasswordManager: ) ) else: - print(priv_key) + print(color_text(priv_key, "deterministic")) except Exception as e: logging.error(f"Error deriving PGP key: {e}", exc_info=True) print(colored(f"Error: Failed to derive PGP key: {e}", "red")) @@ -1470,7 +1472,7 @@ class PasswordManager: ) ) else: - print(colored(f"nsec: {nsec}", "cyan")) + print(color_text(f"nsec: {nsec}", "deterministic")) if confirm_action("Show QR code for npub? (Y/N): "): TotpManager.print_qr_code(f"nostr:{npub}") if confirm_action( @@ -1527,7 +1529,7 @@ class PasswordManager: "green", ) ) - print(colored(f"Password: {password}", "yellow")) + print(color_text(f"Password: {password}", "deterministic")) print(colored(f"Associated Username: {username or 'N/A'}", "cyan")) print(colored(f"Associated URL: {url or 'N/A'}", "cyan")) print( @@ -2044,7 +2046,9 @@ class PasswordManager: f"[{idx}] {label}: [HIDDEN] {bar} {remaining:2d}s - copied to clipboard" ) else: - print(f"[{idx}] {label}: {code} {bar} {remaining:2d}s") + print( + f"[{idx}] {label}: {color_text(code, 'deterministic')} {bar} {remaining:2d}s" + ) if imported_list: print(colored("\nImported 2FA Codes:", "green")) for label, idx, period, _ in imported_list: @@ -2058,7 +2062,9 @@ class PasswordManager: f"[{idx}] {label}: [HIDDEN] {bar} {remaining:2d}s - copied to clipboard" ) else: - print(f"[{idx}] {label}: {code} {bar} {remaining:2d}s") + print( + f"[{idx}] {label}: {color_text(code, 'imported')} {bar} {remaining:2d}s" + ) sys.stdout.flush() try: user_input = timed_input("", 1) @@ -2341,7 +2347,7 @@ class PasswordManager: # Reveal the parent seed print(colored("\n=== Your BIP-85 Parent Seed ===", "green")) - print(colored(self.parent_seed, "yellow")) + print(color_text(self.parent_seed, "imported")) print( colored( "\nPlease write this down and store it securely. Do not share it with anyone.", From 74bbbbe16abe64d28da2b6426a2c7ff1864125b3 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 5 Jul 2025 19:57:47 -0400 Subject: [PATCH 06/18] Make warnings yellow --- src/password_manager/manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index d9668e8..9d8d8c0 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -142,7 +142,7 @@ class PasswordManager: colored( "Warning: script checksum mismatch. " "Run 'Generate Script Checksum' in Settings if you've updated the app.", - "red", + "yellow", ) ) @@ -1507,7 +1507,7 @@ class PasswordManager: print( colored( f"Warning: This password is blacklisted and should not be used.", - "red", + "yellow", ) ) @@ -2320,13 +2320,13 @@ class PasswordManager: print( colored( "Warning: Revealing your parent seed is a highly sensitive operation.", - "red", + "yellow", ) ) print( colored( "Ensure you're in a secure, private environment and no one is watching your screen.", - "red", + "yellow", ) ) From 5b2757830ed05ce48245850e549db2aea6c5caf9 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 5 Jul 2025 20:21:49 -0400 Subject: [PATCH 07/18] Add clear_screen helper and integrate into menus --- src/main.py | 7 ++++++- src/password_manager/manager.py | 3 ++- src/utils/__init__.py | 2 ++ src/utils/terminal_utils.py | 6 ++++++ 4 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 src/utils/terminal_utils.py diff --git a/src/main.py b/src/main.py index 68575a5..caeff93 100644 --- a/src/main.py +++ b/src/main.py @@ -20,7 +20,7 @@ from nostr.client import NostrClient from password_manager.entry_types import EntryType from constants import INACTIVITY_TIMEOUT, initialize_app from utils.password_prompt import PasswordPromptError -from utils import timed_input, copy_to_clipboard +from utils import timed_input, copy_to_clipboard, clear_screen from local_bip85.bip85 import Bip85Error @@ -568,6 +568,7 @@ def handle_toggle_secret_mode(pm: PasswordManager) -> None: def handle_profiles_menu(password_manager: PasswordManager) -> None: """Submenu for managing seed profiles.""" while True: + clear_screen() print(color_text("\nProfiles:", "menu")) print(color_text("1. Switch Seed Profile", "menu")) print(color_text("2. Add a New Seed Profile", "menu")) @@ -604,6 +605,7 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None: return while True: + clear_screen() print(color_text("\nNostr Settings:", "menu")) print(color_text("1. Backup to Nostr", "menu")) print(color_text("2. Restore from Nostr", "menu")) @@ -638,6 +640,7 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None: def handle_settings(password_manager: PasswordManager) -> None: """Interactive settings menu with submenus for profiles and Nostr.""" while True: + clear_screen() print(color_text("\nSettings:", "menu")) print(color_text("1. Profiles", "menu")) print(color_text("2. Nostr", "menu")) @@ -716,6 +719,7 @@ def display_menu( if callable(display_fn): display_fn() while True: + clear_screen() if time.time() - password_manager.last_activity > inactivity_timeout: print(colored("Session timed out. Vault locked.", "yellow")) password_manager.lock_vault() @@ -788,6 +792,7 @@ def display_menu( elif choice == "2": password_manager.update_activity() password_manager.handle_retrieve_entry() + clear_screen() elif choice == "3": password_manager.update_activity() password_manager.handle_search_entries() diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 9d8d8c0..459c27c 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -52,6 +52,7 @@ from utils.password_prompt import ( ) from utils.memory_protection import InMemorySecret from utils.clipboard import copy_to_clipboard +from utils.terminal_utils import clear_screen from constants import MIN_HEALTHY_RELAYS from constants import ( @@ -2029,7 +2030,7 @@ class PasswordManager: totp_list.sort(key=lambda t: t[0].lower()) print(colored("Press 'b' then Enter to return to the menu.", "cyan")) while True: - print("\033c", end="") + clear_screen() print(colored("Press 'b' then Enter to return to the menu.", "cyan")) generated = [t for t in totp_list if not t[3]] imported_list = [t for t in totp_list if t[3]] diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 286d641..11a382d 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -28,6 +28,7 @@ try: from .input_utils import timed_input from .memory_protection import InMemorySecret from .clipboard import copy_to_clipboard + from .terminal_utils import clear_screen if logger.isEnabledFor(logging.DEBUG): logger.info("Modules imported successfully.") @@ -55,4 +56,5 @@ __all__ = [ "timed_input", "InMemorySecret", "copy_to_clipboard", + "clear_screen", ] diff --git a/src/utils/terminal_utils.py b/src/utils/terminal_utils.py new file mode 100644 index 0000000..5df3f79 --- /dev/null +++ b/src/utils/terminal_utils.py @@ -0,0 +1,6 @@ +"""Utility functions for terminal output.""" + + +def clear_screen() -> None: + """Clear the terminal screen using an ANSI escape code.""" + print("\033c", end="") From 2704f97c2f3bbdfd064f069f494bff53d32762e8 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 5 Jul 2025 20:35:03 -0400 Subject: [PATCH 08/18] Add screen clearing to interactive handlers --- src/password_manager/manager.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 459c27c..9425db7 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -886,6 +886,7 @@ class PasswordManager: def handle_add_password(self) -> None: try: + clear_screen() website_name = input("Enter the label or website name: ").strip() if not website_name: print(colored("Error: Label cannot be empty.", "red")) @@ -970,6 +971,7 @@ class PasswordManager: def handle_add_totp(self) -> None: """Add a TOTP entry either derived from the seed or imported.""" try: + clear_screen() while True: print("\nAdd TOTP:") print("1. Make 2FA (derive from seed)") @@ -1067,6 +1069,7 @@ class PasswordManager: def handle_add_ssh_key(self) -> None: """Add an SSH key pair entry and display the derived keys.""" try: + clear_screen() label = input("Label: ").strip() if not label: print(colored("Error: Label cannot be empty.", "red")) @@ -1106,6 +1109,7 @@ class PasswordManager: def handle_add_seed(self) -> None: """Add a derived BIP-39 seed phrase entry.""" try: + clear_screen() label = input("Label: ").strip() if not label: print(colored("Error: Label cannot be empty.", "red")) @@ -1159,6 +1163,7 @@ class PasswordManager: def handle_add_pgp(self) -> None: """Add a PGP key entry and display the generated key.""" try: + clear_screen() label = input("Label: ").strip() if not label: print(colored("Error: Label cannot be empty.", "red")) @@ -1209,6 +1214,7 @@ class PasswordManager: def handle_add_nostr_key(self) -> None: """Add a Nostr key entry and display the derived keys.""" try: + clear_screen() label = input("Label: ").strip() if not label: print(colored("Error: Label cannot be empty.", "red")) @@ -1273,6 +1279,7 @@ class PasswordManager: and displaying the corresponding password and associated details. """ try: + clear_screen() index_input = input( "Enter the index number of the entry to retrieve: " ).strip() @@ -1581,6 +1588,7 @@ class PasswordManager: and new details to update. """ try: + clear_screen() index_input = input( "Enter the index number of the entry to modify: " ).strip() @@ -1818,6 +1826,7 @@ class PasswordManager: def handle_search_entries(self) -> None: """Prompt for a query and display matching entries.""" try: + clear_screen() query = input("Enter search string: ").strip() if not query: print(colored("No search string provided.", "yellow")) @@ -1917,6 +1926,7 @@ class PasswordManager: """List entries and optionally show details.""" try: while True: + clear_screen() print(color_text("\nList Entries:", "menu")) print(color_text("1. All", "menu")) print(color_text("2. Passwords", "menu")) @@ -1951,6 +1961,7 @@ class PasswordManager: if not summaries: continue while True: + clear_screen() print(colored("\n[+] Entries:\n", "green")) for idx, etype, label in summaries: if filter_kind is None: @@ -2085,6 +2096,7 @@ class PasswordManager: Handles verifying the script's checksum against the stored checksum to ensure integrity. """ try: + clear_screen() current_checksum = calculate_checksum(__file__) try: verified = verify_checksum(current_checksum, SCRIPT_CHECKSUM_FILE) @@ -2119,6 +2131,7 @@ class PasswordManager: print(colored("Operation cancelled.", "yellow")) return try: + clear_screen() script_path = Path(__file__).resolve() if update_checksum_file(str(script_path), str(SCRIPT_CHECKSUM_FILE)): print( @@ -2228,6 +2241,7 @@ class PasswordManager: ) -> Path | None: """Export the current database to an encrypted portable file.""" try: + clear_screen() path = export_backup( self.vault, self.backup_manager, @@ -2244,6 +2258,7 @@ class PasswordManager: def handle_import_database(self, src: Path) -> None: """Import a portable database file, replacing the current index.""" try: + clear_screen() import_backup( self.vault, self.backup_manager, @@ -2258,6 +2273,7 @@ class PasswordManager: def handle_export_totp_codes(self) -> Path | None: """Export all 2FA codes to a JSON file for other authenticator apps.""" try: + clear_screen() data = self.entry_manager.vault.load_index() entries = data.get("entries", {}) @@ -2317,6 +2333,7 @@ class PasswordManager: Handles the backup and reveal of the parent seed. """ try: + clear_screen() print(colored("\n=== Backup Parent Seed ===", "yellow")) print( colored( From 7b9b9a082e31034f5185b340dcf6949d30fc3a10 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 5 Jul 2025 21:53:17 -0400 Subject: [PATCH 09/18] Pause before clearing screen --- src/password_manager/manager.py | 11 ++++++++++- src/utils/__init__.py | 3 ++- src/utils/terminal_utils.py | 12 ++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 9425db7..9824fbe 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -52,7 +52,7 @@ from utils.password_prompt import ( ) from utils.memory_protection import InMemorySecret from utils.clipboard import copy_to_clipboard -from utils.terminal_utils import clear_screen +from utils.terminal_utils import clear_screen, pause from constants import MIN_HEALTHY_RELAYS from constants import ( @@ -1285,11 +1285,13 @@ class PasswordManager: ).strip() if not index_input.isdigit(): print(colored("Error: Index must be a number.", "red")) + pause() return index = int(index_input) entry = self.entry_manager.retrieve_entry(index) if not entry: + pause() return entry_type = entry.get("type", EntryType.PASSWORD.value) @@ -1345,6 +1347,7 @@ class PasswordManager: except Exception as e: logging.error(f"Error generating TOTP code: {e}", exc_info=True) print(colored(f"Error: Failed to generate TOTP code: {e}", "red")) + pause() return if entry_type == EntryType.SSH.value: notes = entry.get("notes", "") @@ -1379,6 +1382,7 @@ class PasswordManager: except Exception as e: logging.error(f"Error deriving SSH key pair: {e}", exc_info=True) print(colored(f"Error: Failed to derive SSH keys: {e}", "red")) + pause() return if entry_type == EntryType.SEED.value: notes = entry.get("notes", "") @@ -1428,6 +1432,7 @@ class PasswordManager: except Exception as e: logging.error(f"Error deriving seed phrase: {e}", exc_info=True) print(colored(f"Error: Failed to derive seed phrase: {e}", "red")) + pause() return if entry_type == EntryType.PGP.value: notes = entry.get("notes", "") @@ -1460,6 +1465,7 @@ class PasswordManager: except Exception as e: logging.error(f"Error deriving PGP key: {e}", exc_info=True) print(colored(f"Error: Failed to derive PGP key: {e}", "red")) + pause() return if entry_type == EntryType.NOSTR.value: label = entry.get("label", "") @@ -1492,6 +1498,7 @@ class PasswordManager: except Exception as e: logging.error(f"Error deriving Nostr keys: {e}", exc_info=True) print(colored(f"Error: Failed to derive Nostr keys: {e}", "red")) + pause() return website_name = entry.get("website") @@ -1578,9 +1585,11 @@ class PasswordManager: print(colored(f" {label}: {value}", "cyan")) else: print(colored("Error: Failed to retrieve the password.", "red")) + pause() except Exception as e: logging.error(f"Error during password retrieval: {e}", exc_info=True) print(colored(f"Error: Failed to retrieve password: {e}", "red")) + pause() def handle_modify_entry(self) -> None: """ diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 11a382d..8df494f 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -28,7 +28,7 @@ try: from .input_utils import timed_input from .memory_protection import InMemorySecret from .clipboard import copy_to_clipboard - from .terminal_utils import clear_screen + from .terminal_utils import clear_screen, pause if logger.isEnabledFor(logging.DEBUG): logger.info("Modules imported successfully.") @@ -57,4 +57,5 @@ __all__ = [ "InMemorySecret", "copy_to_clipboard", "clear_screen", + "pause", ] diff --git a/src/utils/terminal_utils.py b/src/utils/terminal_utils.py index 5df3f79..563e0fe 100644 --- a/src/utils/terminal_utils.py +++ b/src/utils/terminal_utils.py @@ -1,6 +1,18 @@ """Utility functions for terminal output.""" +import sys + def clear_screen() -> None: """Clear the terminal screen using an ANSI escape code.""" print("\033c", end="") + + +def pause(message: str = "Press Enter to continue...") -> None: + """Wait for the user to press Enter before proceeding.""" + if not sys.stdin or not sys.stdin.isatty(): + return + try: + input(message) + except EOFError: + pass From 82a27ce85811b07a8137e2c187fbec54c23a921b Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 6 Jul 2025 06:57:57 -0400 Subject: [PATCH 10/18] Add pauses to prevent screen clearing --- src/main.py | 6 +++++- src/password_manager/manager.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index caeff93..37e3dc3 100644 --- a/src/main.py +++ b/src/main.py @@ -20,7 +20,7 @@ from nostr.client import NostrClient from password_manager.entry_types import EntryType from constants import INACTIVITY_TIMEOUT, initialize_app from utils.password_prompt import PasswordPromptError -from utils import timed_input, copy_to_clipboard, clear_screen +from utils import timed_input, copy_to_clipboard, clear_screen, pause from local_bip85.bip85 import Bip85Error @@ -202,6 +202,7 @@ def handle_list_fingerprints(password_manager: PasswordManager): print(colored("Available Seed Profiles:", "cyan")) for fp in fingerprints: print(colored(f"- {fp}", "cyan")) + pause() except Exception as e: logging.error(f"Error listing seed profiles: {e}", exc_info=True) print(colored(f"Error: Failed to list seed profiles: {e}", "red")) @@ -219,6 +220,7 @@ def handle_display_npub(password_manager: PasswordManager): else: print(colored("Nostr public key not available.", "red")) logging.error("Nostr public key not available.") + pause() except Exception as e: logging.error(f"Failed to display npub: {e}", exc_info=True) print(colored(f"Error: Failed to display npub: {e}", "red")) @@ -341,6 +343,7 @@ def handle_view_relays(cfg_mgr: "ConfigManager") -> None: print(colored("\nCurrent Relays:", "cyan")) for idx, relay in enumerate(relays, start=1): print(colored(f"{idx}. {relay}", "cyan")) + pause() except Exception as e: logging.error(f"Error displaying relays: {e}") print(colored(f"Error: {e}", "red")) @@ -718,6 +721,7 @@ def display_menu( display_fn = getattr(password_manager, "display_stats", None) if callable(display_fn): display_fn() + pause() while True: clear_screen() if time.time() - password_manager.last_activity > inactivity_timeout: diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 9824fbe..f9ffb91 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -1849,6 +1849,7 @@ class PasswordManager: print(colored("\n[+] Search Results:\n", "green")) for match in results: self.display_entry_details(match[0]) + pause() except Exception as e: logging.error(f"Failed to search entries: {e}", exc_info=True) print(colored(f"Error: Failed to search entries: {e}", "red")) From 156dc2ad3b15cf78e6ccf475c5ae262754cdc062 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 6 Jul 2025 07:16:32 -0400 Subject: [PATCH 11/18] Add pause prompt after entry creation --- src/password_manager/manager.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index f9ffb91..ff84778 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -963,10 +963,12 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() except Exception as e: logging.error(f"Error during password generation: {e}", exc_info=True) print(colored(f"Error: Failed to generate password: {e}", "red")) + pause() def handle_add_totp(self) -> None: """Add a TOTP entry either derived from the seed or imported.""" @@ -1018,6 +1020,7 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() break elif choice == "2": raw = input("Paste otpauth URI or secret: ").strip() @@ -1055,6 +1058,7 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() break except ValueError as err: print(colored(f"Error: {err}", "red")) @@ -1065,6 +1069,7 @@ class PasswordManager: except Exception as e: logging.error(f"Error during TOTP setup: {e}", exc_info=True) print(colored(f"Error: Failed to add TOTP: {e}", "red")) + pause() def handle_add_ssh_key(self) -> None: """Add an SSH key pair entry and display the derived keys.""" @@ -1102,9 +1107,11 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() except Exception as e: logging.error(f"Error during SSH key setup: {e}", exc_info=True) print(colored(f"Error: Failed to add SSH key: {e}", "red")) + pause() def handle_add_seed(self) -> None: """Add a derived BIP-39 seed phrase entry.""" @@ -1156,9 +1163,11 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() except Exception as e: logging.error(f"Error during seed phrase setup: {e}", exc_info=True) print(colored(f"Error: Failed to add seed phrase: {e}", "red")) + pause() def handle_add_pgp(self) -> None: """Add a PGP key entry and display the generated key.""" @@ -1207,9 +1216,11 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() except Exception as e: logging.error(f"Error during PGP key setup: {e}", exc_info=True) print(colored(f"Error: Failed to add PGP key: {e}", "red")) + pause() def handle_add_nostr_key(self) -> None: """Add a Nostr key entry and display the derived keys.""" @@ -1249,9 +1260,11 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() except Exception as e: logging.error(f"Error during Nostr key setup: {e}", exc_info=True) print(colored(f"Error: Failed to add Nostr key: {e}", "red")) + pause() def show_entry_details_by_index(self, index: int) -> None: """Display entry details using :meth:`handle_retrieve_entry` for the From c3566fcb0cf2a312cd3e03f5f8dced51bf1152de Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 6 Jul 2025 07:29:44 -0400 Subject: [PATCH 12/18] Add pauses in search and settings menus --- src/main.py | 14 ++++++++++++++ src/password_manager/manager.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/main.py b/src/main.py index 37e3dc3..e2295b9 100644 --- a/src/main.py +++ b/src/main.py @@ -663,36 +663,50 @@ def handle_settings(password_manager: PasswordManager) -> None: choice = input("Select an option: ").strip() if choice == "1": handle_profiles_menu(password_manager) + pause() elif choice == "2": handle_nostr_menu(password_manager) + pause() elif choice == "3": password_manager.change_password() + pause() elif choice == "4": password_manager.handle_verify_checksum() + pause() elif choice == "5": password_manager.handle_update_script_checksum() + pause() elif choice == "6": password_manager.handle_backup_reveal_parent_seed() + pause() elif choice == "7": password_manager.handle_export_database() + pause() elif choice == "8": path = input("Enter path to backup file: ").strip() if path: password_manager.handle_import_database(Path(path)) + pause() elif choice == "9": password_manager.handle_export_totp_codes() + pause() elif choice == "10": handle_set_additional_backup_location(password_manager) + pause() elif choice == "11": handle_set_inactivity_timeout(password_manager) + pause() elif choice == "12": password_manager.lock_vault() print(colored("Vault locked. Please re-enter your password.", "yellow")) password_manager.unlock_vault() + pause() elif choice == "13": handle_display_stats(password_manager) + pause() elif choice == "14": handle_toggle_secret_mode(password_manager) + pause() elif choice == "15": break else: diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index f9ffb91..a19a066 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -963,10 +963,12 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() except Exception as e: logging.error(f"Error during password generation: {e}", exc_info=True) print(colored(f"Error: Failed to generate password: {e}", "red")) + pause() def handle_add_totp(self) -> None: """Add a TOTP entry either derived from the seed or imported.""" @@ -1018,6 +1020,7 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() break elif choice == "2": raw = input("Paste otpauth URI or secret: ").strip() @@ -1055,6 +1058,7 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() break except ValueError as err: print(colored(f"Error: {err}", "red")) @@ -1065,6 +1069,7 @@ class PasswordManager: except Exception as e: logging.error(f"Error during TOTP setup: {e}", exc_info=True) print(colored(f"Error: Failed to add TOTP: {e}", "red")) + pause() def handle_add_ssh_key(self) -> None: """Add an SSH key pair entry and display the derived keys.""" @@ -1102,9 +1107,11 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() except Exception as e: logging.error(f"Error during SSH key setup: {e}", exc_info=True) print(colored(f"Error: Failed to add SSH key: {e}", "red")) + pause() def handle_add_seed(self) -> None: """Add a derived BIP-39 seed phrase entry.""" @@ -1156,9 +1163,11 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() except Exception as e: logging.error(f"Error during seed phrase setup: {e}", exc_info=True) print(colored(f"Error: Failed to add seed phrase: {e}", "red")) + pause() def handle_add_pgp(self) -> None: """Add a PGP key entry and display the generated key.""" @@ -1207,9 +1216,11 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() except Exception as e: logging.error(f"Error during PGP key setup: {e}", exc_info=True) print(colored(f"Error: Failed to add PGP key: {e}", "red")) + pause() def handle_add_nostr_key(self) -> None: """Add a Nostr key entry and display the derived keys.""" @@ -1249,9 +1260,11 @@ class PasswordManager: f"Failed to post updated index to Nostr: {nostr_error}", exc_info=True, ) + pause() except Exception as e: logging.error(f"Error during Nostr key setup: {e}", exc_info=True) print(colored(f"Error: Failed to add Nostr key: {e}", "red")) + pause() def show_entry_details_by_index(self, index: int) -> None: """Display entry details using :meth:`handle_retrieve_entry` for the @@ -1839,11 +1852,13 @@ class PasswordManager: query = input("Enter search string: ").strip() if not query: print(colored("No search string provided.", "yellow")) + pause() return results = self.entry_manager.search_entries(query) if not results: print(colored("No matching entries found.", "yellow")) + pause() return print(colored("\n[+] Search Results:\n", "green")) @@ -1853,6 +1868,7 @@ class PasswordManager: except Exception as e: logging.error(f"Failed to search entries: {e}", exc_info=True) print(colored(f"Error: Failed to search entries: {e}", "red")) + pause() def display_entry_details(self, index: int) -> None: """Print detailed information for a single entry.""" From 5cefc294d66843f73f07bf1b9ee7a9c4a881a57c Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 6 Jul 2025 07:41:30 -0400 Subject: [PATCH 13/18] Improve search interaction --- src/password_manager/manager.py | 27 +++++++++++++++++++----- src/tests/test_manager_search_display.py | 18 ++++++++++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index a19a066..d756aa5 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -1846,7 +1846,7 @@ class PasswordManager: print(colored(f"Error: Failed to modify entry: {e}", "red")) def handle_search_entries(self) -> None: - """Prompt for a query and display matching entries.""" + """Prompt for a query, list matches and optionally show details.""" try: clear_screen() query = input("Enter search string: ").strip() @@ -1861,10 +1861,27 @@ class PasswordManager: pause() return - print(colored("\n[+] Search Results:\n", "green")) - for match in results: - self.display_entry_details(match[0]) - pause() + while True: + clear_screen() + print(colored("\n[+] Search Results:\n", "green")) + for idx, label, username, _url, _b in results: + display_label = label + if username: + display_label += f" ({username})" + print(colored(f"{idx}. {display_label}", "cyan")) + + idx_input = input( + "Enter index to view details or press Enter to go back: " + ).strip() + if not idx_input: + break + if not idx_input.isdigit() or int(idx_input) not in [ + r[0] for r in results + ]: + print(colored("Invalid index.", "red")) + pause() + continue + self.show_entry_details_by_index(int(idx_input)) except Exception as e: logging.error(f"Failed to search entries: {e}", exc_info=True) print(colored(f"Error: Failed to search entries: {e}", "red")) diff --git a/src/tests/test_manager_search_display.py b/src/tests/test_manager_search_display.py index 2c46009..16c83c2 100644 --- a/src/tests/test_manager_search_display.py +++ b/src/tests/test_manager_search_display.py @@ -13,7 +13,7 @@ from password_manager.manager import PasswordManager, EncryptionMode from password_manager.config_manager import ConfigManager -def test_search_entries_shows_totp_details(monkeypatch, capsys): +def test_search_entries_prompt_for_details(monkeypatch, capsys): with TemporaryDirectory() as tmpdir: tmp_path = Path(tmpdir) vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) @@ -27,15 +27,25 @@ def test_search_entries_shows_totp_details(monkeypatch, capsys): pm.vault = vault pm.entry_manager = entry_mgr pm.backup_manager = backup_mgr + pm.parent_seed = TEST_SEED pm.nostr_client = SimpleNamespace() pm.fingerprint_dir = tmp_path pm.secret_mode_enabled = False entry_mgr.add_totp("Example", TEST_SEED) - monkeypatch.setattr("builtins.input", lambda *a, **k: "Example") + monkeypatch.setattr(pm.entry_manager, "get_totp_code", lambda *a, **k: "123456") + monkeypatch.setattr( + pm.entry_manager, "get_totp_time_remaining", lambda *a, **k: 1 + ) + monkeypatch.setattr("password_manager.manager.time.sleep", lambda *a, **k: None) + monkeypatch.setattr("password_manager.manager.timed_input", lambda *a, **k: "b") + + inputs = iter(["Example", "0", ""]) + monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs)) pm.handle_search_entries() out = capsys.readouterr().out - assert "Label: Example" in out - assert "Derivation Index" in out + assert "0. Example" in out + assert "Retrieved 2FA Code" in out + assert "123456" in out From 876ed71f74d8b82a136dfc840392e2e548009d07 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 6 Jul 2025 07:57:02 -0400 Subject: [PATCH 14/18] Simplify menus with Enter navigation --- src/main.py | 42 +++++++++++------------------ src/password_manager/manager.py | 10 +++---- src/tests/test_auto_sync.py | 2 +- src/tests/test_cli_invalid_input.py | 9 +++---- src/tests/test_inactivity_lock.py | 4 +-- src/tests/test_menu_options.py | 4 +-- src/tests/test_menu_search.py | 2 +- src/tests/test_settings_menu.py | 2 +- 8 files changed, 31 insertions(+), 44 deletions(-) diff --git a/src/main.py b/src/main.py index e2295b9..3b06edc 100644 --- a/src/main.py +++ b/src/main.py @@ -577,8 +577,7 @@ def handle_profiles_menu(password_manager: PasswordManager) -> None: print(color_text("2. Add a New Seed Profile", "menu")) print(color_text("3. Remove an Existing Seed Profile", "menu")) print(color_text("4. List All Seed Profiles", "menu")) - print(color_text("5. Back", "menu")) - choice = input("Select an option: ").strip() + choice = input("Select an option or press Enter to go back: ").strip() password_manager.update_activity() if choice == "1": if not password_manager.handle_switch_fingerprint(): @@ -589,7 +588,7 @@ def handle_profiles_menu(password_manager: PasswordManager) -> None: handle_remove_fingerprint(password_manager) elif choice == "4": handle_list_fingerprints(password_manager) - elif choice == "5": + elif not choice: break else: print(colored("Invalid choice.", "red")) @@ -617,8 +616,7 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None: print(color_text("5. Remove a relay by number", "menu")) print(color_text("6. Reset to default relays", "menu")) print(color_text("7. Display Nostr Public Key", "menu")) - print(color_text("8. Back", "menu")) - choice = input("Select an option: ").strip() + choice = input("Select an option or press Enter to go back: ").strip() password_manager.update_activity() if choice == "1": handle_post_to_nostr(password_manager) @@ -634,7 +632,7 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None: handle_reset_relays(password_manager) elif choice == "7": handle_display_npub(password_manager) - elif choice == "8": + elif not choice: break else: print(colored("Invalid choice.", "red")) @@ -659,8 +657,7 @@ def handle_settings(password_manager: PasswordManager) -> None: print(color_text("12. Lock Vault", "menu")) print(color_text("13. Stats", "menu")) print(color_text("14. Toggle Secret Mode", "menu")) - print(color_text("15. Back", "menu")) - choice = input("Select an option: ").strip() + choice = input("Select an option or press Enter to go back: ").strip() if choice == "1": handle_profiles_menu(password_manager) pause() @@ -707,7 +704,7 @@ def handle_settings(password_manager: PasswordManager) -> None: elif choice == "14": handle_toggle_secret_mode(password_manager) pause() - elif choice == "15": + elif not choice: break else: print(colored("Invalid choice.", "red")) @@ -730,7 +727,6 @@ def display_menu( 5. Modify an Existing Entry 6. 2FA Codes 7. Settings - 8. Exit """ display_fn = getattr(password_manager, "display_stats", None) if callable(display_fn): @@ -757,7 +753,8 @@ def display_menu( print(color_text(menu, "menu")) try: choice = timed_input( - "Enter your choice (1-8): ", inactivity_timeout + "Enter your choice (1-7) or press Enter to exit: ", + inactivity_timeout, ).strip() except TimeoutError: print(colored("Session timed out. Vault locked.", "yellow")) @@ -766,13 +763,10 @@ def display_menu( continue password_manager.update_activity() if not choice: - print( - colored( - "No input detected. Please enter a number between 1 and 8.", - "yellow", - ) - ) - continue # Re-display the menu without marking as invalid + logging.info("Exiting the program.") + print(colored("Exiting the program.", "green")) + password_manager.nostr_client.close_client_pool() + sys.exit(0) if choice == "1": while True: print(color_text("\nAdd Entry:", "menu")) @@ -782,8 +776,9 @@ def display_menu( print(color_text("4. Seed Phrase", "menu")) print(color_text("5. Nostr Key Pair", "menu")) print(color_text("6. PGP Key", "menu")) - print(color_text("7. Back", "menu")) - sub_choice = input("Select entry type: ").strip() + sub_choice = input( + "Select entry type or press Enter to go back: " + ).strip() password_manager.update_activity() if sub_choice == "1": password_manager.handle_add_password() @@ -803,7 +798,7 @@ def display_menu( elif sub_choice == "6": password_manager.handle_add_pgp() break - elif sub_choice == "7": + elif not sub_choice: break else: print(colored("Invalid choice.", "red")) @@ -826,11 +821,6 @@ def display_menu( elif choice == "7": password_manager.update_activity() handle_settings(password_manager) - elif choice == "8": - logging.info("Exiting the program.") - print(colored("Exiting the program.", "green")) - password_manager.nostr_client.close_client_pool() - sys.exit(0) else: print(colored("Invalid choice. Please select a valid option.", "red")) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index d756aa5..a0776e1 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -978,8 +978,7 @@ class PasswordManager: print("\nAdd TOTP:") print("1. Make 2FA (derive from seed)") print("2. Import 2FA (paste otpauth URI or secret)") - print("3. Back") - choice = input("Select option: ").strip() + choice = input("Select option or press Enter to go back: ").strip() if choice == "1": label = input("Label: ").strip() if not label: @@ -1062,7 +1061,7 @@ class PasswordManager: break except ValueError as err: print(colored(f"Error: {err}", "red")) - elif choice == "3": + elif not choice: return else: print(colored("Invalid choice.", "red")) @@ -1978,8 +1977,7 @@ class PasswordManager: print(color_text("5. Seed Phrase", "menu")) print(color_text("6. Nostr Key Pair", "menu")) print(color_text("7. PGP", "menu")) - print(color_text("8. Back", "menu")) - choice = input("Select entry type: ").strip() + choice = input("Select entry type or press Enter to go back: ").strip() if choice == "1": filter_kind = None elif choice == "2": @@ -1994,7 +1992,7 @@ class PasswordManager: filter_kind = EntryType.NOSTR.value elif choice == "7": filter_kind = EntryType.PGP.value - elif choice == "8": + elif not choice: return else: print(colored("Invalid choice.", "red")) diff --git a/src/tests/test_auto_sync.py b/src/tests/test_auto_sync.py index 7c8466e..53cf949 100644 --- a/src/tests/test_auto_sync.py +++ b/src/tests/test_auto_sync.py @@ -31,7 +31,7 @@ def test_auto_sync_triggers_post(monkeypatch): called = True monkeypatch.setattr(main, "handle_post_to_nostr", fake_post) - monkeypatch.setattr(main, "timed_input", lambda *_: "8") + monkeypatch.setattr(main, "timed_input", lambda *_: "") with pytest.raises(SystemExit): main.display_menu(pm, sync_interval=0.1) diff --git a/src/tests/test_cli_invalid_input.py b/src/tests/test_cli_invalid_input.py index 7fa4b16..6331c8c 100644 --- a/src/tests/test_cli_invalid_input.py +++ b/src/tests/test_cli_invalid_input.py @@ -52,12 +52,11 @@ def _make_pm(called, locked=None): def test_empty_and_non_numeric_choice(monkeypatch, capsys): called = {"add": False, "retrieve": False, "modify": False} pm, _ = _make_pm(called) - inputs = iter(["", "abc", "8"]) + inputs = iter(["abc", ""]) monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs)) with pytest.raises(SystemExit): main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000) out = capsys.readouterr().out - assert "No input detected" in out assert "Invalid choice. Please select a valid option." in out assert not any(called.values()) @@ -65,7 +64,7 @@ def test_empty_and_non_numeric_choice(monkeypatch, capsys): def test_out_of_range_menu(monkeypatch, capsys): called = {"add": False, "retrieve": False, "modify": False} pm, _ = _make_pm(called) - inputs = iter(["9", "8"]) + inputs = iter(["9", ""]) monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs)) with pytest.raises(SystemExit): main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000) @@ -77,7 +76,7 @@ def test_out_of_range_menu(monkeypatch, capsys): def test_invalid_add_entry_submenu(monkeypatch, capsys): called = {"add": False, "retrieve": False, "modify": False} pm, _ = _make_pm(called) - inputs = iter(["1", "8", "7", "8"]) + inputs = iter(["1", "8", "", ""]) monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs)) monkeypatch.setattr("builtins.input", lambda *_: next(inputs)) with pytest.raises(SystemExit): @@ -92,7 +91,7 @@ def test_inactivity_timeout_loop(monkeypatch, capsys): pm, locked = _make_pm(called) pm.last_activity = 0 monkeypatch.setattr(time, "time", lambda: 100.0) - monkeypatch.setattr(main, "timed_input", lambda *_: "8") + monkeypatch.setattr(main, "timed_input", lambda *_: "") with pytest.raises(SystemExit): main.display_menu(pm, sync_interval=1000, inactivity_timeout=0.1) out = capsys.readouterr().out diff --git a/src/tests/test_inactivity_lock.py b/src/tests/test_inactivity_lock.py index 46253f4..0c68561 100644 --- a/src/tests/test_inactivity_lock.py +++ b/src/tests/test_inactivity_lock.py @@ -36,7 +36,7 @@ def test_inactivity_triggers_lock(monkeypatch): unlock_vault=unlock_vault, ) - monkeypatch.setattr(main, "timed_input", lambda *_: "8") + monkeypatch.setattr(main, "timed_input", lambda *_: "") with pytest.raises(SystemExit): main.display_menu(pm, sync_interval=1000, inactivity_timeout=0.1) @@ -72,7 +72,7 @@ def test_input_timeout_triggers_lock(monkeypatch): unlock_vault=unlock_vault, ) - responses = iter([TimeoutError(), "8"]) + responses = iter([TimeoutError(), ""]) def fake_input(*_args, **_kwargs): val = next(responses) diff --git a/src/tests/test_menu_options.py b/src/tests/test_menu_options.py index c45ba9d..ff8e7cf 100644 --- a/src/tests/test_menu_options.py +++ b/src/tests/test_menu_options.py @@ -30,7 +30,7 @@ def _make_pm(calls): def test_menu_totp_option(monkeypatch): calls = [] pm = _make_pm(calls) - inputs = iter(["6", "8"]) + inputs = iter(["6", ""]) monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs)) monkeypatch.setattr(main, "handle_settings", lambda *_: None) with pytest.raises(SystemExit): @@ -41,7 +41,7 @@ def test_menu_totp_option(monkeypatch): def test_menu_settings_option(monkeypatch): calls = [] pm = _make_pm(calls) - inputs = iter(["7", "8"]) + inputs = iter(["7", ""]) monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs)) monkeypatch.setattr(main, "handle_settings", lambda *_: calls.append("settings")) with pytest.raises(SystemExit): diff --git a/src/tests/test_menu_search.py b/src/tests/test_menu_search.py index 7f1d70e..0e1d439 100644 --- a/src/tests/test_menu_search.py +++ b/src/tests/test_menu_search.py @@ -30,7 +30,7 @@ def _make_pm(called): def test_menu_search_option(monkeypatch): called = [] pm = _make_pm(called) - inputs = iter(["3", "8"]) + inputs = iter(["3", ""]) monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs)) monkeypatch.setattr("builtins.input", lambda *_: "query") with pytest.raises(SystemExit): diff --git a/src/tests/test_settings_menu.py b/src/tests/test_settings_menu.py index bd8a6f0..6899822 100644 --- a/src/tests/test_settings_menu.py +++ b/src/tests/test_settings_menu.py @@ -93,7 +93,7 @@ def test_settings_menu_additional_backup(monkeypatch): tmp_path = Path(tmpdir) pm, cfg_mgr, fp_mgr = setup_pm(tmp_path, monkeypatch) - inputs = iter(["10", "15"]) + inputs = iter(["10", ""]) with patch("main.handle_set_additional_backup_location") as handler: with patch("builtins.input", side_effect=lambda *_: next(inputs)): main.handle_settings(pm) From 512a92a65b6311fceb03b7e9d4765d735990b6a2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 6 Jul 2025 08:10:12 -0400 Subject: [PATCH 15/18] feat: show seed fingerprint on each screen --- src/main.py | 28 +++++++++++++++----- src/password_manager/manager.py | 46 +++++++++++++++++++-------------- src/utils/__init__.py | 3 ++- src/utils/terminal_utils.py | 10 +++++++ 4 files changed, 60 insertions(+), 27 deletions(-) diff --git a/src/main.py b/src/main.py index 3b06edc..0415ddd 100644 --- a/src/main.py +++ b/src/main.py @@ -20,7 +20,13 @@ from nostr.client import NostrClient from password_manager.entry_types import EntryType from constants import INACTIVITY_TIMEOUT, initialize_app from utils.password_prompt import PasswordPromptError -from utils import timed_input, copy_to_clipboard, clear_screen, pause +from utils import ( + timed_input, + copy_to_clipboard, + clear_screen, + pause, + clear_and_print_fingerprint, +) from local_bip85.bip85 import Bip85Error @@ -571,7 +577,9 @@ def handle_toggle_secret_mode(pm: PasswordManager) -> None: def handle_profiles_menu(password_manager: PasswordManager) -> None: """Submenu for managing seed profiles.""" while True: - clear_screen() + clear_and_print_fingerprint( + getattr(password_manager, "current_fingerprint", None) + ) print(color_text("\nProfiles:", "menu")) print(color_text("1. Switch Seed Profile", "menu")) print(color_text("2. Add a New Seed Profile", "menu")) @@ -607,7 +615,9 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None: return while True: - clear_screen() + clear_and_print_fingerprint( + getattr(password_manager, "current_fingerprint", None) + ) print(color_text("\nNostr Settings:", "menu")) print(color_text("1. Backup to Nostr", "menu")) print(color_text("2. Restore from Nostr", "menu")) @@ -641,7 +651,9 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None: def handle_settings(password_manager: PasswordManager) -> None: """Interactive settings menu with submenus for profiles and Nostr.""" while True: - clear_screen() + clear_and_print_fingerprint( + getattr(password_manager, "current_fingerprint", None) + ) print(color_text("\nSettings:", "menu")) print(color_text("1. Profiles", "menu")) print(color_text("2. Nostr", "menu")) @@ -733,7 +745,9 @@ def display_menu( display_fn() pause() while True: - clear_screen() + clear_and_print_fingerprint( + getattr(password_manager, "current_fingerprint", None) + ) if time.time() - password_manager.last_activity > inactivity_timeout: print(colored("Session timed out. Vault locked.", "yellow")) password_manager.lock_vault() @@ -805,7 +819,9 @@ def display_menu( elif choice == "2": password_manager.update_activity() password_manager.handle_retrieve_entry() - clear_screen() + clear_and_print_fingerprint( + getattr(password_manager, "current_fingerprint", None) + ) elif choice == "3": password_manager.update_activity() password_manager.handle_search_entries() diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index a0776e1..36112bb 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -52,7 +52,11 @@ from utils.password_prompt import ( ) from utils.memory_protection import InMemorySecret from utils.clipboard import copy_to_clipboard -from utils.terminal_utils import clear_screen, pause +from utils.terminal_utils import ( + clear_screen, + pause, + clear_and_print_fingerprint, +) from constants import MIN_HEALTHY_RELAYS from constants import ( @@ -886,7 +890,7 @@ class PasswordManager: def handle_add_password(self) -> None: try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) website_name = input("Enter the label or website name: ").strip() if not website_name: print(colored("Error: Label cannot be empty.", "red")) @@ -973,7 +977,7 @@ class PasswordManager: def handle_add_totp(self) -> None: """Add a TOTP entry either derived from the seed or imported.""" try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) while True: print("\nAdd TOTP:") print("1. Make 2FA (derive from seed)") @@ -1073,7 +1077,7 @@ class PasswordManager: def handle_add_ssh_key(self) -> None: """Add an SSH key pair entry and display the derived keys.""" try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) label = input("Label: ").strip() if not label: print(colored("Error: Label cannot be empty.", "red")) @@ -1115,7 +1119,7 @@ class PasswordManager: def handle_add_seed(self) -> None: """Add a derived BIP-39 seed phrase entry.""" try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) label = input("Label: ").strip() if not label: print(colored("Error: Label cannot be empty.", "red")) @@ -1171,7 +1175,7 @@ class PasswordManager: def handle_add_pgp(self) -> None: """Add a PGP key entry and display the generated key.""" try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) label = input("Label: ").strip() if not label: print(colored("Error: Label cannot be empty.", "red")) @@ -1224,7 +1228,7 @@ class PasswordManager: def handle_add_nostr_key(self) -> None: """Add a Nostr key entry and display the derived keys.""" try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) label = input("Label: ").strip() if not label: print(colored("Error: Label cannot be empty.", "red")) @@ -1291,7 +1295,7 @@ class PasswordManager: and displaying the corresponding password and associated details. """ try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) index_input = input( "Enter the index number of the entry to retrieve: " ).strip() @@ -1609,7 +1613,7 @@ class PasswordManager: and new details to update. """ try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) index_input = input( "Enter the index number of the entry to modify: " ).strip() @@ -1847,7 +1851,7 @@ class PasswordManager: def handle_search_entries(self) -> None: """Prompt for a query, list matches and optionally show details.""" try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) query = input("Enter search string: ").strip() if not query: print(colored("No search string provided.", "yellow")) @@ -1861,7 +1865,7 @@ class PasswordManager: return while True: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) print(colored("\n[+] Search Results:\n", "green")) for idx, label, username, _url, _b in results: display_label = label @@ -1968,7 +1972,7 @@ class PasswordManager: """List entries and optionally show details.""" try: while True: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) print(color_text("\nList Entries:", "menu")) print(color_text("1. All", "menu")) print(color_text("2. Passwords", "menu")) @@ -2002,7 +2006,9 @@ class PasswordManager: if not summaries: continue while True: - clear_screen() + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None) + ) print(colored("\n[+] Entries:\n", "green")) for idx, etype, label in summaries: if filter_kind is None: @@ -2082,7 +2088,7 @@ class PasswordManager: totp_list.sort(key=lambda t: t[0].lower()) print(colored("Press 'b' then Enter to return to the menu.", "cyan")) while True: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) print(colored("Press 'b' then Enter to return to the menu.", "cyan")) generated = [t for t in totp_list if not t[3]] imported_list = [t for t in totp_list if t[3]] @@ -2137,7 +2143,7 @@ class PasswordManager: Handles verifying the script's checksum against the stored checksum to ensure integrity. """ try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) current_checksum = calculate_checksum(__file__) try: verified = verify_checksum(current_checksum, SCRIPT_CHECKSUM_FILE) @@ -2172,7 +2178,7 @@ class PasswordManager: print(colored("Operation cancelled.", "yellow")) return try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) script_path = Path(__file__).resolve() if update_checksum_file(str(script_path), str(SCRIPT_CHECKSUM_FILE)): print( @@ -2282,7 +2288,7 @@ class PasswordManager: ) -> Path | None: """Export the current database to an encrypted portable file.""" try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) path = export_backup( self.vault, self.backup_manager, @@ -2299,7 +2305,7 @@ class PasswordManager: def handle_import_database(self, src: Path) -> None: """Import a portable database file, replacing the current index.""" try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) import_backup( self.vault, self.backup_manager, @@ -2314,7 +2320,7 @@ class PasswordManager: def handle_export_totp_codes(self) -> Path | None: """Export all 2FA codes to a JSON file for other authenticator apps.""" try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) data = self.entry_manager.vault.load_index() entries = data.get("entries", {}) @@ -2374,7 +2380,7 @@ class PasswordManager: Handles the backup and reveal of the parent seed. """ try: - clear_screen() + clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) print(colored("\n=== Backup Parent Seed ===", "yellow")) print( colored( diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 8df494f..25a2ca0 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -28,7 +28,7 @@ try: from .input_utils import timed_input from .memory_protection import InMemorySecret from .clipboard import copy_to_clipboard - from .terminal_utils import clear_screen, pause + from .terminal_utils import clear_screen, pause, clear_and_print_fingerprint if logger.isEnabledFor(logging.DEBUG): logger.info("Modules imported successfully.") @@ -57,5 +57,6 @@ __all__ = [ "InMemorySecret", "copy_to_clipboard", "clear_screen", + "clear_and_print_fingerprint", "pause", ] diff --git a/src/utils/terminal_utils.py b/src/utils/terminal_utils.py index 563e0fe..a520074 100644 --- a/src/utils/terminal_utils.py +++ b/src/utils/terminal_utils.py @@ -3,11 +3,21 @@ import sys +from termcolor import colored + + def clear_screen() -> None: """Clear the terminal screen using an ANSI escape code.""" print("\033c", end="") +def clear_and_print_fingerprint(fingerprint: str | None) -> None: + """Clear the screen and optionally display the current fingerprint.""" + clear_screen() + if fingerprint: + print(colored(f"Seed Profile: {fingerprint}", "green")) + + def pause(message: str = "Press Enter to continue...") -> None: """Wait for the user to press Enter before proceeding.""" if not sys.stdin or not sys.stdin.isatty(): From 06587afb3538b3c95678aecdf106ee347c5d4f4f Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 6 Jul 2025 08:19:12 -0400 Subject: [PATCH 16/18] Allow Enter to exit 2FA menus --- src/password_manager/manager.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 36112bb..e019ec6 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -1317,7 +1317,7 @@ class PasswordManager: period = int(entry.get("period", 30)) notes = entry.get("notes", "") print(colored(f"Retrieving 2FA code for '{label}'.", "cyan")) - print(colored("Press 'b' then Enter to return to the menu.", "cyan")) + print(colored("Press Enter to return to the menu.", "cyan")) try: while True: code = self.entry_manager.get_totp_code(index, self.parent_seed) @@ -1346,7 +1346,10 @@ class PasswordManager: sys.stdout.flush() try: user_input = timed_input("", 1) - if user_input.strip().lower() == "b": + if ( + user_input.strip() == "" + or user_input.strip().lower() == "b" + ): exit_loop = True break except TimeoutError: @@ -2086,10 +2089,10 @@ class PasswordManager: return totp_list.sort(key=lambda t: t[0].lower()) - print(colored("Press 'b' then Enter to return to the menu.", "cyan")) + print(colored("Press Enter to return to the menu.", "cyan")) while True: clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) - print(colored("Press 'b' then Enter to return to the menu.", "cyan")) + print(colored("Press Enter to return to the menu.", "cyan")) generated = [t for t in totp_list if not t[3]] imported_list = [t for t in totp_list if t[3]] if generated: @@ -2127,7 +2130,7 @@ class PasswordManager: sys.stdout.flush() try: user_input = timed_input("", 1) - if user_input.strip().lower() == "b": + if user_input.strip() == "" or user_input.strip().lower() == "b": break except TimeoutError: pass From a3d8e6cf09584cecd69550140527556d0c0aa7aa Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 6 Jul 2025 08:36:01 -0400 Subject: [PATCH 17/18] Update menu snippets for new UI --- README.md | 22 +++++++++++----------- landing/index.html | 11 ++++++----- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 29859ea..dc17765 100644 --- a/README.md +++ b/README.md @@ -220,18 +220,18 @@ python src/main.py Example menu: - ```bash - Select an option: - 1. Add Entry - 2. Retrieve Entry - 3. Search Entries - 4. Modify an Existing Entry - 5. 2FA Codes - 6. Settings - 7. Exit + ```bash + Select an option: + 1. Add Entry + 2. Retrieve Entry + 3. Search Entries + 4. List Entries + 5. Modify an Existing Entry + 6. 2FA Codes + 7. Settings - Enter your choice (1-7): - ``` + Enter your choice (1-7) or press Enter to exit: + ``` When choosing **Add Entry**, you can now select **Password**, **2FA (TOTP)**, **SSH Key**, **Seed Phrase**, or **PGP Key**. diff --git a/landing/index.html b/landing/index.html index 60b4612..bb7a0fa 100644 --- a/landing/index.html +++ b/landing/index.html @@ -105,12 +105,13 @@ Select an option: 1. Add Entry 2. Retrieve Entry -3. Modify an Existing Entry -4. 2FA Codes -5. Settings -6. Exit +3. Search Entries +4. List Entries +5. Modify an Existing Entry +6. 2FA Codes +7. Settings -Enter your choice (1-6): +Enter your choice (1-7) or press Enter to exit:

Secret Mode

When Secret Mode is enabled, retrieved passwords are copied directly to your clipboard instead of displayed. The clipboard clears automatically after a delay you set.

From c35c4d66a0b98b80705b3edc7c87808df6688bd6 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 6 Jul 2025 09:25:45 -0400 Subject: [PATCH 18/18] Enhance CLI breadcrumbs and update menu color --- src/main.py | 19 +++++-- src/password_manager/manager.py | 97 ++++++++++++++++++++++++++------- src/utils/color_scheme.py | 2 +- src/utils/terminal_utils.py | 11 +++- 4 files changed, 101 insertions(+), 28 deletions(-) diff --git a/src/main.py b/src/main.py index 0415ddd..617550b 100644 --- a/src/main.py +++ b/src/main.py @@ -578,7 +578,8 @@ def handle_profiles_menu(password_manager: PasswordManager) -> None: """Submenu for managing seed profiles.""" while True: clear_and_print_fingerprint( - getattr(password_manager, "current_fingerprint", None) + getattr(password_manager, "current_fingerprint", None), + "Main Menu > Settings > Profiles", ) print(color_text("\nProfiles:", "menu")) print(color_text("1. Switch Seed Profile", "menu")) @@ -616,7 +617,8 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None: while True: clear_and_print_fingerprint( - getattr(password_manager, "current_fingerprint", None) + getattr(password_manager, "current_fingerprint", None), + "Main Menu > Settings > Nostr", ) print(color_text("\nNostr Settings:", "menu")) print(color_text("1. Backup to Nostr", "menu")) @@ -652,7 +654,8 @@ def handle_settings(password_manager: PasswordManager) -> None: """Interactive settings menu with submenus for profiles and Nostr.""" while True: clear_and_print_fingerprint( - getattr(password_manager, "current_fingerprint", None) + getattr(password_manager, "current_fingerprint", None), + "Main Menu > Settings", ) print(color_text("\nSettings:", "menu")) print(color_text("1. Profiles", "menu")) @@ -746,7 +749,8 @@ def display_menu( pause() while True: clear_and_print_fingerprint( - getattr(password_manager, "current_fingerprint", None) + getattr(password_manager, "current_fingerprint", None), + "Main Menu", ) if time.time() - password_manager.last_activity > inactivity_timeout: print(colored("Session timed out. Vault locked.", "yellow")) @@ -783,6 +787,10 @@ def display_menu( sys.exit(0) if choice == "1": while True: + clear_and_print_fingerprint( + getattr(password_manager, "current_fingerprint", None), + "Main Menu > Add Entry", + ) print(color_text("\nAdd Entry:", "menu")) print(color_text("1. Password", "menu")) print(color_text("2. 2FA (TOTP)", "menu")) @@ -820,7 +828,8 @@ def display_menu( password_manager.update_activity() password_manager.handle_retrieve_entry() clear_and_print_fingerprint( - getattr(password_manager, "current_fingerprint", None) + getattr(password_manager, "current_fingerprint", None), + "Main Menu", ) elif choice == "3": password_manager.update_activity() diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index e019ec6..536cde7 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -890,7 +890,10 @@ class PasswordManager: def handle_add_password(self) -> None: try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Add Entry > Password", + ) website_name = input("Enter the label or website name: ").strip() if not website_name: print(colored("Error: Label cannot be empty.", "red")) @@ -977,7 +980,10 @@ class PasswordManager: def handle_add_totp(self) -> None: """Add a TOTP entry either derived from the seed or imported.""" try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Add Entry > 2FA (TOTP)", + ) while True: print("\nAdd TOTP:") print("1. Make 2FA (derive from seed)") @@ -1077,7 +1083,10 @@ class PasswordManager: def handle_add_ssh_key(self) -> None: """Add an SSH key pair entry and display the derived keys.""" try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Add Entry > SSH Key", + ) label = input("Label: ").strip() if not label: print(colored("Error: Label cannot be empty.", "red")) @@ -1119,7 +1128,10 @@ class PasswordManager: def handle_add_seed(self) -> None: """Add a derived BIP-39 seed phrase entry.""" try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Add Entry > Seed Phrase", + ) label = input("Label: ").strip() if not label: print(colored("Error: Label cannot be empty.", "red")) @@ -1175,7 +1187,10 @@ class PasswordManager: def handle_add_pgp(self) -> None: """Add a PGP key entry and display the generated key.""" try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Add Entry > PGP Key", + ) label = input("Label: ").strip() if not label: print(colored("Error: Label cannot be empty.", "red")) @@ -1228,7 +1243,10 @@ class PasswordManager: def handle_add_nostr_key(self) -> None: """Add a Nostr key entry and display the derived keys.""" try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Add Entry > Nostr Key Pair", + ) label = input("Label: ").strip() if not label: print(colored("Error: Label cannot be empty.", "red")) @@ -1295,7 +1313,10 @@ class PasswordManager: and displaying the corresponding password and associated details. """ try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Retrieve Entry", + ) index_input = input( "Enter the index number of the entry to retrieve: " ).strip() @@ -1616,7 +1637,10 @@ class PasswordManager: and new details to update. """ try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Modify Entry", + ) index_input = input( "Enter the index number of the entry to modify: " ).strip() @@ -1854,7 +1878,10 @@ class PasswordManager: def handle_search_entries(self) -> None: """Prompt for a query, list matches and optionally show details.""" try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Search Entries", + ) query = input("Enter search string: ").strip() if not query: print(colored("No search string provided.", "yellow")) @@ -1868,7 +1895,10 @@ class PasswordManager: return while True: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Search Entries", + ) print(colored("\n[+] Search Results:\n", "green")) for idx, label, username, _url, _b in results: display_label = label @@ -1975,7 +2005,10 @@ class PasswordManager: """List entries and optionally show details.""" try: while True: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > List Entries", + ) print(color_text("\nList Entries:", "menu")) print(color_text("1. All", "menu")) print(color_text("2. Passwords", "menu")) @@ -2010,7 +2043,8 @@ class PasswordManager: continue while True: clear_and_print_fingerprint( - getattr(self, "current_fingerprint", None) + getattr(self, "current_fingerprint", None), + "Main Menu > List Entries", ) print(colored("\n[+] Entries:\n", "green")) for idx, etype, label in summaries: @@ -2072,6 +2106,10 @@ class PasswordManager: def handle_display_totp_codes(self) -> None: """Display all stored TOTP codes with a countdown progress bar.""" try: + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > 2FA Codes", + ) data = self.entry_manager.vault.load_index() entries = data.get("entries", {}) totp_list: list[tuple[str, int, int, bool]] = [] @@ -2091,7 +2129,10 @@ class PasswordManager: totp_list.sort(key=lambda t: t[0].lower()) print(colored("Press Enter to return to the menu.", "cyan")) while True: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > 2FA Codes", + ) print(colored("Press Enter to return to the menu.", "cyan")) generated = [t for t in totp_list if not t[3]] imported_list = [t for t in totp_list if t[3]] @@ -2146,7 +2187,10 @@ class PasswordManager: Handles verifying the script's checksum against the stored checksum to ensure integrity. """ try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Settings > Verify Script Checksum", + ) current_checksum = calculate_checksum(__file__) try: verified = verify_checksum(current_checksum, SCRIPT_CHECKSUM_FILE) @@ -2181,7 +2225,10 @@ class PasswordManager: print(colored("Operation cancelled.", "yellow")) return try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Settings > Generate Script Checksum", + ) script_path = Path(__file__).resolve() if update_checksum_file(str(script_path), str(SCRIPT_CHECKSUM_FILE)): print( @@ -2291,7 +2338,10 @@ class PasswordManager: ) -> Path | None: """Export the current database to an encrypted portable file.""" try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Settings > Export database", + ) path = export_backup( self.vault, self.backup_manager, @@ -2308,7 +2358,10 @@ class PasswordManager: def handle_import_database(self, src: Path) -> None: """Import a portable database file, replacing the current index.""" try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Settings > Import database", + ) import_backup( self.vault, self.backup_manager, @@ -2323,7 +2376,10 @@ class PasswordManager: def handle_export_totp_codes(self) -> Path | None: """Export all 2FA codes to a JSON file for other authenticator apps.""" try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Settings > Export 2FA codes", + ) data = self.entry_manager.vault.load_index() entries = data.get("entries", {}) @@ -2383,7 +2439,10 @@ class PasswordManager: Handles the backup and reveal of the parent seed. """ try: - clear_and_print_fingerprint(getattr(self, "current_fingerprint", None)) + clear_and_print_fingerprint( + getattr(self, "current_fingerprint", None), + "Main Menu > Settings > Backup Parent Seed", + ) print(colored("\n=== Backup Parent Seed ===", "yellow")) print( colored( diff --git a/src/utils/color_scheme.py b/src/utils/color_scheme.py index d468074..db6798f 100644 --- a/src/utils/color_scheme.py +++ b/src/utils/color_scheme.py @@ -18,7 +18,7 @@ _COLOR_MAP = { "deterministic": "red", "imported": "orange", "index": "yellow", - "menu": "blue", + "menu": "cyan", "stats": "green", "default": "white", } diff --git a/src/utils/terminal_utils.py b/src/utils/terminal_utils.py index a520074..00f1975 100644 --- a/src/utils/terminal_utils.py +++ b/src/utils/terminal_utils.py @@ -11,11 +11,16 @@ def clear_screen() -> None: print("\033c", end="") -def clear_and_print_fingerprint(fingerprint: str | None) -> None: - """Clear the screen and optionally display the current fingerprint.""" +def clear_and_print_fingerprint( + fingerprint: str | None, breadcrumb: str | None = None +) -> None: + """Clear the screen and optionally display the current fingerprint and path.""" clear_screen() if fingerprint: - print(colored(f"Seed Profile: {fingerprint}", "green")) + header = f"Seed Profile: {fingerprint}" + if breadcrumb: + header += f" > {breadcrumb}" + print(colored(header, "green")) def pause(message: str = "Press Enter to continue...") -> None: