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.
- evolver_tools/__init__.py +2 -0
- evolver_tools/__main__.py +3 -0
- evolver_tools/cli.py +89 -0
- evolver_tools/vendor/b64/__init__.py +2 -0
- evolver_tools/vendor/b64/b64.py +176 -0
- evolver_tools/vendor/cal_tool/__init__.py +1 -0
- evolver_tools/vendor/cal_tool/cli.py +234 -0
- evolver_tools/vendor/chart_cli/__init__.py +444 -0
- evolver_tools/vendor/chart_cli/__main__.py +3 -0
- evolver_tools/vendor/colors/__init__.py +5 -0
- evolver_tools/vendor/colors/__main__.py +97 -0
- evolver_tools/vendor/csv_stats/__init__.py +5 -0
- evolver_tools/vendor/csv_stats/__main__.py +4 -0
- evolver_tools/vendor/csv_stats/analyzer.py +258 -0
- evolver_tools/vendor/csv_stats/cli.py +45 -0
- evolver_tools/vendor/dirsize/__init__.py +183 -0
- evolver_tools/vendor/envcheck/__init__.py +426 -0
- evolver_tools/vendor/ff/__init__.py +427 -0
- evolver_tools/vendor/ff/__main__.py +3 -0
- evolver_tools/vendor/find_dups/__init__.py +7 -0
- evolver_tools/vendor/find_dups/cli.py +392 -0
- evolver_tools/vendor/hashsum/__init__.py +211 -0
- evolver_tools/vendor/hashsum/__main__.py +5 -0
- evolver_tools/vendor/http_live/__init__.py +265 -0
- evolver_tools/vendor/http_live/__main__.py +2 -0
- evolver_tools/vendor/ipinfo/__init__.py +3 -0
- evolver_tools/vendor/ipinfo/__main__.py +30 -0
- evolver_tools/vendor/jq_lite/__init__.py +257 -0
- evolver_tools/vendor/jq_lite/__main__.py +5 -0
- evolver_tools/vendor/json2csv/__init__.py +3 -0
- evolver_tools/vendor/json2csv/__main__.py +82 -0
- evolver_tools/vendor/jsonql/__init__.py +326 -0
- evolver_tools/vendor/jsonql/__main__.py +5 -0
- evolver_tools/vendor/license_cli/__init__.py +1 -0
- evolver_tools/vendor/license_cli/__main__.py +4 -0
- evolver_tools/vendor/license_cli/cli.py +289 -0
- evolver_tools/vendor/markdown_check/__init__.py +211 -0
- evolver_tools/vendor/nb/__init__.py +319 -0
- evolver_tools/vendor/nb/__main__.py +3 -0
- evolver_tools/vendor/passgen/__init__.py +224 -0
- evolver_tools/vendor/portcheck/__init__.py +2 -0
- evolver_tools/vendor/portcheck/__main__.py +66 -0
- evolver_tools/vendor/project_doctor/__init__.py +412 -0
- evolver_tools/vendor/project_doctor/__main__.py +3 -0
- evolver_tools/vendor/ren/__init__.py +283 -0
- evolver_tools/vendor/ren/__main__.py +3 -0
- evolver_tools/vendor/siege_lite/__init__.py +250 -0
- evolver_tools/vendor/siege_lite/__main__.py +3 -0
- evolver_tools/vendor/smellfinder/__init__.py +376 -0
- evolver_tools/vendor/smellfinder/__main__.py +3 -0
- evolver_tools/vendor/sqlite_cli/__init__.py +326 -0
- evolver_tools/vendor/sqlite_cli/__main__.py +5 -0
- evolver_tools/vendor/sysmon/__init__.py +299 -0
- evolver_tools/vendor/sysmon/__main__.py +3 -0
- evolver_tools/vendor/timer/__init__.py +127 -0
- evolver_tools/vendor/treedir/__init__.py +2 -0
- evolver_tools/vendor/treedir/__main__.py +128 -0
- evolver_tools/vendor/urlparse_tool/__init__.py +3 -0
- evolver_tools/vendor/urlparse_tool/cli.py +212 -0
- evolver_tools/vendor/web_summary/__init__.py +341 -0
- evolver_tools/vendor/web_summary/__main__.py +3 -0
- evolver_tools/vendor/wordcount/__init__.py +2 -0
- evolver_tools/vendor/wordcount/__main__.py +101 -0
- evolver_tools-1.4.0.dist-info/METADATA +107 -0
- evolver_tools-1.4.0.dist-info/RECORD +69 -0
- evolver_tools-1.4.0.dist-info/WHEEL +5 -0
- evolver_tools-1.4.0.dist-info/entry_points.txt +34 -0
- evolver_tools-1.4.0.dist-info/licenses/LICENSE +21 -0
- 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()
|