diff --git a/README.md b/README.md index a4d9e50..6488169 100644 --- a/README.md +++ b/README.md @@ -378,8 +378,12 @@ SeedPass allows you to manage multiple seed profiles (previously referred to as 3. Enter the number corresponding to the seed profile you wish to switch to. 4. Enter the master password associated with that seed profile. -- **List All Seed Profiles:** +- **List All Seed Profiles:** In the **Profiles** menu, choose "List All Seed Profiles" to view all existing profiles. +- **Set Seed Profile Name:** + In the **Profiles** menu, choose "Set Seed Profile Name" to assign an optional + label to the currently selected profile. The name is stored locally and shown + alongside the fingerprint in menus. **Note:** The term "seed profile" is used to represent different sets of seeds you can manage within SeedPass. This provides an intuitive way to handle multiple identities or sets of passwords. diff --git a/docs/docs/content/index.md b/docs/docs/content/index.md index 09b38fd..7489fdf 100644 --- a/docs/docs/content/index.md +++ b/docs/docs/content/index.md @@ -368,6 +368,8 @@ SeedPass allows you to manage multiple seed profiles (previously referred to as - **List All Seed Profiles:** - In the **Profiles** menu, choose "List All Seed Profiles" to view all existing profiles. +- **Set Seed Profile Name:** + - In the **Profiles** menu, choose "Set Seed Profile Name" to assign a label to the current profile. The name is stored locally and shown next to the fingerprint. **Note:** The term "seed profile" is used to represent different sets of seeds you can manage within SeedPass. This provides an intuitive way to handle multiple identities or sets of passwords. diff --git a/src/main.py b/src/main.py index 1751c3f..565ade0 100644 --- a/src/main.py +++ b/src/main.py @@ -151,7 +151,8 @@ def handle_switch_fingerprint(password_manager: PasswordManager): print(colored("Available Seed Profiles:", "cyan")) for idx, fp in enumerate(fingerprints, start=1): - print(colored(f"{idx}. {fp}", "cyan")) + label = password_manager.fingerprint_manager.display_name(fp) + print(colored(f"{idx}. {label}", "cyan")) choice = input("Select a seed profile by number to switch: ").strip() if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)): @@ -195,7 +196,8 @@ def handle_remove_fingerprint(password_manager: PasswordManager): print(colored("Available Seed Profiles:", "cyan")) for idx, fp in enumerate(fingerprints, start=1): - print(colored(f"{idx}. {fp}", "cyan")) + label = password_manager.fingerprint_manager.display_name(fp) + print(colored(f"{idx}. {label}", "cyan")) choice = input("Select a seed profile by number to remove: ").strip() if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)): @@ -239,7 +241,8 @@ def handle_list_fingerprints(password_manager: PasswordManager): print(colored("Available Seed Profiles:", "cyan")) for fp in fingerprints: - print(colored(f"- {fp}", "cyan")) + label = password_manager.fingerprint_manager.display_name(fp) + print(colored(f"- {label}", "cyan")) pause() except Exception as e: logging.error(f"Error listing seed profiles: {e}", exc_info=True) @@ -641,6 +644,25 @@ def handle_set_additional_backup_location(pm: PasswordManager) -> None: print(colored(f"Error: {e}", "red")) +def handle_set_profile_name(pm: PasswordManager) -> None: + """Set or clear the custom name for the current seed profile.""" + fp = getattr(pm.fingerprint_manager, "current_fingerprint", None) + if not fp: + print(colored("No seed profile selected.", "red")) + return + current = pm.fingerprint_manager.get_name(fp) + if current: + print(colored(f"Current name: {current}", "cyan")) + else: + print(colored("No custom name set.", "cyan")) + value = input("Enter new name (leave blank to remove): ").strip() + if pm.fingerprint_manager.set_name(fp, value or None): + if value: + print(colored("Name updated.", "green")) + else: + print(colored("Name removed.", "green")) + + def handle_toggle_secret_mode(pm: PasswordManager) -> None: """Toggle secret mode and adjust clipboard delay.""" cfg = pm.config_manager @@ -756,6 +778,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. Set Seed Profile Name", "menu")) choice = input("Select an option or press Enter to go back: ").strip() password_manager.update_activity() if choice == "1": @@ -767,6 +790,8 @@ def handle_profiles_menu(password_manager: PasswordManager) -> None: handle_remove_fingerprint(password_manager) elif choice == "4": handle_list_fingerprints(password_manager) + elif choice == "5": + handle_set_profile_name(password_manager) elif not choice: break else: diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index b2a2452..8f94817 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -329,8 +329,13 @@ class PasswordManager: print(colored("\nAvailable Seed Profiles:", "cyan")) for idx, fp in enumerate(fingerprints, start=1): + label = ( + self.fingerprint_manager.display_name(fp) + if hasattr(self.fingerprint_manager, "display_name") + else fp + ) marker = " *" if fp == current else "" - print(colored(f"{idx}. {fp}{marker}", "cyan")) + print(colored(f"{idx}. {label}{marker}", "cyan")) print(colored(f"{len(fingerprints)+1}. Add a new seed profile", "cyan")) @@ -532,7 +537,12 @@ class PasswordManager: print(colored("\nAvailable Seed Profiles:", "cyan")) fingerprints = self.fingerprint_manager.list_fingerprints() for idx, fp in enumerate(fingerprints, start=1): - print(colored(f"{idx}. {fp}", "cyan")) + display = ( + self.fingerprint_manager.display_name(fp) + if hasattr(self.fingerprint_manager, "display_name") + else fp + ) + print(colored(f"{idx}. {display}", "cyan")) choice = input("Select a seed profile by number to switch: ").strip() if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)): @@ -680,7 +690,12 @@ class PasswordManager: print(colored("Available Seed Profiles:", "cyan")) for idx, fp in enumerate(fingerprints, start=1): - print(colored(f"{idx}. {fp}", "cyan")) + label = ( + self.fingerprint_manager.display_name(fp) + if hasattr(self.fingerprint_manager, "display_name") + else fp + ) + print(colored(f"{idx}. {label}", "cyan")) choice = input("Select a seed profile by number: ").strip() if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)): diff --git a/src/utils/fingerprint_manager.py b/src/utils/fingerprint_manager.py index 34ee4a0..470b063 100644 --- a/src/utils/fingerprint_manager.py +++ b/src/utils/fingerprint_manager.py @@ -34,7 +34,11 @@ class FingerprintManager: self.app_dir = app_dir self.fingerprints_file = self.app_dir / "fingerprints.json" self._ensure_app_directory() - self.fingerprints, self.current_fingerprint = self._load_fingerprints() + ( + self.fingerprints, + self.current_fingerprint, + self.names, + ) = self._load_fingerprints() def get_current_fingerprint_dir(self) -> Optional[Path]: """ @@ -62,25 +66,26 @@ class FingerprintManager: ) raise - def _load_fingerprints(self) -> tuple[list[str], Optional[str]]: - """Return stored fingerprints and the last used fingerprint.""" + def _load_fingerprints(self) -> tuple[list[str], Optional[str], dict[str, str]]: + """Return stored fingerprints, the last used fingerprint, and name mapping.""" try: if self.fingerprints_file.exists(): with open(self.fingerprints_file, "r") as f: data = json.load(f) fingerprints = data.get("fingerprints", []) current = data.get("last_used") + names = data.get("names", {}) logger.debug( f"Loaded fingerprints: {fingerprints} (last used: {current})" ) - return fingerprints, current + return fingerprints, current, names logger.debug( "fingerprints.json not found. Initializing empty fingerprint list." ) - return [], None + return [], None, {} except Exception as e: logger.error(f"Failed to load fingerprints: {e}", exc_info=True) - return [], None + return [], None, {} def _save_fingerprints(self): """ @@ -92,6 +97,7 @@ class FingerprintManager: { "fingerprints": self.fingerprints, "last_used": self.current_fingerprint, + "names": self.names, }, f, indent=4, @@ -116,6 +122,7 @@ class FingerprintManager: fingerprint = generate_fingerprint(seed_phrase) if fingerprint and fingerprint not in self.fingerprints: self.fingerprints.append(fingerprint) + self.names.setdefault(fingerprint, "") self.current_fingerprint = fingerprint self._save_fingerprints() logger.info(f"Fingerprint {fingerprint} added successfully.") @@ -144,6 +151,7 @@ class FingerprintManager: if fingerprint in self.fingerprints: try: self.fingerprints.remove(fingerprint) + self.names.pop(fingerprint, None) if self.current_fingerprint == fingerprint: self.current_fingerprint = ( self.fingerprints[0] if self.fingerprints else None @@ -198,6 +206,26 @@ class FingerprintManager: logger.error(f"Fingerprint {fingerprint} not found.") return False + def set_name(self, fingerprint: str, name: str | None) -> bool: + """Set a custom name for a fingerprint.""" + if fingerprint not in self.fingerprints: + return False + if name: + self.names[fingerprint] = name + else: + self.names.pop(fingerprint, None) + self._save_fingerprints() + return True + + def get_name(self, fingerprint: str) -> Optional[str]: + """Return the custom name for ``fingerprint`` if set.""" + return self.names.get(fingerprint) or None + + def display_name(self, fingerprint: str) -> str: + """Return name and fingerprint for display.""" + name = self.get_name(fingerprint) + return f"{name} ({fingerprint})" if name else fingerprint + def get_fingerprint_directory(self, fingerprint: str) -> Optional[Path]: """ Retrieves the directory path for a given fingerprint. diff --git a/src/utils/terminal_utils.py b/src/utils/terminal_utils.py index f1e05e7..3b1e853 100644 --- a/src/utils/terminal_utils.py +++ b/src/utils/terminal_utils.py @@ -8,6 +8,20 @@ from termcolor import colored from utils.color_scheme import color_text +def format_profile(fingerprint: str | None, pm=None) -> str | None: + """Return display string for a fingerprint with optional custom name.""" + if not fingerprint: + return None + if pm and getattr(pm, "fingerprint_manager", None): + try: + name = pm.fingerprint_manager.get_name(fingerprint) + if name: + return f"{name} ({fingerprint})" + except Exception: + pass + return fingerprint + + def clear_screen() -> None: """Clear the terminal screen using an ANSI escape code.""" print("\033c", end="") @@ -18,16 +32,17 @@ def clear_and_print_fingerprint( breadcrumb: str | None = None, parent_fingerprint: str | None = None, child_fingerprint: str | None = None, + pm=None, ) -> None: """Clear the screen and optionally display the current fingerprint and path.""" clear_screen() header_fp = None if parent_fingerprint and child_fingerprint: - header_fp = f"{parent_fingerprint} > Managed Account > {child_fingerprint}" + header_fp = f"{format_profile(parent_fingerprint, pm)} > Managed Account > {format_profile(child_fingerprint, pm)}" elif fingerprint: - header_fp = fingerprint + header_fp = format_profile(fingerprint, pm) elif parent_fingerprint or child_fingerprint: - header_fp = parent_fingerprint or child_fingerprint + header_fp = format_profile(parent_fingerprint or child_fingerprint, pm) if header_fp: header = f"Seed Profile: {header_fp}" if breadcrumb: @@ -36,15 +51,15 @@ def clear_and_print_fingerprint( def clear_and_print_profile_chain( - fingerprints: list[str] | None, breadcrumb: str | None = None + fingerprints: list[str] | None, breadcrumb: str | None = None, pm=None ) -> None: """Clear the screen and display a chain of fingerprints.""" clear_screen() if not fingerprints: return - chain = fingerprints[0] + chain = format_profile(fingerprints[0], pm) for fp in fingerprints[1:]: - chain += f" > Managed Account > {fp}" + chain += f" > Managed Account > {format_profile(fp, pm)}" header = f"Seed Profile: {chain}" if breadcrumb: header += f" > {breadcrumb}" @@ -63,11 +78,11 @@ def clear_header_with_notification( clear_screen() header_fp = None if parent_fingerprint and child_fingerprint: - header_fp = f"{parent_fingerprint} > Managed Account > {child_fingerprint}" + header_fp = f"{format_profile(parent_fingerprint, pm)} > Managed Account > {format_profile(child_fingerprint, pm)}" elif fingerprint: - header_fp = fingerprint + header_fp = format_profile(fingerprint, pm) elif parent_fingerprint or child_fingerprint: - header_fp = parent_fingerprint or child_fingerprint + header_fp = format_profile(parent_fingerprint or child_fingerprint, pm) if header_fp: header = f"Seed Profile: {header_fp}" if breadcrumb: