From df9983f14b79c4ec9ccfc6b4bdb7c72d06988a98 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Mon, 16 Mar 2026 11:38:05 -0400 Subject: [PATCH] fix: add missing menu.py module to tracked files hate_crack/menu.py was imported by main.py but never committed, causing ModuleNotFoundError on fresh installs. --- hate_crack/menu.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 hate_crack/menu.py diff --git a/hate_crack/menu.py b/hate_crack/menu.py new file mode 100644 index 0000000..2f5504c --- /dev/null +++ b/hate_crack/menu.py @@ -0,0 +1,89 @@ +"""Reusable interactive menu with optional arrow-key navigation. + +When ``simple-term-menu`` is installed AND stdout is a TTY, renders an +arrow-key navigable menu. Otherwise falls back to classic numbered +``print()`` + ``input()`` selection. + +Set ``HATE_CRACK_PLAIN_MENU=1`` to force the plain numbered menu. +""" + +from __future__ import annotations + +import os +import sys + +try: + from simple_term_menu import TerminalMenu + + _HAS_TERM_MENU = True +except ImportError: + _HAS_TERM_MENU = False + + +def _use_arrow_menu() -> bool: + if os.environ.get("HATE_CRACK_PLAIN_MENU", "") == "1": + return False + if not _HAS_TERM_MENU: + return False + if not sys.stdout.isatty(): + return False + return True + + +def _arrow_menu( + items: list[tuple[str, str]], + title: str | None, +) -> str | None: + menu_entries = [f"[{key}] {label}" for key, label in items] + shortcuts = [key for key, _ in items] + + # Build shortcut_key_highlight_style so pressing a number jumps there + menu = TerminalMenu( + menu_entries, + title=title, + shortcut_key_highlight_style=("standout",), + ) + idx = menu.show() + if idx is None: + return None + return shortcuts[idx] + + +def _numbered_menu( + items: list[tuple[str, str]], + prompt: str, +) -> str | None: + for key, label in items: + print(f"\t({key}) {label}") + choice = input(prompt).strip() + if not choice: + return None + return choice + + +def interactive_menu( + items: list[tuple[str, str]], + title: str | None = None, + prompt: str = "\nSelect a task: ", +) -> str | None: + """Display *items* as a menu and return the selected key string. + + Parameters + ---------- + items: + Ordered ``(key, label)`` pairs. *key* is the string returned on + selection (e.g. ``"1"``, ``"99"``). + title: + Optional heading printed above the menu (arrow-key mode) or + ignored (numbered mode, where callers print their own headers). + prompt: + Input prompt shown in numbered-menu mode. + + Returns + ------- + str | None + The *key* of the selected item, or ``None`` on Escape / cancel. + """ + if _use_arrow_menu(): + return _arrow_menu(items, title) + return _numbered_menu(items, prompt)