from __future__ import annotations import logging import sys from typing import TYPE_CHECKING from termcolor import colored from .entry_types import EntryType import seedpass.core.manager as manager_module from utils.color_scheme import color_text from utils.input_utils import timed_input from utils.terminal_utils import clear_header_with_notification if TYPE_CHECKING: # pragma: no cover - typing only from .manager import PasswordManager class MenuHandler: """Handle interactive menu operations for :class:`PasswordManager`.""" def __init__(self, manager: PasswordManager) -> None: self.manager = manager def handle_list_entries(self) -> None: """List entries and optionally show details.""" pm = self.manager try: while True: fp, parent_fp, child_fp = pm.header_fingerprint_args clear_header_with_notification( pm, fp, "Main Menu > List Entries", parent_fingerprint=parent_fp, child_fingerprint=child_fp, ) 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. Key/Value", "menu")) print(color_text("9. Managed Account", "menu")) choice = input("Select entry type or press Enter to go back: ").strip() if choice == "1": filter_kind = None elif choice == "2": filter_kind = EntryType.PASSWORD.value elif choice == "3": filter_kind = EntryType.TOTP.value elif choice == "4": filter_kind = EntryType.SSH.value elif choice == "5": filter_kind = EntryType.SEED.value elif choice == "6": filter_kind = EntryType.NOSTR.value elif choice == "7": filter_kind = EntryType.PGP.value elif choice == "8": filter_kind = EntryType.KEY_VALUE.value elif choice == "9": filter_kind = EntryType.MANAGED_ACCOUNT.value elif not choice: return else: print(colored("Invalid choice.", "red")) continue while True: summaries = pm.entry_manager.get_entry_summaries( filter_kind, include_archived=False ) if not summaries: break fp, parent_fp, child_fp = pm.header_fingerprint_args clear_header_with_notification( pm, fp, "Main Menu > List Entries", parent_fingerprint=parent_fp, child_fingerprint=child_fp, ) print(colored("\n[+] Entries:\n", "green")) for idx, etype, label in summaries: if filter_kind is None: display_type = etype.capitalize() print(colored(f"{idx}. {display_type} - {label}", "cyan")) else: print(colored(f"{idx}. {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(): print(colored("Invalid index.", "red")) continue pm.show_entry_details_by_index(int(idx_input)) except Exception as e: # pragma: no cover - defensive logging.error(f"Failed to list entries: {e}", exc_info=True) print(colored(f"Error: Failed to list entries: {e}", "red")) def handle_display_totp_codes(self) -> None: """Display all stored TOTP codes with a countdown progress bar.""" pm = self.manager try: fp, parent_fp, child_fp = pm.header_fingerprint_args clear_header_with_notification( pm, fp, "Main Menu > 2FA Codes", parent_fingerprint=parent_fp, child_fingerprint=child_fp, ) data = pm.entry_manager.vault.load_index() entries = data.get("entries", {}) totp_list: list[tuple[str, int, int, bool]] = [] for idx_str, entry in entries.items(): if pm._entry_type_str(entry) == EntryType.TOTP.value and not entry.get( "archived", entry.get("blacklisted", False) ): label = entry.get("label", "") period = int(entry.get("period", 30)) imported = "secret" in entry totp_list.append((label, int(idx_str), period, imported)) if not totp_list: pm.notify("No 2FA entries found.", level="WARNING") return totp_list.sort(key=lambda t: t[0].lower()) print(colored("Press Enter to return to the menu.", "cyan")) while True: fp, parent_fp, child_fp = pm.header_fingerprint_args clear_header_with_notification( pm, fp, "Main Menu > 2FA Codes", parent_fingerprint=parent_fp, child_fingerprint=child_fp, ) 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: print(colored("\nGenerated 2FA Codes:", "green")) for label, idx, period, _ in generated: code = pm.entry_manager.get_totp_code(idx, pm.parent_seed) remaining = pm.entry_manager.get_totp_time_remaining(idx) filled = int(20 * (period - remaining) / period) bar = "[" + "#" * filled + "-" * (20 - filled) + "]" if pm.secret_mode_enabled: if manager_module.copy_to_clipboard( code, pm.clipboard_clear_delay ): print( f"[{idx}] {label}: [HIDDEN] {bar} {remaining:2d}s - copied to clipboard" ) else: 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: code = pm.entry_manager.get_totp_code(idx, pm.parent_seed) remaining = pm.entry_manager.get_totp_time_remaining(idx) filled = int(20 * (period - remaining) / period) bar = "[" + "#" * filled + "-" * (20 - filled) + "]" if pm.secret_mode_enabled: if manager_module.copy_to_clipboard( code, pm.clipboard_clear_delay ): print( f"[{idx}] {label}: [HIDDEN] {bar} {remaining:2d}s - copied to clipboard" ) else: print( f"[{idx}] {label}: {color_text(code, 'imported')} {bar} {remaining:2d}s" ) sys.stdout.flush() try: user_input = timed_input("", 1) if user_input.strip() == "" or user_input.strip().lower() == "b": break except TimeoutError: pass except KeyboardInterrupt: print() break except Exception as e: # pragma: no cover - defensive logging.error(f"Error displaying TOTP codes: {e}", exc_info=True) print(colored(f"Error: Failed to display TOTP codes: {e}", "red"))