From 24e574bc8e487c28c6f30c95ebd06b0692aaf576 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Fri, 24 Jul 2026 18:33:11 -0400 Subject: [PATCH] fix(ui): print picker grids once and report rejected menu keys The re-prompt loops repainted the whole multicolumn wordlist grid after every typo, burying the error message under dozens of entries. Hoist the grid and the p/q legend out of the loop so only the prompt repeats. ollama_attack's loop also fell through to a silent redraw on an unrecognised key, leaving no way to tell a rejected key from a repainted prompt. Co-Authored-By: Claude --- hate_crack/attacks.py | 58 ++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/hate_crack/attacks.py b/hate_crack/attacks.py index ac4560c..4288d94 100644 --- a/hate_crack/attacks.py +++ b/hate_crack/attacks.py @@ -549,23 +549,30 @@ def ollama_attack(ctx: Any) -> None: elif choice == "3" and has_cracked: ctx.hcatOllama(ctx.hcatHashType, ctx.hcatHashFile, "cracked", out_path) return + else: + # Without this the menu just silently redraws and the user cannot + # tell a rejected key from a repainted prompt. + print("\t[!] Invalid selection.") def _omen_pick_training_wordlist(ctx: Any, title: str = "Training Wordlists"): """Show wordlist picker. Returns path or None (user cancelled with 'q').""" wordlist_files = ctx.list_wordlist_files(ctx.hcatWordlists) + # Print the grid once, outside the retry loop: a wordlists directory can + # hold dozens of entries, and repainting the whole thing after every typo + # buries the error message. + if wordlist_files: + entries = [f"{i}) {f}" for i, f in enumerate(wordlist_files, start=1)] + max_len = max((len(e) for e in entries), default=24) + print_multicolumn_list( + title, + entries, + min_col_width=max_len, + max_col_width=max_len, + ) + print("\tp. Enter a custom path") + print("\tq. Cancel") while True: - if wordlist_files: - entries = [f"{i}) {f}" for i, f in enumerate(wordlist_files, start=1)] - max_len = max((len(e) for e in entries), default=24) - print_multicolumn_list( - title, - entries, - min_col_width=max_len, - max_col_width=max_len, - ) - print("\tp. Enter a custom path") - print("\tq. Cancel") sel = input("\n\tSelect wordlist: ").strip() if sel.lower() == "q": return None @@ -645,21 +652,22 @@ def _markov_pick_training_source(ctx: Any): has_cracked = os.path.isfile(out_path) and os.path.getsize(out_path) > 0 wordlist_files = ctx.list_wordlist_files(ctx.hcatWordlists) + # Print the grid once, outside the retry loop — see _omen_pick_training_wordlist. + entries = [] + if has_cracked: + entries.append("0) Cracked passwords (current session)") + entries.extend([f"{i}) {f}" for i, f in enumerate(wordlist_files, start=1)]) + if entries: + max_len = max((len(e) for e in entries), default=24) + print_multicolumn_list( + "Markov Training Source", + entries, + min_col_width=max_len, + max_col_width=max_len, + ) + print("\tp. Enter a custom path") + print("\tq. Cancel") while True: - entries = [] - if has_cracked: - entries.append("0) Cracked passwords (current session)") - entries.extend([f"{i}) {f}" for i, f in enumerate(wordlist_files, start=1)]) - if entries: - max_len = max((len(e) for e in entries), default=24) - print_multicolumn_list( - "Markov Training Source", - entries, - min_col_width=max_len, - max_col_width=max_len, - ) - print("\tp. Enter a custom path") - print("\tq. Cancel") sel = input("\n\tSelect training source: ").strip() if sel.lower() == "q": return None