evolver-tools 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. evolver_tools/__init__.py +2 -0
  2. evolver_tools/__main__.py +3 -0
  3. evolver_tools/cli.py +89 -0
  4. evolver_tools/vendor/b64/__init__.py +2 -0
  5. evolver_tools/vendor/b64/b64.py +176 -0
  6. evolver_tools/vendor/cal_tool/__init__.py +1 -0
  7. evolver_tools/vendor/cal_tool/cli.py +234 -0
  8. evolver_tools/vendor/chart_cli/__init__.py +444 -0
  9. evolver_tools/vendor/chart_cli/__main__.py +3 -0
  10. evolver_tools/vendor/colors/__init__.py +5 -0
  11. evolver_tools/vendor/colors/__main__.py +97 -0
  12. evolver_tools/vendor/csv_stats/__init__.py +5 -0
  13. evolver_tools/vendor/csv_stats/__main__.py +4 -0
  14. evolver_tools/vendor/csv_stats/analyzer.py +258 -0
  15. evolver_tools/vendor/csv_stats/cli.py +45 -0
  16. evolver_tools/vendor/dirsize/__init__.py +183 -0
  17. evolver_tools/vendor/envcheck/__init__.py +426 -0
  18. evolver_tools/vendor/ff/__init__.py +427 -0
  19. evolver_tools/vendor/ff/__main__.py +3 -0
  20. evolver_tools/vendor/find_dups/__init__.py +7 -0
  21. evolver_tools/vendor/find_dups/cli.py +392 -0
  22. evolver_tools/vendor/hashsum/__init__.py +211 -0
  23. evolver_tools/vendor/hashsum/__main__.py +5 -0
  24. evolver_tools/vendor/http_live/__init__.py +265 -0
  25. evolver_tools/vendor/http_live/__main__.py +2 -0
  26. evolver_tools/vendor/ipinfo/__init__.py +3 -0
  27. evolver_tools/vendor/ipinfo/__main__.py +30 -0
  28. evolver_tools/vendor/jq_lite/__init__.py +257 -0
  29. evolver_tools/vendor/jq_lite/__main__.py +5 -0
  30. evolver_tools/vendor/json2csv/__init__.py +3 -0
  31. evolver_tools/vendor/json2csv/__main__.py +82 -0
  32. evolver_tools/vendor/jsonql/__init__.py +326 -0
  33. evolver_tools/vendor/jsonql/__main__.py +5 -0
  34. evolver_tools/vendor/license_cli/__init__.py +1 -0
  35. evolver_tools/vendor/license_cli/__main__.py +4 -0
  36. evolver_tools/vendor/license_cli/cli.py +289 -0
  37. evolver_tools/vendor/markdown_check/__init__.py +211 -0
  38. evolver_tools/vendor/nb/__init__.py +319 -0
  39. evolver_tools/vendor/nb/__main__.py +3 -0
  40. evolver_tools/vendor/passgen/__init__.py +224 -0
  41. evolver_tools/vendor/portcheck/__init__.py +2 -0
  42. evolver_tools/vendor/portcheck/__main__.py +66 -0
  43. evolver_tools/vendor/project_doctor/__init__.py +412 -0
  44. evolver_tools/vendor/project_doctor/__main__.py +3 -0
  45. evolver_tools/vendor/ren/__init__.py +283 -0
  46. evolver_tools/vendor/ren/__main__.py +3 -0
  47. evolver_tools/vendor/siege_lite/__init__.py +250 -0
  48. evolver_tools/vendor/siege_lite/__main__.py +3 -0
  49. evolver_tools/vendor/smellfinder/__init__.py +376 -0
  50. evolver_tools/vendor/smellfinder/__main__.py +3 -0
  51. evolver_tools/vendor/sqlite_cli/__init__.py +326 -0
  52. evolver_tools/vendor/sqlite_cli/__main__.py +5 -0
  53. evolver_tools/vendor/sysmon/__init__.py +299 -0
  54. evolver_tools/vendor/sysmon/__main__.py +3 -0
  55. evolver_tools/vendor/timer/__init__.py +127 -0
  56. evolver_tools/vendor/treedir/__init__.py +2 -0
  57. evolver_tools/vendor/treedir/__main__.py +128 -0
  58. evolver_tools/vendor/urlparse_tool/__init__.py +3 -0
  59. evolver_tools/vendor/urlparse_tool/cli.py +212 -0
  60. evolver_tools/vendor/web_summary/__init__.py +341 -0
  61. evolver_tools/vendor/web_summary/__main__.py +3 -0
  62. evolver_tools/vendor/wordcount/__init__.py +2 -0
  63. evolver_tools/vendor/wordcount/__main__.py +101 -0
  64. evolver_tools-1.4.0.dist-info/METADATA +107 -0
  65. evolver_tools-1.4.0.dist-info/RECORD +69 -0
  66. evolver_tools-1.4.0.dist-info/WHEEL +5 -0
  67. evolver_tools-1.4.0.dist-info/entry_points.txt +34 -0
  68. evolver_tools-1.4.0.dist-info/licenses/LICENSE +21 -0
  69. evolver_tools-1.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,427 @@
1
+ """ff: Interactive fuzzy finder — pure Python, zero dependencies.
2
+
3
+ Read items from stdin or file, then interactively search and select.
4
+ Heavily inspired by fzf (junegunn/fzf). Uses curses for the TUI.
5
+
6
+ Usage:
7
+ cat file.txt | ff # Pipe mode
8
+ ff -f file.txt # File mode
9
+ find . -type f | ff # Pipe from find
10
+ ps aux | ff # Pipe process list
11
+ ff -m < file.txt # Multi-select mode
12
+ """
13
+
14
+ import sys
15
+ import os
16
+ import curses
17
+ import textwrap
18
+
19
+ __version__ = "1.0.0"
20
+
21
+
22
+ # ─── Fuzzy matching ───────────────────────────────────────────────────────────
23
+
24
+ def fuzzy_match(query: str, text: str) -> bool:
25
+ """Simple subsequence matching: does query appear in order in text?"""
26
+ query = query.lower()
27
+ text_lower = text.lower()
28
+ qi = 0
29
+ for ch in text_lower:
30
+ if qi < len(query) and ch == query[qi]:
31
+ qi += 1
32
+ return qi == len(query)
33
+
34
+
35
+ def score_match(query: str, text: str) -> int:
36
+ """Score a fuzzy match — higher is better.
37
+
38
+ Bonuses for:
39
+ - Consecutive match (adjacent chars in query match adjacent in text)
40
+ - Match at word boundary
41
+ - Match at start of line
42
+ - Match starting at capital letter
43
+ """
44
+ query = query.lower()
45
+ text_lower = text.lower()
46
+ score = 0
47
+ qi = 0
48
+ prev_was_boundary = True # Start-of-line is a boundary
49
+
50
+ for ti, ch in enumerate(text_lower):
51
+ if qi < len(query) and ch == query[qi]:
52
+ if qi == 0:
53
+ # First character match
54
+ if ti == 0:
55
+ score += 50 # Start of line
56
+ elif prev_was_boundary:
57
+ score += 40 # After word boundary
58
+ else:
59
+ score += 10 # General match
60
+ else:
61
+ if prev_was_boundary:
62
+ score += 30 # Consecutive + boundary
63
+ else:
64
+ # Check if previous char in text matches previous char in query
65
+ if qi > 0 and ti > 0 and text_lower[ti - 1] == query[qi - 1]:
66
+ score += 20 # Consecutive match
67
+ else:
68
+ score += 5 # Non-consecutive
69
+ qi += 1
70
+
71
+ # Track word boundaries
72
+ prev = text_lower[ti] if ti < len(text_lower) else ''
73
+ prev_was_boundary = ch in (' ', '-', '_', '/', '.', ':', '(', '[') or (
74
+ ch.isupper() and ti > 0 and text[ti - 1].islower()
75
+ )
76
+
77
+ return score
78
+
79
+
80
+ def highlight_matches(query: str, text: str) -> list:
81
+ """Return list of (char, is_match) tuples for display."""
82
+ if not query:
83
+ return [(c, False) for c in text]
84
+
85
+ query_lower = query.lower()
86
+ text_lower = text.lower()
87
+ qi = 0
88
+ result = []
89
+ for ch in text:
90
+ if qi < len(query_lower) and ch.lower() == query_lower[qi]:
91
+ result.append((ch, True))
92
+ qi += 1
93
+ else:
94
+ result.append((ch, False))
95
+ return result
96
+
97
+
98
+ # ─── Curses UI ────────────────────────────────────────────────────────────────
99
+
100
+ def run_fzf(items: list, multi: bool = False, preview: str = None) -> list:
101
+ """Run the interactive fuzzy finder and return selected items."""
102
+ if not items:
103
+ return []
104
+
105
+ selected = []
106
+ current_idx = 0
107
+ query = ""
108
+
109
+ def get_matches():
110
+ if not query:
111
+ return list(enumerate(items))
112
+ scored = []
113
+ for i, item in enumerate(items):
114
+ if fuzzy_match(query, item):
115
+ sc = score_match(query, item)
116
+ scored.append((sc, i, item))
117
+ scored.sort(key=lambda x: (-x[0], x[1]))
118
+ return [(i, item) for _, i, item in scored]
119
+
120
+ def draw(stdscr):
121
+ nonlocal current_idx
122
+ curses.curs_set(1)
123
+ curses.use_default_colors()
124
+ max_y, max_x = stdscr.getmaxyx()
125
+
126
+ # Color pairs
127
+ curses.init_pair(1, curses.COLOR_CYAN, -1) # Query text
128
+ curses.init_pair(2, curses.COLOR_GREEN, -1) # Selected items
129
+ curses.init_pair(3, curses.COLOR_YELLOW, -1) # Match highlights
130
+ curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE) # Cursor line
131
+ curses.init_pair(5, curses.COLOR_RED, -1) # Count / status
132
+
133
+ matches = get_matches()
134
+
135
+ # Clamp selection
136
+ if current_idx >= len(matches):
137
+ current_idx = max(0, len(matches) - 1)
138
+
139
+ # Input line
140
+ prompt = "> " if not multi else "> [multi] "
141
+ stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
142
+ stdscr.addstr(0, 0, prompt)
143
+ stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
144
+
145
+ query_display = query
146
+ if len(query_display) > max_x - len(prompt) - 1:
147
+ query_display = query_display[-(max_x - len(prompt) - 1):]
148
+ stdscr.addstr(0, len(prompt), query_display)
149
+ cursor_x = len(prompt) + len(query_display)
150
+
151
+ # Status line
152
+ status = f" {len(matches)}/{len(items)}"
153
+ if multi:
154
+ sel_count = len(selected)
155
+ status += f" | {sel_count} selected"
156
+
157
+ stdscr.attron(curses.A_REVERSE)
158
+ try:
159
+ if len(status) < max_x:
160
+ stdscr.addstr(1, 0, status.ljust(max_x - 1))
161
+ except curses.error:
162
+ pass
163
+ stdscr.attroff(curses.A_REVERSE)
164
+
165
+ # Results area
166
+ visible_height = max_y - 2
167
+ scroll_offset = 0
168
+ if current_idx >= visible_height:
169
+ scroll_offset = current_idx - visible_height + 1
170
+
171
+ for i in range(visible_height):
172
+ idx = scroll_offset + i
173
+ if idx >= len(matches):
174
+ break
175
+
176
+ match_idx, item = matches[idx]
177
+ is_cursor = idx == current_idx
178
+
179
+ # Truncate item
180
+ display_item = item.replace('\t', ' ' * 4).replace('\n', ' ')
181
+ if len(display_item) > max_x - 2:
182
+ display_item = display_item[:max_x - 5] + '...'
183
+
184
+ is_selected = match_idx in selected
185
+
186
+ if is_cursor:
187
+ stdscr.attron(curses.color_pair(4))
188
+ sel_marker = ">" if is_selected else " "
189
+ line = f"{sel_marker} {display_item}".ljust(max_x - 1)
190
+ try:
191
+ stdscr.addstr(i + 2, 0, line[:max_x - 1])
192
+ except curses.error:
193
+ pass
194
+ stdscr.attroff(curses.color_pair(4))
195
+ elif is_selected:
196
+ stdscr.attron(curses.color_pair(2))
197
+ try:
198
+ stdscr.addstr(i + 2, 0, f"* {display_item}"[:max_x - 1])
199
+ except curses.error:
200
+ pass
201
+ stdscr.attroff(curses.color_pair(2))
202
+ else:
203
+ try:
204
+ stdscr.addstr(i + 2, 0, f" {display_item}"[:max_x - 1])
205
+ except curses.error:
206
+ pass
207
+
208
+ # Clear rest of screen
209
+ for i in range(len(matches) - scroll_offset, visible_height):
210
+ try:
211
+ stdscr.addstr(i + 2, 0, " " * (max_x - 1))
212
+ except curses.error:
213
+ pass
214
+
215
+ return cursor_x
216
+
217
+ # ─── Main input loop ──────────────────────────────────────────────────
218
+
219
+ selected_result = []
220
+ done = False
221
+
222
+ def main(stdscr):
223
+ nonlocal current_idx, query, selected_result, done
224
+
225
+ curses.cbreak()
226
+ curses.noecho()
227
+ stdscr.keypad(True)
228
+
229
+ while not done:
230
+ cursor_x = draw(stdscr)
231
+ stdscr.move(1, 0) # Move cursor to status line field (safe zone)
232
+ stdscr.refresh()
233
+
234
+ try:
235
+ key = stdscr.get_wch()
236
+ except KeyboardInterrupt:
237
+ selected_result = None
238
+ break
239
+
240
+ if isinstance(key, str):
241
+ if key == '\n': # Enter
242
+ matches = [item for _, item in (
243
+ (i, item) for i, item in enumerate(items) if fuzzy_match(query, item)
244
+ ) if True]
245
+ if multi:
246
+ if selected:
247
+ selected_result = selected.copy()
248
+ elif matches:
249
+ idx = current_idx if current_idx < len(matches) else 0
250
+ selected_result = [matches[idx] if idx < len(matches) else '']
251
+ else:
252
+ matches_list = [
253
+ item for _, item in (list(enumerate(items)) if not query else
254
+ [(i, items[i]) for i, _ in [(i, item) for i, item in enumerate(items) if fuzzy_match(query, item)]]
255
+ )
256
+ ]
257
+ # Re-get matches correctly
258
+ scored = []
259
+ for i, item in enumerate(items):
260
+ if fuzzy_match(query, item):
261
+ sc = score_match(query, item)
262
+ scored.append((sc, i, item))
263
+ scored.sort(key=lambda x: (-x[0], x[1]))
264
+ matches_list = [item for _, _, item in scored]
265
+ if matches_list:
266
+ idx = min(current_idx, len(matches_list) - 1)
267
+ selected_result = [matches_list[idx]]
268
+ done = True
269
+ return
270
+
271
+ elif key == '\x1b': # Escape
272
+ selected_result = None
273
+ done = True
274
+ return
275
+
276
+ elif key == '\t' and multi: # Tab to select/deselect
277
+ matches = [
278
+ item for _, item in (
279
+ [(i, items[i]) for i, item in enumerate(items) if fuzzy_match(query, item)]
280
+ )
281
+ ]
282
+ if matches:
283
+ idx = min(current_idx, len(matches) - 1)
284
+ match_orig_idx = items.index(matches[idx]) # inefficient but works
285
+ # Find the original index
286
+ orig_idx = -1
287
+ count = 0
288
+ for i, item in enumerate(items):
289
+ if fuzzy_match(query, item):
290
+ if count == current_idx:
291
+ orig_idx = i
292
+ break
293
+ count += 1
294
+ if orig_idx >= 0:
295
+ if orig_idx in selected:
296
+ selected.remove(orig_idx)
297
+ else:
298
+ selected.append(orig_idx)
299
+
300
+ elif key == '\x7f': # Backspace
301
+ query = query[:-1]
302
+ current_idx = 0
303
+
304
+ elif key == '\x15': # Ctrl+U — clear query
305
+ query = ""
306
+ current_idx = 0
307
+
308
+ elif key == '\x0c': # Ctrl+L — redraw
309
+ pass
310
+
311
+ elif key.isprintable():
312
+ if len(query) < 200: # Cap query length
313
+ query += key
314
+ current_idx = 0
315
+
316
+ elif isinstance(key, int):
317
+ if key == curses.KEY_UP:
318
+ if current_idx > 0:
319
+ current_idx -= 1
320
+ elif key == curses.KEY_DOWN:
321
+ matches = sum(1 for _ in items if fuzzy_match(query, items[_] if isinstance(items, list) else items))
322
+ # simpler: just count matches
323
+ match_count = len([i for i in items if fuzzy_match(query, i)])
324
+ if current_idx < match_count - 1:
325
+ current_idx += 1
326
+ elif key == curses.KEY_NPAGE: # Page Down
327
+ match_count = len([i for i in items if fuzzy_match(query, i)])
328
+ current_idx = min(current_idx + 10, max(0, match_count - 1))
329
+ elif key == curses.KEY_PPAGE: # Page Up
330
+ current_idx = max(0, current_idx - 10)
331
+ elif key == curses.KEY_HOME:
332
+ current_idx = 0
333
+ elif key == curses.KEY_END:
334
+ match_count = len([i for i in items if fuzzy_match(query, i)])
335
+ current_idx = max(0, match_count - 1)
336
+ elif key == curses.KEY_RESIZE:
337
+ pass
338
+
339
+ curses.wrapper(main)
340
+
341
+ if selected_result is None:
342
+ return []
343
+ return selected_result
344
+
345
+
346
+ # ─── Preview support ──────────────────────────────────────────────────────────
347
+
348
+ def preview_item(item: str, preview_cmd: str) -> str:
349
+ """Run preview command on an item and return output."""
350
+ import subprocess
351
+ try:
352
+ cmd = preview_cmd.replace('{}', item)
353
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=2)
354
+ return result.stdout[:500] # Limit preview size
355
+ except Exception as e:
356
+ return f"Preview error: {e}"
357
+
358
+
359
+ # ─── Main CLI ─────────────────────────────────────────────────────────────────
360
+
361
+ def main():
362
+ import argparse
363
+
364
+ parser = argparse.ArgumentParser(
365
+ description="ff — Interactive fuzzy finder (zero dependencies)",
366
+ formatter_class=argparse.RawDescriptionHelpFormatter,
367
+ epilog=textwrap.dedent("""\\
368
+ Examples:
369
+ cat file.txt | ff # Search through file lines
370
+ ff -f /etc/passwd # Search from file
371
+ find . -type f | ff # Search file listing
372
+ ps aux | ff # Search process list
373
+ ff -m < tags.txt # Multi-select
374
+ ff --header "Pick a file:" < files # Custom header
375
+ """),
376
+ )
377
+ parser.add_argument("-f", "--file", help="Read items from file instead of stdin")
378
+ parser.add_argument("-m", "--multi", action="store_true", help="Multi-select mode (Tab to toggle)")
379
+ parser.add_argument("-q", "--query", default="", help="Start with initial query")
380
+ parser.add_argument("--header", default="", help="Header message")
381
+ parser.add_argument("--version", action="store_true", help="Show version")
382
+ args = parser.parse_args()
383
+
384
+ if args.version:
385
+ print(f"ff {__version__}")
386
+ return
387
+
388
+ # Read items
389
+ items = []
390
+ if args.file:
391
+ try:
392
+ with open(args.file) as f:
393
+ items = [line.rstrip('\n\r') for line in f]
394
+ except FileNotFoundError:
395
+ print(f"ff: {args.file}: No such file", file=sys.stderr)
396
+ sys.exit(1)
397
+ except PermissionError:
398
+ print(f"ff: {args.file}: Permission denied", file=sys.stderr)
399
+ sys.exit(1)
400
+ else:
401
+ if sys.stdin.isatty():
402
+ print("ff: No input. Pipe data to stdin or use -f FILE.", file=sys.stderr)
403
+ sys.exit(1)
404
+ items = [line.rstrip('\n\r') for line in sys.stdin]
405
+
406
+ if not items:
407
+ sys.exit(1)
408
+
409
+ # Remove trailing empty lines
410
+ while items and items[-1] == '':
411
+ items.pop()
412
+
413
+ # Apply initial query
414
+ query = args.query
415
+
416
+ # Run the finder
417
+ result = run_fzf(items, multi=args.multi)
418
+
419
+ if result:
420
+ for item in result:
421
+ print(item)
422
+ else:
423
+ sys.exit(1)
424
+
425
+
426
+ if __name__ == "__main__":
427
+ main()
@@ -0,0 +1,3 @@
1
+ """ff — Interactive fuzzy finder (CLI entry point)."""
2
+ from evolver_tools.vendor.ff import main
3
+ main()
@@ -0,0 +1,7 @@
1
+ """
2
+ find-dups — Find duplicate files by SHA256 content hash.
3
+
4
+ Zero external dependencies, stdlib only.
5
+ """
6
+
7
+ __version__ = "1.0.0"