From 59f0052c0ee34b9ad7feb18125d3891a4fa238cf Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Fri, 20 Mar 2026 10:30:08 -0400 Subject: [PATCH] feat: add tab autocomplete to wordlist menu file path prompts Replace input() with ctx.select_file_with_autocomplete() for all file and directory path prompts in the 7 wordlist tools submenu functions. Non-path prompts (lengths, masks, offsets, mode selection) remain as plain input() calls. Update tests to set ctx.select_file_with_autocomplete.side_effect for file path values and leave builtins.input patches only for non-path inputs. --- hate_crack/attacks.py | 56 ++++++++++++++++-------- tests/test_wordlist_tools.py | 84 ++++++++++++++++++++---------------- 2 files changed, 86 insertions(+), 54 deletions(-) diff --git a/hate_crack/attacks.py b/hate_crack/attacks.py index c5c8ebd..cb7100a 100644 --- a/hate_crack/attacks.py +++ b/hate_crack/attacks.py @@ -951,11 +951,13 @@ def rule_tools_submenu(ctx: Any) -> None: def wordlist_filter_length(ctx: Any) -> None: """Prompt for paths and lengths, then filter wordlist by word length.""" - infile = input("\n[*] Enter path to input wordlist: ").strip() + infile = ctx.select_file_with_autocomplete( + "\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists + ).strip() if not os.path.isfile(infile): print(f"[!] File not found: {infile}") return - outfile = input("[*] Enter path to output wordlist: ").strip() + outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip() if not outfile: print("[!] Output path cannot be empty.") return @@ -969,11 +971,13 @@ def wordlist_filter_length(ctx: Any) -> None: def wordlist_filter_charclass_include(ctx: Any) -> None: """Prompt for paths and mask, then keep only words matching required char classes.""" - infile = input("\n[*] Enter path to input wordlist: ").strip() + infile = ctx.select_file_with_autocomplete( + "\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists + ).strip() if not os.path.isfile(infile): print(f"[!] File not found: {infile}") return - outfile = input("[*] Enter path to output wordlist: ").strip() + outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip() if not outfile: print("[!] Output path cannot be empty.") return @@ -987,11 +991,13 @@ def wordlist_filter_charclass_include(ctx: Any) -> None: def wordlist_filter_charclass_exclude(ctx: Any) -> None: """Prompt for paths and mask, then remove words containing excluded char classes.""" - infile = input("\n[*] Enter path to input wordlist: ").strip() + infile = ctx.select_file_with_autocomplete( + "\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists + ).strip() if not os.path.isfile(infile): print(f"[!] File not found: {infile}") return - outfile = input("[*] Enter path to output wordlist: ").strip() + outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip() if not outfile: print("[!] Output path cannot be empty.") return @@ -1005,11 +1011,13 @@ def wordlist_filter_charclass_exclude(ctx: Any) -> None: def wordlist_cut_substring(ctx: Any) -> None: """Prompt for paths, offset, and optional length, then extract substring from each word.""" - infile = input("\n[*] Enter path to input wordlist: ").strip() + infile = ctx.select_file_with_autocomplete( + "\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists + ).strip() if not os.path.isfile(infile): print(f"[!] File not found: {infile}") return - outfile = input("[*] Enter path to output wordlist: ").strip() + outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip() if not outfile: print("[!] Output path cannot be empty.") return @@ -1024,11 +1032,13 @@ def wordlist_cut_substring(ctx: Any) -> None: def wordlist_split_by_length(ctx: Any) -> None: """Prompt for input wordlist and output directory, then split by word length.""" - infile = input("\n[*] Enter path to input wordlist: ").strip() + infile = ctx.select_file_with_autocomplete( + "\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists + ).strip() if not os.path.isfile(infile): print(f"[!] File not found: {infile}") return - outdir = input("[*] Enter output directory path: ").strip() + outdir = ctx.select_file_with_autocomplete("[*] Enter output directory path").strip() if not outdir: print("[!] Output directory cannot be empty.") return @@ -1047,15 +1057,19 @@ def wordlist_subtract_words(ctx: Any) -> None: mode = input("[*] Choose mode (1/2): ").strip() if mode == "1": - infile = input("[*] Enter path to input wordlist: ").strip() + infile = ctx.select_file_with_autocomplete( + "[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists + ).strip() if not os.path.isfile(infile): print(f"[!] File not found: {infile}") return - remove_file = input("[*] Enter path to wordlist to subtract: ").strip() + remove_file = ctx.select_file_with_autocomplete( + "[*] Enter path to wordlist to subtract", base_dir=ctx.hcatWordlists + ).strip() if not os.path.isfile(remove_file): print(f"[!] File not found: {remove_file}") return - outfile = input("[*] Enter path to output wordlist: ").strip() + outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip() if not outfile: print("[!] Output path cannot be empty.") return @@ -1064,15 +1078,19 @@ def wordlist_subtract_words(ctx: Any) -> None: else: print("[!] Subtraction failed.") elif mode == "2": - infile = input("[*] Enter path to input wordlist: ").strip() + infile = ctx.select_file_with_autocomplete( + "[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists + ).strip() if not os.path.isfile(infile): print(f"[!] File not found: {infile}") return - outfile = input("[*] Enter path to output wordlist: ").strip() + outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip() if not outfile: print("[!] Output path cannot be empty.") return - raw = input("[*] Enter remove file paths (comma-separated): ").strip() + raw = ctx.select_file_with_autocomplete( + "[*] Enter remove file paths", allow_multiple=True, base_dir=ctx.hcatWordlists + ).strip() remove_files = [r.strip() for r in raw.split(",") if r.strip()] if not remove_files: print("[!] No remove files provided.") @@ -1087,11 +1105,13 @@ def wordlist_subtract_words(ctx: Any) -> None: def wordlist_shard(ctx: Any) -> None: """Prompt for input/output paths, modulus, and offset, then shard the wordlist.""" - infile = input("\n[*] Enter path to input wordlist: ").strip() + infile = ctx.select_file_with_autocomplete( + "\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists + ).strip() if not os.path.isfile(infile): print(f"[!] File not found: {infile}") return - outfile = input("[*] Enter path to output wordlist: ").strip() + outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip() if not outfile: print("[!] Output path cannot be empty.") return diff --git a/tests/test_wordlist_tools.py b/tests/test_wordlist_tools.py index b4c538f..36cd5b8 100644 --- a/tests/test_wordlist_tools.py +++ b/tests/test_wordlist_tools.py @@ -35,22 +35,23 @@ class TestWordlistFilterLength: infile = tmp_path / "in.txt" infile.write_text("test\n") outfile = tmp_path / "out.txt" - with patch("builtins.input", side_effect=[str(infile), str(outfile), "4", "8"]): + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)] + with patch("builtins.input", side_effect=["4", "8"]): wordlist_filter_length(ctx) ctx.wordlist_filter_len.assert_called_once_with(str(infile), str(outfile), 4, 8) def test_rejects_nonexistent_infile(self, tmp_path): ctx = _make_ctx() - with patch("builtins.input", return_value="/nonexistent/file.txt"): - wordlist_filter_length(ctx) + ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt" + wordlist_filter_length(ctx) ctx.wordlist_filter_len.assert_not_called() def test_rejects_empty_outfile(self, tmp_path): ctx = _make_ctx() infile = tmp_path / "in.txt" infile.write_text("test\n") - with patch("builtins.input", side_effect=[str(infile), ""]): - wordlist_filter_length(ctx) + ctx.select_file_with_autocomplete.side_effect = [str(infile), ""] + wordlist_filter_length(ctx) ctx.wordlist_filter_len.assert_not_called() def test_prints_success_message(self, tmp_path, capsys): @@ -58,7 +59,8 @@ class TestWordlistFilterLength: infile = tmp_path / "in.txt" infile.write_text("test\n") outfile = tmp_path / "out.txt" - with patch("builtins.input", side_effect=[str(infile), str(outfile), "4", "8"]): + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)] + with patch("builtins.input", side_effect=["4", "8"]): wordlist_filter_length(ctx) out = capsys.readouterr().out assert "success" in out.lower() or "wrote" in out.lower() or str(outfile) in out @@ -69,7 +71,8 @@ class TestWordlistFilterLength: infile = tmp_path / "in.txt" infile.write_text("test\n") outfile = tmp_path / "out.txt" - with patch("builtins.input", side_effect=[str(infile), str(outfile), "4", "8"]): + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)] + with patch("builtins.input", side_effect=["4", "8"]): wordlist_filter_length(ctx) out = capsys.readouterr().out assert "fail" in out.lower() or "error" in out.lower() or "!" in out @@ -81,14 +84,15 @@ class TestWordlistFilterCharclassInclude: infile = tmp_path / "in.txt" infile.write_text("test\n") outfile = tmp_path / "out.txt" - with patch("builtins.input", side_effect=[str(infile), str(outfile), "7"]): + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)] + with patch("builtins.input", side_effect=["7"]): wordlist_filter_charclass_include(ctx) ctx.wordlist_filter_req_include.assert_called_once_with(str(infile), str(outfile), 7) def test_rejects_nonexistent_infile(self, tmp_path): ctx = _make_ctx() - with patch("builtins.input", return_value="/nonexistent/file.txt"): - wordlist_filter_charclass_include(ctx) + ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt" + wordlist_filter_charclass_include(ctx) ctx.wordlist_filter_req_include.assert_not_called() @@ -98,14 +102,15 @@ class TestWordlistFilterCharclassExclude: infile = tmp_path / "in.txt" infile.write_text("test\n") outfile = tmp_path / "out.txt" - with patch("builtins.input", side_effect=[str(infile), str(outfile), "8"]): + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)] + with patch("builtins.input", side_effect=["8"]): wordlist_filter_charclass_exclude(ctx) ctx.wordlist_filter_req_exclude.assert_called_once_with(str(infile), str(outfile), 8) def test_rejects_nonexistent_infile(self, tmp_path): ctx = _make_ctx() - with patch("builtins.input", return_value="/nonexistent/file.txt"): - wordlist_filter_charclass_exclude(ctx) + ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt" + wordlist_filter_charclass_exclude(ctx) ctx.wordlist_filter_req_exclude.assert_not_called() @@ -115,7 +120,8 @@ class TestWordlistCutSubstring: infile = tmp_path / "in.txt" infile.write_text("test\n") outfile = tmp_path / "out.txt" - with patch("builtins.input", side_effect=[str(infile), str(outfile), "2", "4"]): + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)] + with patch("builtins.input", side_effect=["2", "4"]): wordlist_cut_substring(ctx) ctx.wordlist_cutb.assert_called_once_with(str(infile), str(outfile), 2, 4) @@ -124,14 +130,15 @@ class TestWordlistCutSubstring: infile = tmp_path / "in.txt" infile.write_text("test\n") outfile = tmp_path / "out.txt" - with patch("builtins.input", side_effect=[str(infile), str(outfile), "2", ""]): + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)] + with patch("builtins.input", side_effect=["2", ""]): wordlist_cut_substring(ctx) ctx.wordlist_cutb.assert_called_once_with(str(infile), str(outfile), 2, None) def test_rejects_nonexistent_infile(self, tmp_path): ctx = _make_ctx() - with patch("builtins.input", return_value="/nonexistent/file.txt"): - wordlist_cut_substring(ctx) + ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt" + wordlist_cut_substring(ctx) ctx.wordlist_cutb.assert_not_called() @@ -141,8 +148,8 @@ class TestWordlistSplitByLength: infile = tmp_path / "in.txt" infile.write_text("test\n") outdir = tmp_path / "split" - with patch("builtins.input", side_effect=[str(infile), str(outdir)]): - wordlist_split_by_length(ctx) + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outdir)] + wordlist_split_by_length(ctx) ctx.wordlist_splitlen.assert_called_once_with(str(infile), str(outdir)) def test_creates_outdir_if_missing(self, tmp_path): @@ -150,14 +157,14 @@ class TestWordlistSplitByLength: infile = tmp_path / "in.txt" infile.write_text("test\n") outdir = tmp_path / "split" / "nested" - with patch("builtins.input", side_effect=[str(infile), str(outdir)]): - wordlist_split_by_length(ctx) + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outdir)] + wordlist_split_by_length(ctx) assert outdir.exists() def test_rejects_nonexistent_infile(self, tmp_path): ctx = _make_ctx() - with patch("builtins.input", return_value="/nonexistent/file.txt"): - wordlist_split_by_length(ctx) + ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt" + wordlist_split_by_length(ctx) ctx.wordlist_splitlen.assert_not_called() @@ -169,8 +176,8 @@ class TestWordlistSubtractWords: removefile = tmp_path / "remove.txt" removefile.write_text("word1\n") outfile = tmp_path / "out.txt" - # "1" = single remove - with patch("builtins.input", side_effect=["1", str(infile), str(removefile), str(outfile)]): + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(removefile), str(outfile)] + with patch("builtins.input", side_effect=["1"]): wordlist_subtract_words(ctx) ctx.wordlist_subtract_single.assert_called_once_with(str(infile), str(removefile), str(outfile)) @@ -183,11 +190,12 @@ class TestWordlistSubtractWords: removefile2 = tmp_path / "remove2.txt" removefile2.write_text("word2\n") outfile = tmp_path / "out.txt" - # "2" = multiple removes, then two remove files separated by comma or newline - with patch( - "builtins.input", - side_effect=["2", str(infile), str(outfile), f"{removefile1},{removefile2}"], - ): + ctx.select_file_with_autocomplete.side_effect = [ + str(infile), + str(outfile), + f"{removefile1},{removefile2}", + ] + with patch("builtins.input", side_effect=["2"]): wordlist_subtract_words(ctx) ctx.wordlist_subtract.assert_called_once_with( str(infile), str(outfile), str(removefile1), str(removefile2) @@ -195,7 +203,8 @@ class TestWordlistSubtractWords: def test_single_remove_rejects_nonexistent_infile(self, tmp_path): ctx = _make_ctx() - with patch("builtins.input", side_effect=["1", "/nonexistent.txt"]): + ctx.select_file_with_autocomplete.return_value = "/nonexistent.txt" + with patch("builtins.input", side_effect=["1"]): wordlist_subtract_words(ctx) ctx.wordlist_subtract_single.assert_not_called() @@ -206,7 +215,8 @@ class TestWordlistShard: infile = tmp_path / "in.txt" infile.write_text("word1\nword2\nword3\n") outfile = tmp_path / "shard.txt" - with patch("builtins.input", side_effect=[str(infile), str(outfile), "3", "0"]): + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)] + with patch("builtins.input", side_effect=["3", "0"]): wordlist_shard(ctx) ctx.wordlist_gate.assert_called_once_with(str(infile), str(outfile), 3, 0) @@ -215,14 +225,15 @@ class TestWordlistShard: infile = tmp_path / "in.txt" infile.write_text("word1\n") outfile = tmp_path / "shard.txt" - with patch("builtins.input", side_effect=[str(infile), str(outfile), "3", "3"]): + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)] + with patch("builtins.input", side_effect=["3", "3"]): wordlist_shard(ctx) ctx.wordlist_gate.assert_not_called() def test_rejects_nonexistent_infile(self, tmp_path): ctx = _make_ctx() - with patch("builtins.input", return_value="/nonexistent/file.txt"): - wordlist_shard(ctx) + ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt" + wordlist_shard(ctx) ctx.wordlist_gate.assert_not_called() def test_rejects_mod_less_than_2(self, tmp_path): @@ -230,7 +241,8 @@ class TestWordlistShard: infile = tmp_path / "in.txt" infile.write_text("word1\n") outfile = tmp_path / "shard.txt" - with patch("builtins.input", side_effect=[str(infile), str(outfile), "1", "0"]): + ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)] + with patch("builtins.input", side_effect=["1", "0"]): wordlist_shard(ctx) ctx.wordlist_gate.assert_not_called()