mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-03 11:22:03 +00:00
Refactor entry menus to use generic MenuHandler.run_menu
Consolidated duplicated menu loop logic from `_entry_actions_menu`, `_entry_edit_menu`, and `_entry_qr_menu` in `src/seedpass/core/manager.py` into a reusable `run_menu` method in `src/seedpass/core/menu_handler.py`. This improves maintainability by centralizing the loop handling, input prompting, and header clearing logic. * Added `run_menu` to `MenuHandler`. * Refactored `_entry_actions_menu` to use `run_menu`. * Refactored `_entry_edit_menu` to use `run_menu`. * Refactored `_entry_qr_menu` (NOSTR section) to use `run_menu`. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
ece0dda09f
commit
a1b459f98f
2 files changed
+254
-182
No files matched your search
+211
-182
@@ -2782,174 +2782,204 @@ class PasswordManager:
|
|||||||
|
|
||||||
def _entry_actions_menu(self, index: int, entry: dict) -> None:
|
def _entry_actions_menu(self, index: int, entry: dict) -> None:
|
||||||
"""Provide actions for a retrieved entry."""
|
"""Provide actions for a retrieved entry."""
|
||||||
while True:
|
|
||||||
fp, parent_fp, child_fp = self.header_fingerprint_args
|
# Keep entry dict updated in closure scope
|
||||||
clear_header_with_notification(
|
current_entry = entry
|
||||||
self,
|
|
||||||
fp,
|
def refresh_entry():
|
||||||
"Entry Actions",
|
nonlocal current_entry
|
||||||
parent_fingerprint=parent_fp,
|
refreshed = self.entry_manager.retrieve_entry(index)
|
||||||
child_fingerprint=child_fp,
|
if refreshed:
|
||||||
|
current_entry = refreshed
|
||||||
|
|
||||||
|
def toggle_archive():
|
||||||
|
archived = current_entry.get(
|
||||||
|
"archived", current_entry.get("blacklisted", False)
|
||||||
)
|
)
|
||||||
archived = entry.get("archived", entry.get("blacklisted", False))
|
|
||||||
entry_type = self._entry_type_str(entry)
|
|
||||||
print(colored("\n[+] Entry Actions:", "green"))
|
|
||||||
if archived:
|
if archived:
|
||||||
print(colored("U. Unarchive", "cyan"))
|
self.entry_manager.restore_entry(index)
|
||||||
else:
|
else:
|
||||||
print(colored("A. Archive", "cyan"))
|
self.entry_manager.archive_entry(index)
|
||||||
print(colored("N. Add Note", "cyan"))
|
self.is_dirty = True
|
||||||
print(colored("C. Add Custom Field", "cyan"))
|
self.last_update = time.time()
|
||||||
print(colored("H. Add Hidden Field", "cyan"))
|
refresh_entry()
|
||||||
print(colored("E. Edit", "cyan"))
|
|
||||||
print(colored("T. Edit Tags", "cyan"))
|
def add_note():
|
||||||
|
note = input("Enter note: ").strip()
|
||||||
|
if note:
|
||||||
|
notes = current_entry.get("notes", "")
|
||||||
|
notes = f"{notes}\n{note}" if notes else note
|
||||||
|
self.entry_manager.modify_entry(index, notes=notes)
|
||||||
|
self.is_dirty = True
|
||||||
|
self.last_update = time.time()
|
||||||
|
refresh_entry()
|
||||||
|
|
||||||
|
def add_custom_field(hidden: bool):
|
||||||
|
label = input(" Field label: ").strip()
|
||||||
|
if not label:
|
||||||
|
print(colored("Field label cannot be empty.", "red"))
|
||||||
|
else:
|
||||||
|
value = input(" Field value: ").strip()
|
||||||
|
custom_fields = current_entry.get("custom_fields", [])
|
||||||
|
custom_fields.append(
|
||||||
|
{"label": label, "value": value, "is_hidden": hidden}
|
||||||
|
)
|
||||||
|
self.entry_manager.modify_entry(index, custom_fields=custom_fields)
|
||||||
|
self.is_dirty = True
|
||||||
|
self.last_update = time.time()
|
||||||
|
refresh_entry()
|
||||||
|
|
||||||
|
def edit_tags():
|
||||||
|
current_tags = current_entry.get("tags", [])
|
||||||
|
print(
|
||||||
|
colored(
|
||||||
|
f"Current tags: {', '.join(current_tags) if current_tags else 'None'}",
|
||||||
|
"cyan",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tags_input = input(
|
||||||
|
"Enter tags (comma-separated, leave blank to remove all tags): "
|
||||||
|
).strip()
|
||||||
|
tags = (
|
||||||
|
[t.strip() for t in tags_input.split(",") if t.strip()]
|
||||||
|
if tags_input
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
self.entry_manager.modify_entry(index, tags=tags)
|
||||||
|
self.is_dirty = True
|
||||||
|
self.last_update = time.time()
|
||||||
|
refresh_entry()
|
||||||
|
|
||||||
|
def edit_entry():
|
||||||
|
self._entry_edit_menu(index, current_entry)
|
||||||
|
refresh_entry()
|
||||||
|
|
||||||
|
def show_qr():
|
||||||
|
self._entry_qr_menu(index, current_entry)
|
||||||
|
pause()
|
||||||
|
refresh_entry()
|
||||||
|
|
||||||
|
def get_options():
|
||||||
|
archived = current_entry.get(
|
||||||
|
"archived", current_entry.get("blacklisted", False)
|
||||||
|
)
|
||||||
|
entry_type = self._entry_type_str(current_entry)
|
||||||
|
options = []
|
||||||
|
|
||||||
|
if archived:
|
||||||
|
options.append(("u", "Unarchive", toggle_archive))
|
||||||
|
else:
|
||||||
|
options.append(("a", "Archive", toggle_archive))
|
||||||
|
|
||||||
|
options.append(("n", "Add Note", add_note))
|
||||||
|
options.append(("c", "Add Custom Field", lambda: add_custom_field(False)))
|
||||||
|
options.append(("h", "Add Hidden Field", lambda: add_custom_field(True)))
|
||||||
|
options.append(("e", "Edit", edit_entry))
|
||||||
|
options.append(("t", "Edit Tags", edit_tags))
|
||||||
|
|
||||||
if entry_type in {
|
if entry_type in {
|
||||||
EntryType.SEED.value,
|
EntryType.SEED.value,
|
||||||
EntryType.MANAGED_ACCOUNT.value,
|
EntryType.MANAGED_ACCOUNT.value,
|
||||||
EntryType.NOSTR.value,
|
EntryType.NOSTR.value,
|
||||||
}:
|
}:
|
||||||
print(colored("Q. Show QR codes", "cyan"))
|
options.append(("q", "Show QR codes", show_qr))
|
||||||
|
|
||||||
choice = (
|
return options
|
||||||
input("Select an action or press Enter to return: ").strip().lower()
|
|
||||||
)
|
self.menu_handler.run_menu("Entry Actions", get_options)
|
||||||
if not choice:
|
|
||||||
break
|
|
||||||
if choice == "a" and not archived:
|
|
||||||
self.entry_manager.archive_entry(index)
|
|
||||||
self.is_dirty = True
|
|
||||||
self.last_update = time.time()
|
|
||||||
elif choice == "u" and archived:
|
|
||||||
self.entry_manager.restore_entry(index)
|
|
||||||
self.is_dirty = True
|
|
||||||
self.last_update = time.time()
|
|
||||||
elif choice == "n":
|
|
||||||
note = input("Enter note: ").strip()
|
|
||||||
if note:
|
|
||||||
notes = entry.get("notes", "")
|
|
||||||
notes = f"{notes}\n{note}" if notes else note
|
|
||||||
self.entry_manager.modify_entry(index, notes=notes)
|
|
||||||
self.is_dirty = True
|
|
||||||
self.last_update = time.time()
|
|
||||||
elif choice in {"c", "h"}:
|
|
||||||
label = input(" Field label: ").strip()
|
|
||||||
if not label:
|
|
||||||
print(colored("Field label cannot be empty.", "red"))
|
|
||||||
else:
|
|
||||||
value = input(" Field value: ").strip()
|
|
||||||
hidden = choice == "h"
|
|
||||||
custom_fields = entry.get("custom_fields", [])
|
|
||||||
custom_fields.append(
|
|
||||||
{"label": label, "value": value, "is_hidden": hidden}
|
|
||||||
)
|
|
||||||
self.entry_manager.modify_entry(index, custom_fields=custom_fields)
|
|
||||||
self.is_dirty = True
|
|
||||||
self.last_update = time.time()
|
|
||||||
elif choice == "t":
|
|
||||||
current_tags = entry.get("tags", [])
|
|
||||||
print(
|
|
||||||
colored(
|
|
||||||
f"Current tags: {', '.join(current_tags) if current_tags else 'None'}",
|
|
||||||
"cyan",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
tags_input = input(
|
|
||||||
"Enter tags (comma-separated, leave blank to remove all tags): "
|
|
||||||
).strip()
|
|
||||||
tags = (
|
|
||||||
[t.strip() for t in tags_input.split(",") if t.strip()]
|
|
||||||
if tags_input
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
self.entry_manager.modify_entry(index, tags=tags)
|
|
||||||
self.is_dirty = True
|
|
||||||
self.last_update = time.time()
|
|
||||||
elif choice == "e":
|
|
||||||
self._entry_edit_menu(index, entry)
|
|
||||||
elif choice == "q":
|
|
||||||
self._entry_qr_menu(index, entry)
|
|
||||||
pause()
|
|
||||||
else:
|
|
||||||
print(colored("Invalid choice.", "red"))
|
|
||||||
entry = self.entry_manager.retrieve_entry(index) or entry
|
|
||||||
|
|
||||||
def _entry_edit_menu(self, index: int, entry: dict) -> None:
|
def _entry_edit_menu(self, index: int, entry: dict) -> None:
|
||||||
"""Sub-menu for editing common entry fields."""
|
"""Sub-menu for editing common entry fields."""
|
||||||
entry_type = self._entry_type_str(entry)
|
|
||||||
while True:
|
# Keep entry dict updated in closure scope
|
||||||
fp, parent_fp, child_fp = self.header_fingerprint_args
|
current_entry = entry
|
||||||
clear_header_with_notification(
|
|
||||||
self,
|
def refresh_entry():
|
||||||
fp,
|
nonlocal current_entry
|
||||||
"Edit Entry",
|
refreshed = self.entry_manager.retrieve_entry(index)
|
||||||
parent_fingerprint=parent_fp,
|
if refreshed:
|
||||||
child_fingerprint=child_fp,
|
current_entry = refreshed
|
||||||
)
|
|
||||||
print(colored("\n[+] Edit Menu:", "green"))
|
def edit_label():
|
||||||
print(colored("L. Edit Label", "cyan"))
|
new_label = input("New label: ").strip()
|
||||||
if entry_type == EntryType.KEY_VALUE.value:
|
if new_label:
|
||||||
print(colored("K. Edit Key", "cyan"))
|
self.entry_manager.modify_entry(index, label=new_label)
|
||||||
print(
|
|
||||||
colored("V. Edit Value", "cyan")
|
|
||||||
) # 🔧 merged conflicting changes from feature-X vs main
|
|
||||||
if entry_type == EntryType.PASSWORD.value:
|
|
||||||
print(colored("U. Edit Username", "cyan"))
|
|
||||||
print(colored("R. Edit URL", "cyan"))
|
|
||||||
elif entry_type == EntryType.TOTP.value:
|
|
||||||
print(colored("P. Edit Period", "cyan"))
|
|
||||||
print(colored("D. Edit Digits", "cyan"))
|
|
||||||
choice = input("Select option or press Enter to go back: ").strip().lower()
|
|
||||||
if not choice:
|
|
||||||
break
|
|
||||||
if choice == "l":
|
|
||||||
new_label = input("New label: ").strip()
|
|
||||||
if new_label:
|
|
||||||
self.entry_manager.modify_entry(index, label=new_label)
|
|
||||||
self.is_dirty = True
|
|
||||||
self.last_update = time.time()
|
|
||||||
elif entry_type == EntryType.KEY_VALUE.value and choice == "k":
|
|
||||||
new_key = input("New key: ").strip()
|
|
||||||
if new_key:
|
|
||||||
self.entry_manager.modify_entry(index, key=new_key)
|
|
||||||
self.is_dirty = True
|
|
||||||
self.last_update = time.time()
|
|
||||||
elif entry_type == EntryType.KEY_VALUE.value and choice == "v":
|
|
||||||
new_value = input("New value: ").strip()
|
|
||||||
if new_value:
|
|
||||||
self.entry_manager.modify_entry(index, value=new_value)
|
|
||||||
self.is_dirty = True
|
|
||||||
self.last_update = (
|
|
||||||
time.time()
|
|
||||||
) # 🔧 merged conflicting changes from feature-X vs main
|
|
||||||
elif entry_type == EntryType.PASSWORD.value and choice == "u":
|
|
||||||
new_username = input("New username: ").strip()
|
|
||||||
self.entry_manager.modify_entry(index, username=new_username)
|
|
||||||
self.is_dirty = True
|
self.is_dirty = True
|
||||||
self.last_update = time.time()
|
self.last_update = time.time()
|
||||||
elif entry_type == EntryType.PASSWORD.value and choice == "r":
|
refresh_entry()
|
||||||
new_url = input("New URL: ").strip()
|
|
||||||
self.entry_manager.modify_entry(index, url=new_url)
|
def edit_key():
|
||||||
|
new_key = input("New key: ").strip()
|
||||||
|
if new_key:
|
||||||
|
self.entry_manager.modify_entry(index, key=new_key)
|
||||||
self.is_dirty = True
|
self.is_dirty = True
|
||||||
self.last_update = time.time()
|
self.last_update = time.time()
|
||||||
elif entry_type == EntryType.TOTP.value and choice == "p":
|
refresh_entry()
|
||||||
period_str = input("New period (seconds): ").strip()
|
|
||||||
if period_str.isdigit():
|
def edit_value():
|
||||||
self.entry_manager.modify_entry(index, period=int(period_str))
|
new_value = input("New value: ").strip()
|
||||||
self.is_dirty = True
|
if new_value:
|
||||||
self.last_update = time.time()
|
self.entry_manager.modify_entry(index, value=new_value)
|
||||||
else:
|
self.is_dirty = True
|
||||||
print(colored("Invalid period value.", "red"))
|
self.last_update = time.time()
|
||||||
elif entry_type == EntryType.TOTP.value and choice == "d":
|
refresh_entry()
|
||||||
digits_str = input("New digits: ").strip()
|
|
||||||
if digits_str.isdigit():
|
def edit_username():
|
||||||
self.entry_manager.modify_entry(index, digits=int(digits_str))
|
new_username = input("New username: ").strip()
|
||||||
self.is_dirty = True
|
self.entry_manager.modify_entry(index, username=new_username)
|
||||||
self.last_update = time.time()
|
self.is_dirty = True
|
||||||
else:
|
self.last_update = time.time()
|
||||||
print(colored("Invalid digits value.", "red"))
|
refresh_entry()
|
||||||
|
|
||||||
|
def edit_url():
|
||||||
|
new_url = input("New URL: ").strip()
|
||||||
|
self.entry_manager.modify_entry(index, url=new_url)
|
||||||
|
self.is_dirty = True
|
||||||
|
self.last_update = time.time()
|
||||||
|
refresh_entry()
|
||||||
|
|
||||||
|
def edit_period():
|
||||||
|
period_str = input("New period (seconds): ").strip()
|
||||||
|
if period_str.isdigit():
|
||||||
|
self.entry_manager.modify_entry(index, period=int(period_str))
|
||||||
|
self.is_dirty = True
|
||||||
|
self.last_update = time.time()
|
||||||
|
refresh_entry()
|
||||||
else:
|
else:
|
||||||
print(colored("Invalid choice.", "red"))
|
print(colored("Invalid period value.", "red"))
|
||||||
entry = self.entry_manager.retrieve_entry(index) or entry
|
|
||||||
|
def edit_digits():
|
||||||
|
digits_str = input("New digits: ").strip()
|
||||||
|
if digits_str.isdigit():
|
||||||
|
self.entry_manager.modify_entry(index, digits=int(digits_str))
|
||||||
|
self.is_dirty = True
|
||||||
|
self.last_update = time.time()
|
||||||
|
refresh_entry()
|
||||||
|
else:
|
||||||
|
print(colored("Invalid digits value.", "red"))
|
||||||
|
|
||||||
|
def get_options():
|
||||||
|
entry_type = self._entry_type_str(current_entry)
|
||||||
|
options = [("l", "Edit Label", edit_label)]
|
||||||
|
|
||||||
|
if entry_type == EntryType.KEY_VALUE.value:
|
||||||
|
options.append(("k", "Edit Key", edit_key))
|
||||||
|
options.append(("v", "Edit Value", edit_value))
|
||||||
|
|
||||||
|
if entry_type == EntryType.PASSWORD.value:
|
||||||
|
options.append(("u", "Edit Username", edit_username))
|
||||||
|
options.append(("r", "Edit URL", edit_url))
|
||||||
|
elif entry_type == EntryType.TOTP.value:
|
||||||
|
options.append(("p", "Edit Period", edit_period))
|
||||||
|
options.append(("d", "Edit Digits", edit_digits))
|
||||||
|
|
||||||
|
return options
|
||||||
|
|
||||||
|
self.menu_handler.run_menu(
|
||||||
|
"Edit Entry",
|
||||||
|
get_options,
|
||||||
|
prompt="Select option or press Enter to go back: "
|
||||||
|
)
|
||||||
|
|
||||||
def _entry_qr_menu(self, index: int, entry: dict) -> None:
|
def _entry_qr_menu(self, index: int, entry: dict) -> None:
|
||||||
"""Display QR codes for the given ``entry``."""
|
"""Display QR codes for the given ``entry``."""
|
||||||
@@ -2973,40 +3003,39 @@ class PasswordManager:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if entry_type == EntryType.NOSTR.value:
|
if entry_type == EntryType.NOSTR.value:
|
||||||
while True:
|
current_entry = entry
|
||||||
fp, parent_fp, child_fp = self.header_fingerprint_args
|
|
||||||
clear_header_with_notification(
|
|
||||||
self,
|
|
||||||
fp,
|
|
||||||
"QR Codes",
|
|
||||||
parent_fingerprint=parent_fp,
|
|
||||||
child_fingerprint=child_fp,
|
|
||||||
)
|
|
||||||
print(colored("\n[+] QR Codes:", "green"))
|
|
||||||
print(colored("P. Public key", "cyan"))
|
|
||||||
print(colored("K. Private key", "cyan"))
|
|
||||||
choice = (
|
|
||||||
input("Select option or press Enter to return: ")
|
|
||||||
.strip()
|
|
||||||
.lower()
|
|
||||||
)
|
|
||||||
if not choice:
|
|
||||||
break
|
|
||||||
|
|
||||||
npub, nsec = self.entry_manager.get_nostr_key_pair(
|
def refresh_entry():
|
||||||
index, self.parent_seed
|
nonlocal current_entry
|
||||||
)
|
refreshed = self.entry_manager.retrieve_entry(index)
|
||||||
|
if refreshed:
|
||||||
|
current_entry = refreshed
|
||||||
|
|
||||||
if choice == "p":
|
def show_public_key():
|
||||||
print(colored(f"npub: {npub}", "cyan"))
|
npub, _ = self.entry_manager.get_nostr_key_pair(index, self.parent_seed)
|
||||||
TotpManager.print_qr_code(f"nostr:{npub}")
|
print(colored(f"npub: {npub}", "cyan"))
|
||||||
elif choice == "k":
|
TotpManager.print_qr_code(f"nostr:{npub}")
|
||||||
print(color_text(f"nsec: {nsec}", "deterministic"))
|
|
||||||
TotpManager.print_qr_code(nsec)
|
|
||||||
else:
|
|
||||||
print(colored("Invalid choice.", "red"))
|
|
||||||
pause()
|
pause()
|
||||||
entry = self.entry_manager.retrieve_entry(index) or entry
|
refresh_entry()
|
||||||
|
|
||||||
|
def show_private_key():
|
||||||
|
_, nsec = self.entry_manager.get_nostr_key_pair(index, self.parent_seed)
|
||||||
|
print(color_text(f"nsec: {nsec}", "deterministic"))
|
||||||
|
TotpManager.print_qr_code(nsec)
|
||||||
|
pause()
|
||||||
|
refresh_entry()
|
||||||
|
|
||||||
|
def get_options():
|
||||||
|
return [
|
||||||
|
("p", "Public key", show_public_key),
|
||||||
|
("k", "Private key", show_private_key),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.menu_handler.run_menu(
|
||||||
|
"QR Codes",
|
||||||
|
get_options,
|
||||||
|
prompt="Select option or press Enter to return: "
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.notify("No QR codes available for this entry.", level="WARNING")
|
self.notify("No QR codes available for this entry.", level="WARNING")
|
||||||
|
|||||||
@@ -99,6 +99,49 @@ class MenuHandler:
|
|||||||
logging.error(f"Failed to list entries: {e}", exc_info=True)
|
logging.error(f"Failed to list entries: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to list entries: {e}", "red"))
|
print(colored(f"Error: Failed to list entries: {e}", "red"))
|
||||||
|
|
||||||
|
@pause_logging_for_ui
|
||||||
|
def run_menu(
|
||||||
|
self,
|
||||||
|
title: str,
|
||||||
|
options_generator: Callable[[], list[tuple[str, str, Callable[[], Any]]]],
|
||||||
|
prompt: str = "Select an action or press Enter to return: ",
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Generic menu loop handler.
|
||||||
|
|
||||||
|
:param title: The title of the menu to display in the header.
|
||||||
|
:param options_generator: A function that returns a list of tuples (key, label, action).
|
||||||
|
It is called at the start of each loop iteration to allow dynamic options.
|
||||||
|
:param prompt: The input prompt to display.
|
||||||
|
"""
|
||||||
|
pm = self.manager
|
||||||
|
while True:
|
||||||
|
fp, parent_fp, child_fp = pm.header_fingerprint_args
|
||||||
|
clear_header_with_notification(
|
||||||
|
pm,
|
||||||
|
fp,
|
||||||
|
title,
|
||||||
|
parent_fingerprint=parent_fp,
|
||||||
|
child_fingerprint=child_fp,
|
||||||
|
)
|
||||||
|
|
||||||
|
options = options_generator()
|
||||||
|
print(colored(f"\n[+] {title}:", "green"))
|
||||||
|
|
||||||
|
key_map = {}
|
||||||
|
for key, label, action in options:
|
||||||
|
print(colored(f"{key.upper()}. {label}", "cyan"))
|
||||||
|
key_map[key.lower()] = action
|
||||||
|
|
||||||
|
choice = input(prompt).strip().lower()
|
||||||
|
if not choice:
|
||||||
|
break
|
||||||
|
|
||||||
|
if choice in key_map:
|
||||||
|
key_map[choice]()
|
||||||
|
else:
|
||||||
|
print(colored("Invalid choice.", "red"))
|
||||||
|
|
||||||
@pause_logging_for_ui
|
@pause_logging_for_ui
|
||||||
def handle_display_totp_codes(self) -> None:
|
def handle_display_totp_codes(self) -> None:
|
||||||
"""Display all stored TOTP codes with a countdown progress bar."""
|
"""Display all stored TOTP codes with a countdown progress bar."""
|
||||||
|
|||||||
Reference in new issue
Block a user