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] 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)