fpf-cli 1.6.15 → 1.6.17

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 (3) hide show
  1. package/README.md +1 -0
  2. package/fpf +1311 -72
  3. package/package.json +1 -1
package/fpf CHANGED
@@ -3,16 +3,24 @@
3
3
  set -euo pipefail
4
4
 
5
5
  SCRIPT_NAME="fpf"
6
- SCRIPT_VERSION="1.6.15"
6
+ SCRIPT_VERSION="1.6.17"
7
7
  TMP_ROOT="${TMPDIR:-/tmp}/fpf"
8
8
  SESSION_TMP_ROOT=""
9
9
  HELP_FILE=""
10
10
  KBINDS_FILE=""
11
+ CACHE_FORMAT_VERSION="1"
12
+ CACHE_ROOT=""
11
13
 
12
14
  ACTION="search"
13
15
  MANAGER_OVERRIDE=""
16
+ IPC_MANAGER_OVERRIDE=""
17
+ IPC_FALLBACK_FILE=""
14
18
  declare -a QUERY_PARTS=()
15
19
 
20
+ query_cache_flags() {
21
+ printf "%s" "query_limit=${FPF_QUERY_RESULT_LIMIT:-0};per_manager_limit=${FPF_QUERY_PER_MANAGER_LIMIT:-40};no_query_limit=${FPF_NO_QUERY_RESULT_LIMIT:-120};no_query_npm_limit=${FPF_NO_QUERY_NPM_LIMIT:-120}"
22
+ }
23
+
16
24
  log() {
17
25
  printf "%s\n" "$*" >&2
18
26
  }
@@ -30,16 +38,672 @@ ensure_tmp_root() {
30
38
  mkdir -p "${TMP_ROOT}"
31
39
  }
32
40
 
41
+ resolve_cache_root() {
42
+ local os
43
+ os="$(uname -s)"
44
+
45
+ if [[ -n "${FPF_CACHE_DIR:-}" ]]; then
46
+ printf "%s" "${FPF_CACHE_DIR}"
47
+ return
48
+ fi
49
+
50
+ case "${os}" in
51
+ Darwin)
52
+ printf "%s" "${HOME}/Library/Caches/fpf"
53
+ ;;
54
+ Linux)
55
+ if [[ -n "${XDG_CACHE_HOME:-}" ]]; then
56
+ printf "%s" "${XDG_CACHE_HOME}/fpf"
57
+ else
58
+ printf "%s" "${HOME}/.cache/fpf"
59
+ fi
60
+ ;;
61
+ MINGW*|MSYS*|CYGWIN*|Windows_NT)
62
+ if [[ -n "${LOCALAPPDATA:-}" ]]; then
63
+ printf "%s" "${LOCALAPPDATA}/fpf"
64
+ elif [[ -n "${APPDATA:-}" ]]; then
65
+ printf "%s" "${APPDATA}/fpf"
66
+ else
67
+ printf "%s" "${HOME}/.cache/fpf"
68
+ fi
69
+ ;;
70
+ *)
71
+ printf "%s" "${HOME}/.cache/fpf"
72
+ ;;
73
+ esac
74
+ }
75
+
76
+ initialize_cache_root() {
77
+ if [[ -n "${CACHE_ROOT}" ]]; then
78
+ return
79
+ fi
80
+
81
+ CACHE_ROOT="$(resolve_cache_root)"
82
+ mkdir -p "${CACHE_ROOT}"
83
+ }
84
+
85
+ normalize_cache_query() {
86
+ printf "%s" "$1" |
87
+ awk '{
88
+ line=$0
89
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
90
+ gsub(/[[:space:]]+/, " ", line)
91
+ print tolower(line)
92
+ }'
93
+ }
94
+
95
+ platform_cache_token() {
96
+ uname -s | tr '[:upper:]' '[:lower:]'
97
+ }
98
+
99
+ cache_fingerprint() {
100
+ local manager="$1"
101
+ local query="$2"
102
+ local flags="$3"
103
+ local normalized_query
104
+
105
+ normalized_query="$(normalize_cache_query "${query}")"
106
+ printf "%s|%s|%s|%s|%s" "${CACHE_FORMAT_VERSION}" "${manager}" "$(platform_cache_token)" "${normalized_query}" "${flags}"
107
+ }
108
+
109
+ cache_cksum() {
110
+ local input="$1"
111
+ printf "%s" "${input}" | cksum | awk '{ print $1 }'
112
+ }
113
+
114
+ cache_catalog_key() {
115
+ local manager="$1"
116
+ printf "catalog/%s.tsv" "${manager}"
117
+ }
118
+
119
+ cache_search_catalog_fingerprint() {
120
+ local manager="$1"
121
+ local command_path=""
122
+ local extra_token=""
123
+
124
+ case "${manager}" in
125
+ apt)
126
+ command_path="$(command -v apt-cache 2>/dev/null || printf "missing")"
127
+ if [[ "${FPF_TEST_FIXTURES:-0}" == "1" ]]; then
128
+ extra_token="|fixtures=${FPF_TEST_FIXTURE_DIR:-enabled}"
129
+ fi
130
+ ;;
131
+ brew)
132
+ command_path="$(command -v brew 2>/dev/null || printf "missing")"
133
+ if [[ "${FPF_TEST_FIXTURES:-0}" == "1" ]]; then
134
+ extra_token="|fixtures=${FPF_TEST_FIXTURE_DIR:-enabled}"
135
+ fi
136
+ ;;
137
+ *)
138
+ command_path="n/a"
139
+ ;;
140
+ esac
141
+
142
+ printf "%s|cmd=%s%s" "$(cache_fingerprint "${manager}" "" "search-catalog")" "${command_path}" "${extra_token}"
143
+ }
144
+
145
+ cache_search_catalog_key() {
146
+ local manager="$1"
147
+ local fingerprint="$2"
148
+ local checksum
149
+
150
+ checksum="$(cache_cksum "${fingerprint}")"
151
+ printf "search-catalog/%s/%s.tsv" "${manager}" "${checksum}"
152
+ }
153
+
154
+ cache_query_key() {
155
+ local manager="$1"
156
+ local query="$2"
157
+ local flags="$3"
158
+ local fingerprint
159
+ local checksum
160
+
161
+ fingerprint="$(cache_fingerprint "${manager}" "${query}" "${flags}")"
162
+ checksum="$(cache_cksum "${fingerprint}")"
163
+ printf "query/%s/%s.tsv" "${manager}" "${checksum}"
164
+ }
165
+
166
+ cache_meta_key() {
167
+ local key="$1"
168
+ printf "meta/%s.meta" "${key}"
169
+ }
170
+
171
+ cache_path_for_key() {
172
+ local key="$1"
173
+ printf "%s/%s" "${CACHE_ROOT}" "${key}"
174
+ }
175
+
176
+ cache_atomic_write_from_file() {
177
+ local source_file="$1"
178
+ local destination_file="$2"
179
+ local destination_dir
180
+ local destination_base
181
+ local temp_file
182
+
183
+ destination_dir="$(dirname "${destination_file}")"
184
+ destination_base="$(basename "${destination_file}")"
185
+
186
+ mkdir -p "${destination_dir}"
187
+ temp_file="$(mktemp "${destination_dir}/.${destination_base}.tmp.XXXXXX")"
188
+ cp "${source_file}" "${temp_file}"
189
+ mv "${temp_file}" "${destination_file}"
190
+ }
191
+
192
+ cache_write_meta() {
193
+ local key="$1"
194
+ local fingerprint="$2"
195
+ local item_count="$3"
196
+ local refresh_status="${4:-}"
197
+ local generation="${5:-}"
198
+ local last_error_at="${6:-}"
199
+ local last_error="${7:-}"
200
+ local created_at
201
+ local created_epoch
202
+ local meta_key
203
+ local meta_file
204
+ local temp_meta
205
+
206
+ created_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
207
+ created_epoch="$(date +%s)"
208
+ meta_key="$(cache_meta_key "${key}")"
209
+ meta_file="$(cache_path_for_key "${meta_key}")"
210
+ temp_meta="$(mktemp "${SESSION_TMP_ROOT}/meta.XXXXXX")"
211
+
212
+ {
213
+ printf "format_version=%s\n" "${CACHE_FORMAT_VERSION}"
214
+ printf "created_at=%s\n" "${created_at}"
215
+ printf "created_epoch=%s\n" "${created_epoch}"
216
+ printf "fingerprint=%s\n" "${fingerprint}"
217
+ printf "item_count=%s\n" "${item_count}"
218
+ if [[ -n "${refresh_status}" ]]; then
219
+ printf "refresh_status=%s\n" "${refresh_status}"
220
+ fi
221
+ if [[ -n "${generation}" ]]; then
222
+ printf "generation=%s\n" "${generation}"
223
+ fi
224
+ if [[ -n "${last_error_at}" ]]; then
225
+ printf "last_error_at=%s\n" "${last_error_at}"
226
+ fi
227
+ if [[ -n "${last_error}" ]]; then
228
+ printf "last_error=%s\n" "${last_error}"
229
+ fi
230
+ } >"${temp_meta}"
231
+
232
+ cache_atomic_write_from_file "${temp_meta}" "${meta_file}"
233
+ rm -f "${temp_meta}"
234
+ }
235
+
236
+ cache_store_key_from_file() {
237
+ local key="$1"
238
+ local fingerprint="$2"
239
+ local source_file="$3"
240
+ local refresh_status="${4:-}"
241
+ local generation="${5:-}"
242
+ local last_error_at="${6:-}"
243
+ local last_error="${7:-}"
244
+ local cache_file
245
+ local item_count
246
+
247
+ cache_file="$(cache_path_for_key "${key}")"
248
+ item_count="$(awk 'END { print NR + 0 }' "${source_file}")"
249
+
250
+ cache_atomic_write_from_file "${source_file}" "${cache_file}"
251
+ cache_write_meta "${key}" "${fingerprint}" "${item_count}" "${refresh_status}" "${generation}" "${last_error_at}" "${last_error}"
252
+ }
253
+
254
+ cache_meta_value_for_key() {
255
+ local key="$1"
256
+ local field_name="$2"
257
+ local meta_file
258
+
259
+ meta_file="$(cache_path_for_key "$(cache_meta_key "${key}")")"
260
+ if [[ ! -r "${meta_file}" ]]; then
261
+ return 1
262
+ fi
263
+
264
+ awk -F'=' -v key="${field_name}" '$1 == key { value=substr($0, index($0, "=") + 1); print value; exit }' "${meta_file}"
265
+ }
266
+
267
+ cache_is_fresh_with_ttl() {
268
+ local key="$1"
269
+ local ttl_seconds="$2"
270
+ local created_epoch
271
+ local now_epoch
272
+ local age_seconds
273
+
274
+ if ! [[ "${ttl_seconds}" =~ ^[0-9]+$ ]]; then
275
+ return 1
276
+ fi
277
+
278
+ if [[ "${ttl_seconds}" -eq 0 ]]; then
279
+ return 1
280
+ fi
281
+
282
+ created_epoch="$(cache_meta_value_for_key "${key}" "created_epoch" 2>/dev/null || true)"
283
+ if ! [[ "${created_epoch}" =~ ^[0-9]+$ ]]; then
284
+ return 1
285
+ fi
286
+
287
+ now_epoch="$(date +%s)"
288
+ if ! [[ "${now_epoch}" =~ ^[0-9]+$ ]]; then
289
+ return 1
290
+ fi
291
+
292
+ age_seconds=$((now_epoch - created_epoch))
293
+ if [[ "${age_seconds}" -lt 0 ]]; then
294
+ return 1
295
+ fi
296
+
297
+ [[ "${age_seconds}" -lt "${ttl_seconds}" ]]
298
+ }
299
+
300
+ cache_emit_query_rows_if_valid() {
301
+ local cache_file="$1"
302
+
303
+ [[ -s "${cache_file}" ]] || return 1
304
+
305
+ if ! awk -F'\t' 'NF >= 2 && $1 != "" { next } { exit 1 } END { if (NR == 0) exit 1 }' "${cache_file}" >/dev/null 2>&1; then
306
+ return 1
307
+ fi
308
+
309
+ awk -F'\t' 'NF >= 2 && $1 != "" { desc=$2; if (desc == "") desc="-"; print $1 "\t" desc }' "${cache_file}"
310
+ }
311
+
312
+ bun_generation_state_key() {
313
+ local key="$1"
314
+ printf "state/%s.generation" "${key}"
315
+ }
316
+
317
+ bun_generation_read() {
318
+ local key="$1"
319
+ local state_file
320
+ local current="0"
321
+
322
+ state_file="$(cache_path_for_key "$(bun_generation_state_key "${key}")")"
323
+ if [[ -r "${state_file}" ]]; then
324
+ current="$(awk 'NR == 1 { print $1; exit }' "${state_file}")"
325
+ fi
326
+
327
+ if ! [[ "${current}" =~ ^[0-9]+$ ]]; then
328
+ current="0"
329
+ fi
330
+
331
+ printf "%s" "${current}"
332
+ }
333
+
334
+ bun_generation_write() {
335
+ local key="$1"
336
+ local generation="$2"
337
+ local state_key
338
+ local state_file
339
+ local temp_file
340
+
341
+ state_key="$(bun_generation_state_key "${key}")"
342
+ state_file="$(cache_path_for_key "${state_key}")"
343
+ temp_file="$(mktemp "${SESSION_TMP_ROOT}/bun-generation.XXXXXX")"
344
+ printf "%s\n" "${generation}" >"${temp_file}"
345
+ cache_atomic_write_from_file "${temp_file}" "${state_file}"
346
+ rm -f "${temp_file}"
347
+ }
348
+
349
+ bun_generation_next() {
350
+ local key="$1"
351
+ local current
352
+ local next
353
+
354
+ current="$(bun_generation_read "${key}")"
355
+ next=$((current + 1))
356
+ bun_generation_write "${key}" "${next}"
357
+ printf "%s" "${next}"
358
+ }
359
+
360
+ bun_refresh_idle_seconds() {
361
+ local idle_seconds="${FPF_BUN_REFRESH_IDLE:-0.12}"
362
+ if ! [[ "${idle_seconds}" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
363
+ idle_seconds="0.12"
364
+ fi
365
+ printf "%s" "${idle_seconds}"
366
+ }
367
+
368
+ bun_emit_refresh_failure_meta() {
369
+ local key="$1"
370
+ local fingerprint="$2"
371
+ local generation="$3"
372
+ local last_error="$4"
373
+ local cache_file
374
+ local item_count="0"
375
+ local last_error_at
376
+
377
+ cache_file="$(cache_path_for_key "${key}")"
378
+ if [[ -s "${cache_file}" ]]; then
379
+ item_count="$(awk 'END { print NR + 0 }' "${cache_file}")"
380
+ fi
381
+
382
+ last_error_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
383
+ cache_write_meta "${key}" "${fingerprint}" "${item_count}" "error" "${generation}" "${last_error_at}" "${last_error}"
384
+ }
385
+
386
+ bun_try_hot_reload_after_refresh() {
387
+ local query="$1"
388
+
389
+ if [[ -n "${FPF_BUN_REFRESH_MANAGER_OVERRIDE:-}" || -n "${FPF_BUN_REFRESH_FALLBACK_FILE:-}" ]]; then
390
+ FPF_BUN_SKIP_REFRESH_SCHEDULE=1 \
391
+ FPF_IPC_MANAGER_OVERRIDE="${FPF_BUN_REFRESH_MANAGER_OVERRIDE:-}" \
392
+ FPF_IPC_FALLBACK_FILE="${FPF_BUN_REFRESH_FALLBACK_FILE:-}" \
393
+ run_ipc_reload_action "${query}" || true
394
+ fi
395
+
396
+ if [[ -n "${FPF_TEST_CACHE_REFRESH_SIGNAL_FILE:-}" ]] && command_exists fpf-refresh-signal; then
397
+ fpf-refresh-signal >/dev/null 2>&1 || true
398
+ fi
399
+ }
400
+
401
+ bun_run_refresh_worker() {
402
+ local manager="$1"
403
+ local query="$2"
404
+ local flags="$3"
405
+ local key="$4"
406
+ local fingerprint="$5"
407
+ local generation="$6"
408
+ local output_tmp
409
+
410
+ sleep "$(bun_refresh_idle_seconds)"
411
+
412
+ if [[ "${generation}" != "$(bun_generation_read "${key}")" ]]; then
413
+ return 0
414
+ fi
415
+
416
+ output_tmp="$(mktemp "${SESSION_TMP_ROOT}/bun-refresh.XXXXXX")"
417
+ if ! manager_bun_search_entries_strict "${query}" >"${output_tmp}"; then
418
+ rm -f "${output_tmp}"
419
+ bun_emit_refresh_failure_meta "${key}" "${fingerprint}" "${generation}" "bun_search_failed"
420
+ send_fzf_prompt_action "Search> " || true
421
+ return 1
422
+ fi
423
+
424
+ if [[ "${generation}" != "$(bun_generation_read "${key}")" ]]; then
425
+ rm -f "${output_tmp}"
426
+ return 0
427
+ fi
428
+
429
+ cache_store_key_from_file "${key}" "${fingerprint}" "${output_tmp}" "success" "${generation}"
430
+ rm -f "${output_tmp}"
431
+ bun_try_hot_reload_after_refresh "${query}"
432
+ return 0
433
+ }
434
+
435
+ start_bun_refresh_worker_async() {
436
+ local manager="$1"
437
+ local query="$2"
438
+ local flags="$3"
439
+ local key="$4"
440
+ local fingerprint="$5"
441
+ local generation="$6"
442
+ local fallback_file="$7"
443
+ local manager_override="$8"
444
+ local script_path="${BASH_SOURCE[0]}"
445
+
446
+ if [[ "${script_path}" != /* ]]; then
447
+ script_path="$(pwd)/${script_path}"
448
+ fi
449
+
450
+ FPF_BUN_REFRESH_FLAGS="${flags}" \
451
+ FPF_BUN_REFRESH_KEY="${key}" \
452
+ FPF_BUN_REFRESH_FINGERPRINT="${fingerprint}" \
453
+ FPF_BUN_REFRESH_GENERATION="${generation}" \
454
+ FPF_BUN_REFRESH_FALLBACK_FILE="${fallback_file}" \
455
+ FPF_BUN_REFRESH_MANAGER_OVERRIDE="${manager_override}" \
456
+ "${script_path}" --bun-refresh-worker --manager "${manager}" -- "${query}" >/dev/null 2>&1 &
457
+ }
458
+
459
+ build_apt_catalog_entries() {
460
+ apt-cache dumpavail 2>/dev/null |
461
+ awk '
462
+ function flush_entry() {
463
+ if (pkg == "") {
464
+ return
465
+ }
466
+ desc_out = desc
467
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", desc_out)
468
+ if (desc_out == "") {
469
+ desc_out = "-"
470
+ }
471
+ print pkg "\t" desc_out
472
+ }
473
+
474
+ /^Package:[[:space:]]*/ {
475
+ flush_entry()
476
+ pkg = $0
477
+ sub(/^Package:[[:space:]]*/, "", pkg)
478
+ desc = ""
479
+ next
480
+ }
481
+
482
+ /^Description:[[:space:]]*/ {
483
+ desc = $0
484
+ sub(/^Description:[[:space:]]*/, "", desc)
485
+ next
486
+ }
487
+
488
+ /^[[:space:]]+/ {
489
+ if (desc != "") {
490
+ line = $0
491
+ sub(/^[[:space:]]+/, "", line)
492
+ if (line != "") {
493
+ desc = desc " " line
494
+ }
495
+ }
496
+ next
497
+ }
498
+
499
+ /^$/ {
500
+ flush_entry()
501
+ pkg = ""
502
+ desc = ""
503
+ next
504
+ }
505
+
506
+ END {
507
+ flush_entry()
508
+ }
509
+ ' |
510
+ awk -F'\t' 'NF >= 1 && $1 != "" { if ($2 == "") $2 = "-"; print $1 "\t" $2 }' |
511
+ awk -F'\t' '!seen[$1]++'
512
+ }
513
+
514
+ build_brew_catalog_entries() {
515
+ {
516
+ brew formulae 2>/dev/null || true
517
+ brew casks 2>/dev/null || true
518
+ } |
519
+ awk 'NF >= 1 && $1 != "" { print $1 "\t-" }' |
520
+ awk -F'\t' '!seen[$1]++'
521
+ }
522
+
523
+ ensure_search_catalog_cache() {
524
+ local manager="$1"
525
+ local cache_fingerprint_value
526
+ local cache_key
527
+ local cache_file
528
+ local output_tmp
529
+
530
+ initialize_cache_root
531
+
532
+ cache_fingerprint_value="$(cache_search_catalog_fingerprint "${manager}")"
533
+ cache_key="$(cache_search_catalog_key "${manager}" "${cache_fingerprint_value}")"
534
+ cache_file="$(cache_path_for_key "${cache_key}")"
535
+
536
+ if [[ -s "${cache_file}" ]]; then
537
+ return 0
538
+ fi
539
+
540
+ output_tmp="$(mktemp "${SESSION_TMP_ROOT}/search-catalog.XXXXXX")"
541
+
542
+ case "${manager}" in
543
+ apt)
544
+ build_apt_catalog_entries >"${output_tmp}" || true
545
+ ;;
546
+ brew)
547
+ build_brew_catalog_entries >"${output_tmp}" || true
548
+ ;;
549
+ *)
550
+ rm -f "${output_tmp}"
551
+ return 1
552
+ ;;
553
+ esac
554
+
555
+ if [[ -s "${output_tmp}" ]]; then
556
+ cache_store_key_from_file "${cache_key}" "${cache_fingerprint_value}" "${output_tmp}"
557
+ rm -f "${output_tmp}"
558
+ return 0
559
+ fi
560
+
561
+ rm -f "${output_tmp}"
562
+ return 1
563
+ }
564
+
565
+ search_entries_from_catalog_cache() {
566
+ local manager="$1"
567
+ local query="$2"
568
+ local cache_fingerprint_value
569
+ local cache_key
570
+ local cache_file
571
+
572
+ initialize_cache_root
573
+
574
+ cache_fingerprint_value="$(cache_search_catalog_fingerprint "${manager}")"
575
+ cache_key="$(cache_search_catalog_key "${manager}" "${cache_fingerprint_value}")"
576
+ cache_file="$(cache_path_for_key "${cache_key}")"
577
+
578
+ if [[ ! -s "${cache_file}" ]]; then
579
+ return 1
580
+ fi
581
+
582
+ awk -F'\t' -v query="${query}" '
583
+ BEGIN {
584
+ normalized = tolower(query)
585
+ token_count = split(normalized, raw_tokens, /[[:space:]]+/)
586
+ token_index = 0
587
+ for (i = 1; i <= token_count; i++) {
588
+ if (raw_tokens[i] != "") {
589
+ token_index++
590
+ tokens[token_index] = raw_tokens[i]
591
+ }
592
+ }
593
+ }
594
+ {
595
+ haystack = tolower($1 " " $2)
596
+ matched = 1
597
+ for (i = 1; i <= token_index; i++) {
598
+ if (index(haystack, tokens[i]) == 0) {
599
+ matched = 0
600
+ break
601
+ }
602
+ }
603
+ if (matched) {
604
+ desc = $2
605
+ if (desc == "") {
606
+ desc = "-"
607
+ }
608
+ print $1 "\t" desc
609
+ }
610
+ }
611
+ ' "${cache_file}" || return 1
612
+ }
613
+
33
614
  initialize_session_tmp_root() {
34
615
  if [[ -n "${SESSION_TMP_ROOT}" ]]; then
35
616
  return
36
617
  fi
37
618
 
38
- SESSION_TMP_ROOT="$(mktemp -d "${TMP_ROOT}/session.XXXXXX")"
619
+ if [[ -n "${FPF_SESSION_TMP_ROOT:-}" ]]; then
620
+ SESSION_TMP_ROOT="${FPF_SESSION_TMP_ROOT}"
621
+ mkdir -p "${SESSION_TMP_ROOT}"
622
+ else
623
+ SESSION_TMP_ROOT="$(mktemp -d "${TMP_ROOT}/session.XXXXXX")"
624
+ fi
39
625
  HELP_FILE="${SESSION_TMP_ROOT}/help"
40
626
  KBINDS_FILE="${SESSION_TMP_ROOT}/keybinds"
41
627
  }
42
628
 
629
+ run_preview_manager_output() {
630
+ local manager="$1"
631
+ local package="$2"
632
+
633
+ case "${manager}" in
634
+ apt)
635
+ apt-cache show "${package}" 2>/dev/null || true
636
+ printf "\n"
637
+ dpkg -L "${package}" 2>/dev/null || true
638
+ ;;
639
+ dnf)
640
+ dnf info "${package}" 2>/dev/null || true
641
+ printf "\n"
642
+ rpm -ql "${package}" 2>/dev/null || true
643
+ ;;
644
+ pacman)
645
+ pacman -Si "${package}" 2>/dev/null || true
646
+ printf "\n"
647
+ pacman -Fl "${package}" 2>/dev/null | awk '{print $2}' || true
648
+ ;;
649
+ zypper)
650
+ zypper --non-interactive info "${package}" 2>/dev/null || true
651
+ ;;
652
+ emerge)
653
+ emerge --search --color=n "${package}" 2>/dev/null || true
654
+ ;;
655
+ brew)
656
+ brew info "${package}" 2>/dev/null || true
657
+ ;;
658
+ winget)
659
+ winget show --id "${package}" --exact --source winget --accept-source-agreements --disable-interactivity 2>/dev/null || true
660
+ ;;
661
+ choco)
662
+ choco info "${package}" 2>/dev/null || true
663
+ ;;
664
+ scoop)
665
+ scoop info "${package}" 2>/dev/null || true
666
+ ;;
667
+ snap)
668
+ snap info "${package}" 2>/dev/null || true
669
+ ;;
670
+ flatpak)
671
+ flatpak info "${package}" 2>/dev/null || flatpak remote-info flathub "${package}" 2>/dev/null || true
672
+ ;;
673
+ npm)
674
+ npm view "${package}" 2>/dev/null || true
675
+ ;;
676
+ bun)
677
+ bun info "${package}" 2>/dev/null || npm view "${package}" 2>/dev/null || true
678
+ ;;
679
+ esac
680
+ }
681
+
682
+ run_preview_item_action() {
683
+ local manager="$1"
684
+ local package="$2"
685
+ local cache_dir="${SESSION_TMP_ROOT}/preview-cache"
686
+ local cache_key=""
687
+ local cache_file=""
688
+ local temp_file=""
689
+
690
+ [[ -n "${manager}" && -n "${package}" ]] || return 0
691
+
692
+ mkdir -p "${cache_dir}"
693
+ cache_key="$(cache_cksum "${manager}|${package}")"
694
+ cache_file="${cache_dir}/${manager}.${cache_key}.txt"
695
+
696
+ if [[ -f "${cache_file}" ]]; then
697
+ cat "${cache_file}"
698
+ return 0
699
+ fi
700
+
701
+ temp_file="$(mktemp "${SESSION_TMP_ROOT}/preview.XXXXXX")"
702
+ run_preview_manager_output "${manager}" "${package}" >"${temp_file}"
703
+ mv -f "${temp_file}" "${cache_file}"
704
+ cat "${cache_file}"
705
+ }
706
+
43
707
  cleanup_session_tmp_root() {
44
708
  if [[ -n "${SESSION_TMP_ROOT}" && -d "${SESSION_TMP_ROOT}" ]]; then
45
709
  rm -rf "${SESSION_TMP_ROOT}"
@@ -124,6 +788,125 @@ manager_command_ready() {
124
788
  esac
125
789
  }
126
790
 
791
+ flatpak_has_any_remotes() {
792
+ if ! manager_command_ready flatpak; then
793
+ return 1
794
+ fi
795
+
796
+ if flatpak remotes --columns=name 2>/dev/null | awk 'NF > 0 { found=1; exit } END { exit (found ? 0 : 1) }'; then
797
+ return 0
798
+ fi
799
+
800
+ if flatpak remote-list --columns=name 2>/dev/null | awk 'NF > 0 { found=1; exit } END { exit (found ? 0 : 1) }'; then
801
+ return 0
802
+ fi
803
+
804
+ return 1
805
+ }
806
+
807
+ winget_has_default_source() {
808
+ if ! manager_command_ready winget; then
809
+ return 1
810
+ fi
811
+
812
+ if winget source list 2>/dev/null | awk '
813
+ {
814
+ line=$0
815
+ sub(/^[[:space:]]+/, "", line)
816
+ if (line == "") {
817
+ next
818
+ }
819
+ split(line, cols, /[[:space:]]+/)
820
+ if (tolower(cols[1]) == "winget") {
821
+ found=1
822
+ exit
823
+ }
824
+ }
825
+ END { exit (found ? 0 : 1) }
826
+ '; then
827
+ return 0
828
+ fi
829
+
830
+ return 1
831
+ }
832
+
833
+ choco_has_any_sources() {
834
+ if ! manager_command_ready choco; then
835
+ return 1
836
+ fi
837
+
838
+ if choco source list --limit-output 2>/dev/null | awk -F'|' 'NF >= 2 && $1 != "" { found=1; exit } END { exit (found ? 0 : 1) }'; then
839
+ return 0
840
+ fi
841
+
842
+ return 1
843
+ }
844
+
845
+ scoop_has_any_buckets() {
846
+ if ! manager_command_ready scoop; then
847
+ return 1
848
+ fi
849
+
850
+ if scoop bucket list 2>/dev/null | awk '
851
+ {
852
+ line=$0
853
+ sub(/^[[:space:]]+/, "", line)
854
+ if (line == "") {
855
+ next
856
+ }
857
+ if (tolower(line) ~ /^name[[:space:]]+/) {
858
+ next
859
+ }
860
+ if (line ~ /^[-[:space:]]+$/) {
861
+ next
862
+ }
863
+ split(line, cols, /[[:space:]]+/)
864
+ if (length(cols[2]) > 0 && (cols[2] ~ /^https?:\/\// || cols[2] ~ /^git@/ || cols[2] ~ /^ssh:\/\// || cols[2] ~ /^file:\/\// || cols[2] ~ /^\//)) {
865
+ found=1
866
+ exit
867
+ }
868
+ }
869
+ END { exit (found ? 0 : 1) }
870
+ '; then
871
+ return 0
872
+ fi
873
+
874
+ return 1
875
+ }
876
+
877
+ manager_no_query_setup_message() {
878
+ local manager="$1"
879
+
880
+ case "${manager}" in
881
+ flatpak)
882
+ if ! flatpak_has_any_remotes; then
883
+ printf "%s" "Flatpak has no remotes configured. Add Flathub with: flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo"
884
+ return 0
885
+ fi
886
+ ;;
887
+ winget)
888
+ if ! winget_has_default_source; then
889
+ printf "%s" "WinGet source 'winget' is not configured. Restore it with: winget source reset --force"
890
+ return 0
891
+ fi
892
+ ;;
893
+ choco)
894
+ if ! choco_has_any_sources; then
895
+ printf "%s" "Chocolatey has no package sources configured. Add the default source with: choco source add -n=chocolatey -s=https://community.chocolatey.org/api/v2/"
896
+ return 0
897
+ fi
898
+ ;;
899
+ scoop)
900
+ if ! scoop_has_any_buckets; then
901
+ printf "%s" "Scoop has no buckets configured. Add the default bucket with: scoop bucket add main"
902
+ return 0
903
+ fi
904
+ ;;
905
+ esac
906
+
907
+ return 1
908
+ }
909
+
127
910
  manager_can_install_fzf() {
128
911
  local manager="$1"
129
912
  case "${manager}" in
@@ -214,7 +997,7 @@ ensure_fzf() {
214
997
  local candidates=()
215
998
  local manager
216
999
 
217
- if command_exists fzf; then
1000
+ if [[ "${FPF_TEST_FORCE_FZF_MISSING:-0}" != "1" ]] && command_exists fzf; then
218
1001
  return
219
1002
  fi
220
1003
 
@@ -533,6 +1316,18 @@ parse_args() {
533
1316
  --feed-search)
534
1317
  ACTION="feed-search"
535
1318
  ;;
1319
+ --ipc-reload)
1320
+ ACTION="ipc-reload"
1321
+ ;;
1322
+ --ipc-query-notify)
1323
+ ACTION="ipc-query-notify"
1324
+ ;;
1325
+ --preview-item)
1326
+ ACTION="preview-item"
1327
+ ;;
1328
+ --bun-refresh-worker)
1329
+ ACTION="bun-refresh-worker"
1330
+ ;;
536
1331
  -ap|--apt)
537
1332
  MANAGER_OVERRIDE="apt"
538
1333
  ;;
@@ -806,7 +1601,59 @@ exact_match_entry() {
806
1601
  done < <(exact_query_candidates "${query}" | awk '!seen[$0]++')
807
1602
  }
808
1603
 
809
- manager_search_entries() {
1604
+ manager_bun_search_entries_strict() {
1605
+ local query="$1"
1606
+ local effective_query="${query}"
1607
+ local line_limit=0
1608
+ local query_line_limit=40
1609
+ local effective_limit=0
1610
+ local bun_search_file
1611
+
1612
+ if [[ -z "${query}" ]]; then
1613
+ line_limit="${FPF_NO_QUERY_RESULT_LIMIT:-120}"
1614
+ else
1615
+ query_line_limit="${FPF_QUERY_PER_MANAGER_LIMIT:-40}"
1616
+ fi
1617
+
1618
+ if ! [[ "${line_limit}" =~ ^[0-9]+$ ]]; then
1619
+ line_limit=0
1620
+ fi
1621
+
1622
+ if ! [[ "${query_line_limit}" =~ ^[0-9]+$ ]]; then
1623
+ query_line_limit=40
1624
+ fi
1625
+
1626
+ if [[ "${line_limit}" -gt 0 ]]; then
1627
+ effective_limit="${line_limit}"
1628
+ else
1629
+ effective_limit="${query_line_limit}"
1630
+ fi
1631
+
1632
+ if [[ -z "${effective_query}" ]]; then
1633
+ effective_query="aa"
1634
+ fi
1635
+
1636
+ bun_search_file="$(mktemp "${SESSION_TMP_ROOT}/bun-search-strict.XXXXXX")"
1637
+ if ! bun search "${effective_query}" >"${bun_search_file}" 2>/dev/null; then
1638
+ rm -f "${bun_search_file}"
1639
+ return 1
1640
+ fi
1641
+
1642
+ {
1643
+ awk 'NR > 1 && NF > 0 { name=$1; $1=""; sub(/^[[:space:]]+/, "", $0); if ($0 == "") $0="-"; print name "\t" $0 }' "${bun_search_file}"
1644
+ exact_match_entry "bun" "${query}"
1645
+ } | awk -F'\t' 'NF >= 1 { if ($2 == "") $2 = "-"; print $1 "\t" $2 }' | awk -F'\t' '!seen[$1]++' | {
1646
+ if [[ "${effective_limit}" -gt 0 ]]; then
1647
+ awk -v limit="${effective_limit}" 'NR <= limit'
1648
+ else
1649
+ cat
1650
+ fi
1651
+ }
1652
+
1653
+ rm -f "${bun_search_file}"
1654
+ }
1655
+
1656
+ manager_search_entries_uncached() {
810
1657
  local manager="$1"
811
1658
  local query="$2"
812
1659
  local effective_query="${query}"
@@ -856,8 +1703,12 @@ manager_search_entries() {
856
1703
 
857
1704
  case "${manager}" in
858
1705
  apt)
859
- apt-cache search -- "${effective_query}" 2>/dev/null |
860
- awk -F' - ' '{ name=$1; desc=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", name); if (desc == "") desc="-"; print name "\t" desc }'
1706
+ if ensure_search_catalog_cache "${manager}"; then
1707
+ search_entries_from_catalog_cache "${manager}" "${effective_query}" || true
1708
+ else
1709
+ apt-cache search -- "${effective_query}" 2>/dev/null |
1710
+ awk -F' - ' '{ name=$1; desc=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", name); if (desc == "") desc="-"; print name "\t" desc }'
1711
+ fi
861
1712
  ;;
862
1713
  dnf)
863
1714
  local pattern="*"
@@ -914,11 +1765,18 @@ manager_search_entries() {
914
1765
  '
915
1766
  ;;
916
1767
  brew)
917
- {
918
- brew search "${effective_query}" 2>/dev/null |
919
- awk 'NF > 0 && $1 != "==>" { print $1 "\t-" }'
920
- exact_match_entry "${manager}" "${query}"
921
- }
1768
+ if ensure_search_catalog_cache "${manager}"; then
1769
+ {
1770
+ search_entries_from_catalog_cache "${manager}" "${effective_query}" || true
1771
+ exact_match_entry "${manager}" "${query}"
1772
+ }
1773
+ else
1774
+ {
1775
+ brew search "${effective_query}" 2>/dev/null |
1776
+ awk 'NF > 0 && $1 != "==>" { print $1 "\t-" }'
1777
+ exact_match_entry "${manager}" "${query}"
1778
+ }
1779
+ fi
922
1780
  ;;
923
1781
  winget)
924
1782
  winget search "${effective_query}" --source winget --accept-source-agreements --disable-interactivity 2>/dev/null |
@@ -1017,6 +1875,83 @@ manager_search_entries() {
1017
1875
  } || true
1018
1876
  }
1019
1877
 
1878
+ manager_search_entries() {
1879
+ local manager="$1"
1880
+ local query="$2"
1881
+ local query_cache_enabled="${FPF_ENABLE_QUERY_CACHE:-0}"
1882
+ local bun_cache_ttl="${FPF_BUN_QUERY_CACHE_TTL:-900}"
1883
+ local flags
1884
+ local key
1885
+ local fingerprint
1886
+ local cache_file
1887
+ local output_tmp
1888
+ local generation
1889
+ local should_async_refresh=0
1890
+
1891
+ initialize_cache_root
1892
+
1893
+ flags="$(query_cache_flags)"
1894
+
1895
+ if ! [[ "${bun_cache_ttl}" =~ ^[0-9]+$ ]]; then
1896
+ bun_cache_ttl=900
1897
+ fi
1898
+
1899
+ if [[ "${manager}" == "bun" ]]; then
1900
+ query_cache_enabled="1"
1901
+ fi
1902
+
1903
+ key="$(cache_query_key "${manager}" "${query}" "${flags}")"
1904
+ fingerprint="$(cache_fingerprint "${manager}" "${query}" "${flags}")"
1905
+ cache_file="$(cache_path_for_key "${key}")"
1906
+
1907
+ if [[ "${manager}" == "bun" && ( "${ACTION}" == "feed-search" || "${ACTION}" == "ipc-query-notify" ) ]]; then
1908
+ if [[ -s "${cache_file}" ]]; then
1909
+ cache_emit_query_rows_if_valid "${cache_file}" || true
1910
+ fi
1911
+
1912
+ if cache_is_fresh_with_ttl "${key}" "${bun_cache_ttl}"; then
1913
+ return
1914
+ fi
1915
+
1916
+ generation="$(bun_generation_next "${key}")"
1917
+ if [[ -n "${FZF_PORT:-}" && "${FPF_BUN_SKIP_REFRESH_SCHEDULE:-0}" != "1" ]]; then
1918
+ should_async_refresh=1
1919
+ fi
1920
+
1921
+ if [[ "${FPF_BUN_TEST_SYNC_REFRESH:-0}" == "1" ]]; then
1922
+ should_async_refresh=0
1923
+ fi
1924
+
1925
+ if [[ "${should_async_refresh}" -eq 1 ]]; then
1926
+ start_bun_refresh_worker_async "${manager}" "${query}" "${flags}" "${key}" "${fingerprint}" "${generation}" "${FPF_IPC_FALLBACK_FILE:-}" "${FPF_IPC_MANAGER_OVERRIDE:-}"
1927
+ return
1928
+ fi
1929
+
1930
+ bun_run_refresh_worker "${manager}" "${query}" "${flags}" "${key}" "${fingerprint}" "${generation}" || true
1931
+ if [[ -s "${cache_file}" ]]; then
1932
+ cache_emit_query_rows_if_valid "${cache_file}" || true
1933
+ fi
1934
+ return
1935
+ fi
1936
+
1937
+ if [[ "${query_cache_enabled}" == "1" && -s "${cache_file}" ]]; then
1938
+ if [[ "${manager}" != "bun" ]] || cache_is_fresh_with_ttl "${key}" "${bun_cache_ttl}"; then
1939
+ cat "${cache_file}"
1940
+ return
1941
+ fi
1942
+ fi
1943
+
1944
+ output_tmp="$(mktemp "${SESSION_TMP_ROOT}/query-cache.XXXXXX")"
1945
+ manager_search_entries_uncached "${manager}" "${query}" >"${output_tmp}" || true
1946
+
1947
+ if [[ "${query_cache_enabled}" == "1" ]]; then
1948
+ cache_store_key_from_file "${key}" "${fingerprint}" "${output_tmp}"
1949
+ fi
1950
+
1951
+ cat "${output_tmp}"
1952
+ rm -f "${output_tmp}"
1953
+ }
1954
+
1020
1955
  manager_installed_entries() {
1021
1956
  local manager="$1"
1022
1957
 
@@ -1137,34 +2072,35 @@ manager_installed_entries() {
1137
2072
  ;;
1138
2073
  bun)
1139
2074
  {
1140
- if bun pm ls --global >/dev/null 2>&1; then
1141
- bun pm ls --global 2>/dev/null |
1142
- awk '
1143
- NR > 1 {
1144
- line = $0
1145
- gsub(/\r/, "", line)
1146
- sub(/^[[:space:]]*[^[:alnum:]@]*/, "", line)
1147
- sub(/^[[:space:]]+/, "", line)
1148
- sub(/[[:space:]]+$/, "", line)
1149
-
1150
- if (line == "") next
1151
- if (line ~ /[[:space:]]node_modules([[:space:]]|$)/) next
1152
-
1153
- split(line, fields, /[[:space:]]+/)
1154
- pkg = fields[1]
1155
- if (pkg == "") next
1156
-
1157
- pkg_copy = pkg
1158
- at_count = gsub(/@/, "@", pkg_copy)
1159
- if ((substr(pkg, 1, 1) == "@" && at_count >= 2) || (substr(pkg, 1, 1) != "@" && at_count >= 1)) {
1160
- sub(/@[^@[:space:]]*$/, "", pkg)
1161
- }
2075
+ local bun_pm_file
2076
+ bun_pm_file="$(mktemp "${SESSION_TMP_ROOT}/bun-pm.XXXXXX")"
2077
+ if bun pm ls --global >"${bun_pm_file}" 2>/dev/null; then
2078
+ awk '
2079
+ NR > 1 {
2080
+ line = $0
2081
+ gsub(/\r/, "", line)
2082
+ sub(/^[[:space:]]*[^[:alnum:]@]*/, "", line)
2083
+ sub(/^[[:space:]]+/, "", line)
2084
+ sub(/[[:space:]]+$/, "", line)
2085
+
2086
+ if (line == "") next
2087
+ if (line ~ /[[:space:]]node_modules([[:space:]]|$)/) next
2088
+
2089
+ split(line, fields, /[[:space:]]+/)
2090
+ pkg = fields[1]
2091
+ if (pkg == "") next
2092
+
2093
+ pkg_copy = pkg
2094
+ at_count = gsub(/@/, "@", pkg_copy)
2095
+ if ((substr(pkg, 1, 1) == "@" && at_count >= 2) || (substr(pkg, 1, 1) != "@" && at_count >= 1)) {
2096
+ sub(/@[^@[:space:]]*$/, "", pkg)
2097
+ }
1162
2098
 
1163
- if (pkg != "") {
1164
- print pkg "\tglobal"
1165
- }
2099
+ if (pkg != "") {
2100
+ print pkg "\tglobal"
1166
2101
  }
1167
- '
2102
+ }
2103
+ ' "${bun_pm_file}"
1168
2104
  elif command_exists npm; then
1169
2105
  npm ls -g --depth=0 --parseable 2>/dev/null |
1170
2106
  awk '
@@ -1188,6 +2124,7 @@ manager_installed_entries() {
1188
2124
  }
1189
2125
  '
1190
2126
  fi
2127
+ rm -f "${bun_pm_file}"
1191
2128
  } || true
1192
2129
  ;;
1193
2130
  esac | sort -u || true
@@ -1202,8 +2139,15 @@ manager_installed_names_cached() {
1202
2139
  local manager="$1"
1203
2140
  local output_file="$2"
1204
2141
  local cache_enabled="${FPF_DISABLE_INSTALLED_CACHE:-0}"
1205
- local cache_dir="${TMP_ROOT}/cache"
1206
- local cache_file="${cache_dir}/installed.${manager}"
2142
+ local cache_key
2143
+ local cache_file
2144
+ local cache_fingerprint_value
2145
+
2146
+ initialize_cache_root
2147
+
2148
+ cache_key="$(cache_catalog_key "${manager}")"
2149
+ cache_file="$(cache_path_for_key "${cache_key}")"
2150
+ cache_fingerprint_value="$(cache_fingerprint "${manager}" "" "installed")"
1207
2151
 
1208
2152
  if [[ "${cache_enabled}" != "1" && -s "${cache_file}" ]]; then
1209
2153
  cp "${cache_file}" "${output_file}"
@@ -1213,47 +2157,82 @@ manager_installed_names_cached() {
1213
2157
  manager_installed_names "${manager}" >"${output_file}" 2>/dev/null || true
1214
2158
 
1215
2159
  if [[ "${cache_enabled}" != "1" && -s "${output_file}" ]]; then
1216
- mkdir -p "${cache_dir}"
1217
- cp "${output_file}" "${cache_file}"
2160
+ cache_store_key_from_file "${cache_key}" "${cache_fingerprint_value}" "${output_file}"
1218
2161
  fi
1219
2162
  }
1220
2163
 
1221
2164
  mark_installed_packages() {
1222
- local manager="$1"
1223
- local source_file="$2"
1224
- local output_file="$3"
2165
+ local source_file="$1"
2166
+ local output_file="$2"
1225
2167
  local installed_file
2168
+ local installed_map_file
2169
+ local manager
1226
2170
 
1227
2171
  if [[ "${FPF_SKIP_INSTALLED_MARKERS:-0}" == "1" ]]; then
1228
2172
  awk -F'\t' '
1229
2173
  {
1230
- desc = $2
2174
+ desc = $3
1231
2175
  if (desc == "") desc = "-"
1232
- print $1 "\t " desc
2176
+ print $1 "\t" $2 "\t " desc
1233
2177
  }
1234
2178
  ' "${source_file}" >"${output_file}"
1235
2179
  return
1236
2180
  fi
1237
2181
 
1238
- installed_file="$(mktemp "${SESSION_TMP_ROOT}/installed.XXXXXX")"
1239
- manager_installed_names_cached "${manager}" "${installed_file}"
2182
+ installed_map_file="$(mktemp "${SESSION_TMP_ROOT}/installed-map.XXXXXX")"
2183
+ : >"${installed_map_file}"
2184
+
2185
+ while IFS= read -r manager; do
2186
+ [[ -n "${manager}" ]] || continue
2187
+ installed_file="$(mktemp "${SESSION_TMP_ROOT}/installed.${manager}.XXXXXX")"
2188
+ manager_installed_names_cached "${manager}" "${installed_file}"
2189
+ if [[ -s "${installed_file}" ]]; then
2190
+ awk -F'\t' -v mgr="${manager}" 'NF > 0 && $1 != "" { print mgr "\t" $1 }' "${installed_file}" >>"${installed_map_file}"
2191
+ fi
2192
+ rm -f "${installed_file}"
2193
+ done < <(awk -F'\t' 'NF >= 1 && $1 != "" { print $1 }' "${source_file}" | awk '!seen[$0]++')
1240
2194
 
1241
2195
  awk -F'\t' '
1242
2196
  FILENAME == ARGV[1] {
1243
- if ($1 != "") {
1244
- installed[$1] = 1
2197
+ key = $1 "\t" $2
2198
+ if ($1 != "" && $2 != "") {
2199
+ installed[key] = 1
1245
2200
  }
1246
2201
  next
1247
2202
  }
1248
2203
  {
1249
- mark = (installed[$1] ? "* " : " ")
1250
- desc = $2
2204
+ key = $1 "\t" $2
2205
+ mark = (installed[key] ? "* " : " ")
2206
+ desc = $3
1251
2207
  if (desc == "") desc = "-"
1252
- print $1 "\t" mark desc
2208
+ print $1 "\t" $2 "\t" mark desc
1253
2209
  }
1254
- ' "${installed_file}" "${source_file}" >"${output_file}"
2210
+ ' "${installed_map_file}" "${source_file}" >"${output_file}"
2211
+
2212
+ rm -f "${installed_map_file}"
2213
+ }
2214
+
2215
+ merge_search_display_rows() {
2216
+ local source_file="$1"
2217
+ local output_file="$2"
2218
+
2219
+ if [[ ! -s "${source_file}" ]]; then
2220
+ : >"${output_file}"
2221
+ return
2222
+ fi
1255
2223
 
1256
- rm -f "${installed_file}"
2224
+ sort -t $'\t' -k1,1 -k2,2 -k3,3 "${source_file}" |
2225
+ awk -F'\t' '
2226
+ NF >= 2 {
2227
+ key = $1 "\t" $2
2228
+ if (seen[key]++) {
2229
+ next
2230
+ }
2231
+ desc = $3
2232
+ if (desc == "") desc = "-"
2233
+ print $1 "\t" $2 "\t" desc
2234
+ }
2235
+ ' >"${output_file}"
1257
2236
  }
1258
2237
 
1259
2238
  collect_search_display_rows() {
@@ -1270,6 +2249,8 @@ collect_search_display_rows() {
1270
2249
  local manager
1271
2250
  local part_file
1272
2251
  local gather_pid
2252
+ local merged_source_file
2253
+ local merged_marked_file
1273
2254
 
1274
2255
  for manager in "${managers[@]-}"; do
1275
2256
  part_file="$(mktemp "${SESSION_TMP_ROOT}/part.XXXXXX")"
@@ -1277,19 +2258,17 @@ collect_search_display_rows() {
1277
2258
 
1278
2259
  (
1279
2260
  local_source_file="$(mktemp "${SESSION_TMP_ROOT}/source.XXXXXX")"
1280
- local_marked_file="$(mktemp "${SESSION_TMP_ROOT}/marked.XXXXXX")"
1281
2261
  manager_search_entries "${manager}" "${query}" >"${local_source_file}" || true
1282
2262
  if [[ -s "${local_source_file}" ]]; then
1283
- mark_installed_packages "${manager}" "${local_source_file}" "${local_marked_file}"
1284
2263
  awk -F'\t' -v mgr="${manager}" '
1285
2264
  NF >= 1 {
1286
2265
  desc = $2
1287
2266
  if (desc == "") desc = "-"
1288
2267
  print mgr "\t" $1 "\t" desc
1289
2268
  }
1290
- ' "${local_marked_file}" >"${part_file}"
2269
+ ' "${local_source_file}" >"${part_file}"
1291
2270
  fi
1292
- rm -f "${local_source_file}" "${local_marked_file}"
2271
+ rm -f "${local_source_file}"
1293
2272
  ) &
1294
2273
  gather_pids+=("$!")
1295
2274
  done
@@ -1298,15 +2277,19 @@ collect_search_display_rows() {
1298
2277
  wait "${gather_pid}" || true
1299
2278
  done
1300
2279
 
2280
+ merged_source_file="$(mktemp "${SESSION_TMP_ROOT}/merged-source.XXXXXX")"
2281
+ merged_marked_file="$(mktemp "${SESSION_TMP_ROOT}/merged-marked.XXXXXX")"
2282
+
1301
2283
  for part_file in "${part_files[@]-}"; do
1302
2284
  if [[ -s "${part_file}" ]]; then
1303
- cat "${part_file}" >>"${output_file}"
2285
+ cat "${part_file}" >>"${merged_source_file}"
1304
2286
  fi
1305
2287
  rm -f "${part_file}"
1306
2288
  done
1307
2289
 
1308
- if [[ -s "${output_file}" ]]; then
1309
- sort -u "${output_file}" -o "${output_file}"
2290
+ if [[ -s "${merged_source_file}" ]]; then
2291
+ merge_search_display_rows "${merged_source_file}" "${merged_marked_file}"
2292
+ mark_installed_packages "${merged_marked_file}" "${output_file}"
1310
2293
  rank_display_rows_by_query "${query}" "${output_file}"
1311
2294
 
1312
2295
  if [[ -n "${query}" && "${query_limit}" =~ ^[0-9]+$ && "${query_limit}" -gt 0 ]]; then
@@ -1314,6 +2297,8 @@ collect_search_display_rows() {
1314
2297
  mv "${output_file}.limited" "${output_file}"
1315
2298
  fi
1316
2299
  fi
2300
+
2301
+ rm -f "${merged_source_file}" "${merged_marked_file}"
1317
2302
  }
1318
2303
 
1319
2304
  build_dynamic_reload_command() {
@@ -1336,10 +2321,195 @@ build_dynamic_reload_command() {
1336
2321
  fi
1337
2322
 
1338
2323
  if [[ -n "${manager_override}" ]]; then
1339
- printf 'q={q}; if [ ${#q} -lt %s ]; then cat %q; else sleep %s; FPF_SKIP_INSTALLED_MARKERS=1 %q --feed-search --manager %q -- "$q" 2>/dev/null || cat %q; fi' "${min_chars}" "${fallback_file}" "${reload_debounce}" "${script_path}" "${manager_override}" "${fallback_file}"
2324
+ printf 'q={q}; if [ ${#q} -lt %s ]; then cat %q; else sleep %s; FPF_SKIP_INSTALLED_MARKERS=1 FPF_IPC_MANAGER_OVERRIDE=%q FPF_IPC_FALLBACK_FILE=%q %q --feed-search --manager %q -- "$q" 2>/dev/null || cat %q; fi' "${min_chars}" "${fallback_file}" "${reload_debounce}" "${manager_override}" "${fallback_file}" "${script_path}" "${manager_override}" "${fallback_file}"
2325
+ else
2326
+ printf 'q={q}; if [ ${#q} -lt %s ]; then cat %q; else sleep %s; FPF_SKIP_INSTALLED_MARKERS=1 FPF_IPC_MANAGER_OVERRIDE= FPF_IPC_FALLBACK_FILE=%q %q --feed-search -- "$q" 2>/dev/null || cat %q; fi' "${min_chars}" "${fallback_file}" "${reload_debounce}" "${fallback_file}" "${script_path}" "${fallback_file}"
2327
+ fi
2328
+ }
2329
+
2330
+ build_dynamic_reload_command_for_query() {
2331
+ local manager_override="$1"
2332
+ local fallback_file="$2"
2333
+ local query_value="$3"
2334
+ local script_path="${BASH_SOURCE[0]}"
2335
+ local min_chars="${FPF_RELOAD_MIN_CHARS:-2}"
2336
+ local reload_debounce="${FPF_RELOAD_DEBOUNCE:-0.12}"
2337
+
2338
+ if ! [[ "${min_chars}" =~ ^[0-9]+$ ]]; then
2339
+ min_chars=2
2340
+ fi
2341
+
2342
+ if ! [[ "${reload_debounce}" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
2343
+ reload_debounce=0.12
2344
+ fi
2345
+
2346
+ if [[ "${script_path}" != /* ]]; then
2347
+ script_path="$(pwd)/${script_path}"
2348
+ fi
2349
+
2350
+ if [[ ${#query_value} -lt ${min_chars} ]]; then
2351
+ printf 'cat %q' "${fallback_file}"
2352
+ return
2353
+ fi
2354
+
2355
+ if [[ -n "${manager_override}" ]]; then
2356
+ printf 'sleep %s; FPF_SKIP_INSTALLED_MARKERS=1 FPF_IPC_MANAGER_OVERRIDE=%q FPF_IPC_FALLBACK_FILE=%q %q --feed-search --manager %q -- %q 2>/dev/null || cat %q' "${reload_debounce}" "${manager_override}" "${fallback_file}" "${script_path}" "${manager_override}" "${query_value}" "${fallback_file}"
2357
+ else
2358
+ printf 'sleep %s; FPF_SKIP_INSTALLED_MARKERS=1 FPF_IPC_MANAGER_OVERRIDE= FPF_IPC_FALLBACK_FILE=%q %q --feed-search -- %q 2>/dev/null || cat %q' "${reload_debounce}" "${fallback_file}" "${script_path}" "${query_value}" "${fallback_file}"
2359
+ fi
2360
+ }
2361
+
2362
+ build_dynamic_reload_ipc_command() {
2363
+ local manager_override="$1"
2364
+ local fallback_file="$2"
2365
+ local script_path="${BASH_SOURCE[0]}"
2366
+
2367
+ if [[ "${script_path}" != /* ]]; then
2368
+ script_path="$(pwd)/${script_path}"
2369
+ fi
2370
+
2371
+ printf 'FPF_IPC_MANAGER_OVERRIDE=%q FPF_IPC_FALLBACK_FILE=%q %q --ipc-reload -- "{q}"' "${manager_override}" "${fallback_file}" "${script_path}"
2372
+ }
2373
+
2374
+ build_dynamic_query_notify_ipc_command() {
2375
+ local manager_override="$1"
2376
+ local fallback_file="$2"
2377
+ local script_path="${BASH_SOURCE[0]}"
2378
+
2379
+ if [[ "${script_path}" != /* ]]; then
2380
+ script_path="$(pwd)/${script_path}"
2381
+ fi
2382
+
2383
+ printf 'FPF_IPC_MANAGER_OVERRIDE=%q FPF_IPC_FALLBACK_FILE=%q %q --ipc-query-notify -- "{q}"' "${manager_override}" "${fallback_file}" "${script_path}"
2384
+ }
2385
+
2386
+ fzf_supports_listen() {
2387
+ local fzf_help
2388
+ fzf_help="$(fzf --help 2>&1 || true)"
2389
+ [[ "${fzf_help}" == *"--listen"* ]]
2390
+ }
2391
+
2392
+ send_fzf_listen_action() {
2393
+ local action_payload="$1"
2394
+ local port="${FZF_PORT:-}"
2395
+ local host="127.0.0.1"
2396
+ local payload_size
2397
+
2398
+ if ! [[ "${port}" =~ ^[0-9]+$ ]] || [[ "${port}" -le 0 || "${port}" -gt 65535 ]]; then
2399
+ return 1
2400
+ fi
2401
+
2402
+ payload_size="$(printf "%s" "${action_payload}" | wc -c | tr -d '[:space:]')"
2403
+
2404
+ if command_exists curl; then
2405
+ curl --silent --show-error --fail --max-time 2 \
2406
+ --request POST \
2407
+ --header 'Content-Type: text/plain' \
2408
+ --data-binary "${action_payload}" \
2409
+ "http://${host}:${port}" >/dev/null 2>&1
2410
+ return
2411
+ fi
2412
+
2413
+ if command_exists nc; then
2414
+ {
2415
+ printf 'POST / HTTP/1.1\r\n'
2416
+ printf 'Host: %s:%s\r\n' "${host}" "${port}"
2417
+ printf 'Content-Type: text/plain\r\n'
2418
+ printf 'Content-Length: %s\r\n' "${payload_size}"
2419
+ printf '\r\n'
2420
+ printf '%s' "${action_payload}"
2421
+ } | nc -w 2 "${host}" "${port}" >/dev/null 2>&1
2422
+ return
2423
+ fi
2424
+
2425
+ if exec 9<>"/dev/tcp/${host}/${port}" 2>/dev/null; then
2426
+ printf 'POST / HTTP/1.1\r\n' >&9
2427
+ printf 'Host: %s:%s\r\n' "${host}" "${port}" >&9
2428
+ printf 'Content-Type: text/plain\r\n' >&9
2429
+ printf 'Content-Length: %s\r\n' "${payload_size}" >&9
2430
+ printf '\r\n' >&9
2431
+ printf '%s' "${action_payload}" >&9
2432
+ exec 9>&-
2433
+ return 0
2434
+ fi
2435
+
2436
+ return 1
2437
+ }
2438
+
2439
+ send_fzf_prompt_action() {
2440
+ local prompt_text="$1"
2441
+ local action_payload
2442
+
2443
+ action_payload="change-prompt(${prompt_text})"
2444
+ send_fzf_listen_action "${action_payload}"
2445
+ }
2446
+
2447
+ run_ipc_reload_action() {
2448
+ local query="$1"
2449
+ local manager_override="${FPF_IPC_MANAGER_OVERRIDE:-}"
2450
+ local fallback_file="${FPF_IPC_FALLBACK_FILE:-}"
2451
+ local reload_command
2452
+ local action_payload
2453
+
2454
+ if [[ -z "${fallback_file}" || ! -r "${fallback_file}" ]]; then
2455
+ return 1
2456
+ fi
2457
+
2458
+ if [[ -n "${manager_override}" ]]; then
2459
+ manager_override="$(normalize_manager "${manager_override}")"
2460
+ manager_supported "${manager_override}" || return 1
2461
+ fi
2462
+
2463
+ reload_command="$(build_dynamic_reload_command_for_query "${manager_override}" "${fallback_file}" "${query}")"
2464
+ action_payload="reload(${reload_command})+change-prompt(Search> )"
2465
+ send_fzf_listen_action "${action_payload}"
2466
+ }
2467
+
2468
+ run_ipc_query_notify_action() {
2469
+ local query="$1"
2470
+ local manager_override="${FPF_IPC_MANAGER_OVERRIDE:-}"
2471
+ local fallback_file="${FPF_IPC_FALLBACK_FILE:-}"
2472
+ local min_chars="${FPF_RELOAD_MIN_CHARS:-2}"
2473
+ local target_manager=""
2474
+
2475
+ if ! [[ "${min_chars}" =~ ^[0-9]+$ ]]; then
2476
+ min_chars=2
2477
+ fi
2478
+
2479
+ if [[ ${#query} -lt ${min_chars} ]]; then
2480
+ send_fzf_prompt_action "Search> " || true
2481
+ return 0
2482
+ fi
2483
+
2484
+ if [[ -z "${fallback_file}" || ! -r "${fallback_file}" ]]; then
2485
+ return 1
2486
+ fi
2487
+
2488
+ if [[ -n "${manager_override}" ]]; then
2489
+ manager_override="$(normalize_manager "${manager_override}")"
2490
+ manager_supported "${manager_override}" || return 1
2491
+ target_manager="${manager_override}"
1340
2492
  else
1341
- printf 'q={q}; if [ ${#q} -lt %s ]; then cat %q; else sleep %s; FPF_SKIP_INSTALLED_MARKERS=1 %q --feed-search -- "$q" 2>/dev/null || cat %q; fi' "${min_chars}" "${fallback_file}" "${reload_debounce}" "${script_path}" "${fallback_file}"
2493
+ target_manager="bun"
2494
+ fi
2495
+
2496
+ if [[ "${target_manager}" != "bun" ]]; then
2497
+ send_fzf_prompt_action "Search> " || true
2498
+ return 0
2499
+ fi
2500
+
2501
+ if ! manager_command_ready bun; then
2502
+ send_fzf_prompt_action "Search> " || true
2503
+ return 0
1342
2504
  fi
2505
+
2506
+ send_fzf_prompt_action "Loading> " || true
2507
+
2508
+ FPF_IPC_MANAGER_OVERRIDE="${manager_override}" \
2509
+ FPF_IPC_FALLBACK_FILE="${fallback_file}" \
2510
+ manager_search_entries "bun" "${query}" >/dev/null 2>&1 || {
2511
+ send_fzf_prompt_action "Search> " || true
2512
+ }
1343
2513
  }
1344
2514
 
1345
2515
  manager_install() {
@@ -1572,9 +2742,15 @@ run_fuzzy_selector() {
1572
2742
  local input_file="$2"
1573
2743
  local header_line="$3"
1574
2744
  local reload_cmd="${4:-}"
2745
+ local reload_ipc_cmd="${5:-}"
2746
+ local script_path="${BASH_SOURCE[0]}"
1575
2747
  local preview_cmd
1576
2748
 
1577
- preview_cmd='bash -c '\''mgr="$1"; pkg="$2"; case "$mgr" in apt) apt-cache show "$pkg" 2>/dev/null; printf "\n"; dpkg -L "$pkg" 2>/dev/null ;; dnf) dnf info "$pkg" 2>/dev/null; printf "\n"; rpm -ql "$pkg" 2>/dev/null ;; pacman) pacman -Si "$pkg" 2>/dev/null; printf "\n"; pacman -Fl "$pkg" 2>/dev/null | awk "{print \$2}" ;; zypper) zypper --non-interactive info "$pkg" 2>/dev/null ;; emerge) emerge --search --color=n "$pkg" 2>/dev/null ;; brew) brew info "$pkg" 2>/dev/null ;; winget) winget show --id "$pkg" --exact --source winget --accept-source-agreements --disable-interactivity 2>/dev/null ;; choco) choco info "$pkg" 2>/dev/null ;; scoop) scoop info "$pkg" 2>/dev/null ;; snap) snap info "$pkg" 2>/dev/null ;; flatpak) flatpak info "$pkg" 2>/dev/null || flatpak remote-info flathub "$pkg" 2>/dev/null ;; npm) npm view "$pkg" 2>/dev/null ;; bun) bun info "$pkg" 2>/dev/null || npm view "$pkg" 2>/dev/null ;; esac'\'' _ {1} {2}'
2749
+ if [[ "${script_path}" != /* ]]; then
2750
+ script_path="$(pwd)/${script_path}"
2751
+ fi
2752
+
2753
+ preview_cmd="FPF_SESSION_TMP_ROOT=$(printf '%q' "${SESSION_TMP_ROOT}") ${script_path} --preview-item --manager {1} -- {2}"
1578
2754
 
1579
2755
  local -a fzf_args=()
1580
2756
  fzf_args=(-q "${query}" -m \
@@ -1597,8 +2773,14 @@ run_fuzzy_selector() {
1597
2773
  --bind=ctrl-n:next-selected,ctrl-b:prev-selected \
1598
2774
  --bind='focus:transform-preview-label:echo [{1}] {2}')
1599
2775
 
1600
- if [[ -n "${reload_cmd}" ]]; then
1601
- fzf_args+=(--bind="change:reload:${reload_cmd}")
2776
+ if [[ -n "${reload_ipc_cmd}" ]]; then
2777
+ fzf_args+=(--listen=0)
2778
+ fzf_args+=(--bind="change:execute-silent:${reload_ipc_cmd}")
2779
+ if [[ -n "${reload_cmd}" ]]; then
2780
+ fzf_args+=(--bind="ctrl-r:reload:${reload_cmd}")
2781
+ fi
2782
+ elif [[ -n "${reload_cmd}" ]]; then
2783
+ fzf_args+=(--bind="ctrl-r:reload:${reload_cmd}")
1602
2784
  fi
1603
2785
 
1604
2786
  fzf "${fzf_args[@]}" <"${input_file}"
@@ -1607,10 +2789,54 @@ run_fuzzy_selector() {
1607
2789
  main() {
1608
2790
  ensure_tmp_root
1609
2791
  initialize_session_tmp_root
1610
- trap cleanup_session_tmp_root EXIT
2792
+ if [[ -z "${FPF_SESSION_TMP_ROOT:-}" ]]; then
2793
+ trap cleanup_session_tmp_root EXIT
2794
+ fi
1611
2795
 
1612
2796
  parse_args "$@"
1613
2797
 
2798
+ if [[ "${ACTION}" == "ipc-reload" ]]; then
2799
+ run_ipc_reload_action "$(join_query)" || true
2800
+ exit 0
2801
+ fi
2802
+
2803
+ if [[ "${ACTION}" == "ipc-query-notify" ]]; then
2804
+ run_ipc_query_notify_action "$(join_query)" || true
2805
+ exit 0
2806
+ fi
2807
+
2808
+ if [[ "${ACTION}" == "preview-item" ]]; then
2809
+ local preview_manager="${MANAGER_OVERRIDE:-}"
2810
+ local preview_package
2811
+
2812
+ preview_package="$(join_query)"
2813
+ run_preview_item_action "${preview_manager}" "${preview_package}" || true
2814
+ exit 0
2815
+ fi
2816
+
2817
+ if [[ "${ACTION}" == "bun-refresh-worker" ]]; then
2818
+ local worker_manager="${MANAGER_OVERRIDE:-bun}"
2819
+ local worker_query
2820
+ local worker_flags
2821
+ local worker_key
2822
+ local worker_fingerprint
2823
+ local worker_generation
2824
+
2825
+ worker_query="$(join_query)"
2826
+ worker_flags="${FPF_BUN_REFRESH_FLAGS:-$(query_cache_flags)}"
2827
+ worker_key="${FPF_BUN_REFRESH_KEY:-$(cache_query_key "${worker_manager}" "${worker_query}" "${worker_flags}")}"
2828
+ worker_fingerprint="${FPF_BUN_REFRESH_FINGERPRINT:-$(cache_fingerprint "${worker_manager}" "${worker_query}" "${worker_flags}")}"
2829
+ worker_generation="${FPF_BUN_REFRESH_GENERATION:-0}"
2830
+
2831
+ if ! [[ "${worker_generation}" =~ ^[0-9]+$ ]] || [[ "${worker_generation}" -le 0 ]]; then
2832
+ exit 0
2833
+ fi
2834
+
2835
+ initialize_cache_root
2836
+ bun_run_refresh_worker "${worker_manager}" "${worker_query}" "${worker_flags}" "${worker_key}" "${worker_fingerprint}" "${worker_generation}" || true
2837
+ exit 0
2838
+ fi
2839
+
1614
2840
  if [[ "${ACTION}" == "version" ]]; then
1615
2841
  print_version
1616
2842
  exit 0
@@ -1739,6 +2965,15 @@ main() {
1739
2965
 
1740
2966
  if [[ ! -s "${display_file}" ]]; then
1741
2967
  rm -f "${display_file}"
2968
+
2969
+ if [[ "${ACTION}" == "search" && -z "${query}" && "${#managers[@]}" -eq 1 ]]; then
2970
+ local setup_message=""
2971
+ setup_message="$(manager_no_query_setup_message "${managers[0]}" || true)"
2972
+ if [[ -n "${setup_message}" ]]; then
2973
+ die "${setup_message}"
2974
+ fi
2975
+ fi
2976
+
1742
2977
  if [[ -n "${query}" ]]; then
1743
2978
  die "No packages found for ${manager_display} matching '${query}'. Try a broader query or --manager."
1744
2979
  fi
@@ -1762,14 +2997,18 @@ main() {
1762
2997
  esac
1763
2998
 
1764
2999
  local reload_cmd=""
3000
+ local reload_ipc_cmd=""
1765
3001
  if [[ "${ACTION}" == "search" && -z "${query}" ]]; then
1766
3002
  if dynamic_reload_enabled "${#managers[@]}"; then
1767
3003
  reload_cmd="$(build_dynamic_reload_command "${MANAGER_OVERRIDE}" "${display_file}")"
3004
+ if fzf_supports_listen; then
3005
+ reload_ipc_cmd="$(build_dynamic_query_notify_ipc_command "${MANAGER_OVERRIDE}" "${display_file}")"
3006
+ fi
1768
3007
  fi
1769
3008
  fi
1770
3009
 
1771
3010
  local selected
1772
- selected="$(run_fuzzy_selector "${query}" "${display_file}" "${header}" "${reload_cmd}" || true)"
3011
+ selected="$(run_fuzzy_selector "${query}" "${display_file}" "${header}" "${reload_cmd}" "${reload_ipc_cmd}" || true)"
1773
3012
 
1774
3013
  rm -f "${display_file}"
1775
3014
 
@@ -1824,7 +3063,7 @@ main() {
1824
3063
  if confirm_action "Install ${#selected_packages[@]} package(s) with ${selected_manager_display}?"; then
1825
3064
  for manager in "${unique_managers[@]-}"; do
1826
3065
  mgr_packages=()
1827
- for idx in "${!selected_packages[@]-}"; do
3066
+ for idx in "${!selected_packages[@]}"; do
1828
3067
  if [[ "${selected_managers[$idx]}" == "${manager}" ]]; then
1829
3068
  mgr_packages+=("${selected_packages[$idx]}")
1830
3069
  fi
@@ -1842,7 +3081,7 @@ main() {
1842
3081
  if confirm_action "Remove ${#selected_packages[@]} package(s) with ${selected_manager_display}?"; then
1843
3082
  for manager in "${unique_managers[@]-}"; do
1844
3083
  mgr_packages=()
1845
- for idx in "${!selected_packages[@]-}"; do
3084
+ for idx in "${!selected_packages[@]}"; do
1846
3085
  if [[ "${selected_managers[$idx]}" == "${manager}" ]]; then
1847
3086
  mgr_packages+=("${selected_packages[$idx]}")
1848
3087
  fi
@@ -1857,7 +3096,7 @@ main() {
1857
3096
  fi
1858
3097
  ;;
1859
3098
  list)
1860
- for idx in "${!selected_packages[@]-}"; do
3099
+ for idx in "${!selected_packages[@]}"; do
1861
3100
  printf "\n=== %s (%s) ===\n" "${selected_packages[$idx]}" "$(manager_label "${selected_managers[$idx]}")"
1862
3101
  manager_show_info "${selected_managers[$idx]}" "${selected_packages[$idx]}" || true
1863
3102
  done