mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
fix(menu): plain numbered menu by default with aligned key columns
- Switch default from simple-term-menu (arrow-key) to plain numbered input so all keys including 10+ can be selected by typing a number; arrow-key mode is now opt-in via HATE_CRACK_ARROW_MENU=1 - Right-align key numbers within brackets so single- and multi-digit items line up: [ 1] through [99] - Use [key] format consistently in both plain and arrow-key modes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
14c6edf27a
commit
0435314043
+10
-13
@@ -1,10 +1,10 @@
|
||||
"""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.
|
||||
Default: classic numbered ``print()`` + ``input()`` selection (full number
|
||||
entry for all keys).
|
||||
|
||||
Set ``HATE_CRACK_PLAIN_MENU=1`` to force the plain numbered menu.
|
||||
Set ``HATE_CRACK_ARROW_MENU=1`` to enable arrow-key navigation via
|
||||
``simple-term-menu`` (single-digit shortcut keys only; 10+ require arrows).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -21,7 +21,7 @@ except ImportError:
|
||||
|
||||
|
||||
def _use_arrow_menu() -> bool:
|
||||
if os.environ.get("HATE_CRACK_PLAIN_MENU", "") == "1":
|
||||
if os.environ.get("HATE_CRACK_ARROW_MENU", "") != "1":
|
||||
return False
|
||||
if not _HAS_TERM_MENU:
|
||||
return False
|
||||
@@ -34,15 +34,11 @@ def _arrow_menu(
|
||||
items: list[tuple[str, str]],
|
||||
title: str | None,
|
||||
) -> str | None:
|
||||
menu_entries = [f"[{key}] {label}" for key, label in items]
|
||||
w = max(len(key) for key, _ in items)
|
||||
menu_entries = [f"[{key:>{w}}] {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",),
|
||||
)
|
||||
menu = TerminalMenu(menu_entries, title=title)
|
||||
idx = menu.show()
|
||||
if idx is None:
|
||||
return None
|
||||
@@ -53,8 +49,9 @@ def _numbered_menu(
|
||||
items: list[tuple[str, str]],
|
||||
prompt: str,
|
||||
) -> str | None:
|
||||
w = max(len(key) for key, _ in items)
|
||||
for key, label in items:
|
||||
print(f"\t({key}) {label}")
|
||||
print(f"\t[{key:>{w}}] {label}")
|
||||
choice = input(prompt).strip()
|
||||
if not choice:
|
||||
return None
|
||||
|
||||
+10
-8
@@ -19,29 +19,30 @@ class TestUseArrowMenu:
|
||||
import hate_crack.menu as mod
|
||||
|
||||
monkeypatch.setattr(mod, "_HAS_TERM_MENU", False)
|
||||
monkeypatch.delenv("HATE_CRACK_PLAIN_MENU", raising=False)
|
||||
monkeypatch.setenv("HATE_CRACK_ARROW_MENU", "1")
|
||||
assert _use_arrow_menu() is False
|
||||
|
||||
def test_falls_back_on_non_tty(self, monkeypatch):
|
||||
import hate_crack.menu as mod
|
||||
|
||||
monkeypatch.setattr(mod, "_HAS_TERM_MENU", True)
|
||||
monkeypatch.delenv("HATE_CRACK_PLAIN_MENU", raising=False)
|
||||
monkeypatch.setenv("HATE_CRACK_ARROW_MENU", "1")
|
||||
monkeypatch.setattr("sys.stdout.isatty", lambda: False)
|
||||
assert _use_arrow_menu() is False
|
||||
|
||||
def test_falls_back_with_env_var(self, monkeypatch):
|
||||
def test_falls_back_without_env_var(self, monkeypatch):
|
||||
import hate_crack.menu as mod
|
||||
|
||||
monkeypatch.setattr(mod, "_HAS_TERM_MENU", True)
|
||||
monkeypatch.setenv("HATE_CRACK_PLAIN_MENU", "1")
|
||||
monkeypatch.delenv("HATE_CRACK_ARROW_MENU", raising=False)
|
||||
monkeypatch.setattr("sys.stdout.isatty", lambda: True)
|
||||
assert _use_arrow_menu() is False
|
||||
|
||||
def test_enabled_when_all_conditions_met(self, monkeypatch):
|
||||
import hate_crack.menu as mod
|
||||
|
||||
monkeypatch.setattr(mod, "_HAS_TERM_MENU", True)
|
||||
monkeypatch.delenv("HATE_CRACK_PLAIN_MENU", raising=False)
|
||||
monkeypatch.setenv("HATE_CRACK_ARROW_MENU", "1")
|
||||
monkeypatch.setattr("sys.stdout.isatty", lambda: True)
|
||||
assert _use_arrow_menu() is True
|
||||
|
||||
@@ -56,8 +57,9 @@ class TestNumberedMenu:
|
||||
monkeypatch.setattr("builtins.input", lambda _: "1")
|
||||
_numbered_menu(SAMPLE_ITEMS, "\nSelect: ")
|
||||
captured = capsys.readouterr().out
|
||||
w = max(len(key) for key, _ in SAMPLE_ITEMS)
|
||||
for key, label in SAMPLE_ITEMS:
|
||||
assert f"({key}) {label}" in captured
|
||||
assert f"[{key:>{w}}] {label}" in captured
|
||||
|
||||
def test_returns_none_on_empty_input(self, monkeypatch):
|
||||
monkeypatch.setattr("builtins.input", lambda _: "")
|
||||
@@ -94,7 +96,7 @@ class TestInteractiveMenu:
|
||||
import hate_crack.menu as mod
|
||||
|
||||
monkeypatch.setattr(mod, "_HAS_TERM_MENU", False)
|
||||
monkeypatch.delenv("HATE_CRACK_PLAIN_MENU", raising=False)
|
||||
monkeypatch.delenv("HATE_CRACK_ARROW_MENU", raising=False)
|
||||
monkeypatch.setattr("builtins.input", lambda _: "99")
|
||||
result = interactive_menu(SAMPLE_ITEMS)
|
||||
assert result == "99"
|
||||
@@ -103,7 +105,7 @@ class TestInteractiveMenu:
|
||||
import hate_crack.menu as mod
|
||||
|
||||
monkeypatch.setattr(mod, "_HAS_TERM_MENU", True)
|
||||
monkeypatch.delenv("HATE_CRACK_PLAIN_MENU", raising=False)
|
||||
monkeypatch.setenv("HATE_CRACK_ARROW_MENU", "1")
|
||||
monkeypatch.setattr("sys.stdout.isatty", lambda: True)
|
||||
mock_menu_instance = MagicMock()
|
||||
mock_menu_instance.show.return_value = 0
|
||||
|
||||
Reference in New Issue
Block a user