fpf-cli 1.6.15 → 1.6.16

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 +1206 -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.16"
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,22 @@ 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
+
127
807
  manager_can_install_fzf() {
128
808
  local manager="$1"
129
809
  case "${manager}" in
@@ -214,7 +894,7 @@ ensure_fzf() {
214
894
  local candidates=()
215
895
  local manager
216
896
 
217
- if command_exists fzf; then
897
+ if [[ "${FPF_TEST_FORCE_FZF_MISSING:-0}" != "1" ]] && command_exists fzf; then
218
898
  return
219
899
  fi
220
900
 
@@ -533,6 +1213,18 @@ parse_args() {
533
1213
  --feed-search)
534
1214
  ACTION="feed-search"
535
1215
  ;;
1216
+ --ipc-reload)
1217
+ ACTION="ipc-reload"
1218
+ ;;
1219
+ --ipc-query-notify)
1220
+ ACTION="ipc-query-notify"
1221
+ ;;
1222
+ --preview-item)
1223
+ ACTION="preview-item"
1224
+ ;;
1225
+ --bun-refresh-worker)
1226
+ ACTION="bun-refresh-worker"
1227
+ ;;
536
1228
  -ap|--apt)
537
1229
  MANAGER_OVERRIDE="apt"
538
1230
  ;;
@@ -806,7 +1498,59 @@ exact_match_entry() {
806
1498
  done < <(exact_query_candidates "${query}" | awk '!seen[$0]++')
807
1499
  }
808
1500
 
809
- manager_search_entries() {
1501
+ manager_bun_search_entries_strict() {
1502
+ local query="$1"
1503
+ local effective_query="${query}"
1504
+ local line_limit=0
1505
+ local query_line_limit=40
1506
+ local effective_limit=0
1507
+ local bun_search_file
1508
+
1509
+ if [[ -z "${query}" ]]; then
1510
+ line_limit="${FPF_NO_QUERY_RESULT_LIMIT:-120}"
1511
+ else
1512
+ query_line_limit="${FPF_QUERY_PER_MANAGER_LIMIT:-40}"
1513
+ fi
1514
+
1515
+ if ! [[ "${line_limit}" =~ ^[0-9]+$ ]]; then
1516
+ line_limit=0
1517
+ fi
1518
+
1519
+ if ! [[ "${query_line_limit}" =~ ^[0-9]+$ ]]; then
1520
+ query_line_limit=40
1521
+ fi
1522
+
1523
+ if [[ "${line_limit}" -gt 0 ]]; then
1524
+ effective_limit="${line_limit}"
1525
+ else
1526
+ effective_limit="${query_line_limit}"
1527
+ fi
1528
+
1529
+ if [[ -z "${effective_query}" ]]; then
1530
+ effective_query="aa"
1531
+ fi
1532
+
1533
+ bun_search_file="$(mktemp "${SESSION_TMP_ROOT}/bun-search-strict.XXXXXX")"
1534
+ if ! bun search "${effective_query}" >"${bun_search_file}" 2>/dev/null; then
1535
+ rm -f "${bun_search_file}"
1536
+ return 1
1537
+ fi
1538
+
1539
+ {
1540
+ awk 'NR > 1 && NF > 0 { name=$1; $1=""; sub(/^[[:space:]]+/, "", $0); if ($0 == "") $0="-"; print name "\t" $0 }' "${bun_search_file}"
1541
+ exact_match_entry "bun" "${query}"
1542
+ } | awk -F'\t' 'NF >= 1 { if ($2 == "") $2 = "-"; print $1 "\t" $2 }' | awk -F'\t' '!seen[$1]++' | {
1543
+ if [[ "${effective_limit}" -gt 0 ]]; then
1544
+ awk -v limit="${effective_limit}" 'NR <= limit'
1545
+ else
1546
+ cat
1547
+ fi
1548
+ }
1549
+
1550
+ rm -f "${bun_search_file}"
1551
+ }
1552
+
1553
+ manager_search_entries_uncached() {
810
1554
  local manager="$1"
811
1555
  local query="$2"
812
1556
  local effective_query="${query}"
@@ -856,8 +1600,12 @@ manager_search_entries() {
856
1600
 
857
1601
  case "${manager}" in
858
1602
  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 }'
1603
+ if ensure_search_catalog_cache "${manager}"; then
1604
+ search_entries_from_catalog_cache "${manager}" "${effective_query}" || true
1605
+ else
1606
+ apt-cache search -- "${effective_query}" 2>/dev/null |
1607
+ awk -F' - ' '{ name=$1; desc=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", name); if (desc == "") desc="-"; print name "\t" desc }'
1608
+ fi
861
1609
  ;;
862
1610
  dnf)
863
1611
  local pattern="*"
@@ -914,11 +1662,18 @@ manager_search_entries() {
914
1662
  '
915
1663
  ;;
916
1664
  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
- }
1665
+ if ensure_search_catalog_cache "${manager}"; then
1666
+ {
1667
+ search_entries_from_catalog_cache "${manager}" "${effective_query}" || true
1668
+ exact_match_entry "${manager}" "${query}"
1669
+ }
1670
+ else
1671
+ {
1672
+ brew search "${effective_query}" 2>/dev/null |
1673
+ awk 'NF > 0 && $1 != "==>" { print $1 "\t-" }'
1674
+ exact_match_entry "${manager}" "${query}"
1675
+ }
1676
+ fi
922
1677
  ;;
923
1678
  winget)
924
1679
  winget search "${effective_query}" --source winget --accept-source-agreements --disable-interactivity 2>/dev/null |
@@ -1017,6 +1772,83 @@ manager_search_entries() {
1017
1772
  } || true
1018
1773
  }
1019
1774
 
1775
+ manager_search_entries() {
1776
+ local manager="$1"
1777
+ local query="$2"
1778
+ local query_cache_enabled="${FPF_ENABLE_QUERY_CACHE:-0}"
1779
+ local bun_cache_ttl="${FPF_BUN_QUERY_CACHE_TTL:-900}"
1780
+ local flags
1781
+ local key
1782
+ local fingerprint
1783
+ local cache_file
1784
+ local output_tmp
1785
+ local generation
1786
+ local should_async_refresh=0
1787
+
1788
+ initialize_cache_root
1789
+
1790
+ flags="$(query_cache_flags)"
1791
+
1792
+ if ! [[ "${bun_cache_ttl}" =~ ^[0-9]+$ ]]; then
1793
+ bun_cache_ttl=900
1794
+ fi
1795
+
1796
+ if [[ "${manager}" == "bun" ]]; then
1797
+ query_cache_enabled="1"
1798
+ fi
1799
+
1800
+ key="$(cache_query_key "${manager}" "${query}" "${flags}")"
1801
+ fingerprint="$(cache_fingerprint "${manager}" "${query}" "${flags}")"
1802
+ cache_file="$(cache_path_for_key "${key}")"
1803
+
1804
+ if [[ "${manager}" == "bun" && ( "${ACTION}" == "feed-search" || "${ACTION}" == "ipc-query-notify" ) ]]; then
1805
+ if [[ -s "${cache_file}" ]]; then
1806
+ cache_emit_query_rows_if_valid "${cache_file}" || true
1807
+ fi
1808
+
1809
+ if cache_is_fresh_with_ttl "${key}" "${bun_cache_ttl}"; then
1810
+ return
1811
+ fi
1812
+
1813
+ generation="$(bun_generation_next "${key}")"
1814
+ if [[ -n "${FZF_PORT:-}" && "${FPF_BUN_SKIP_REFRESH_SCHEDULE:-0}" != "1" ]]; then
1815
+ should_async_refresh=1
1816
+ fi
1817
+
1818
+ if [[ "${FPF_BUN_TEST_SYNC_REFRESH:-0}" == "1" ]]; then
1819
+ should_async_refresh=0
1820
+ fi
1821
+
1822
+ if [[ "${should_async_refresh}" -eq 1 ]]; then
1823
+ start_bun_refresh_worker_async "${manager}" "${query}" "${flags}" "${key}" "${fingerprint}" "${generation}" "${FPF_IPC_FALLBACK_FILE:-}" "${FPF_IPC_MANAGER_OVERRIDE:-}"
1824
+ return
1825
+ fi
1826
+
1827
+ bun_run_refresh_worker "${manager}" "${query}" "${flags}" "${key}" "${fingerprint}" "${generation}" || true
1828
+ if [[ -s "${cache_file}" ]]; then
1829
+ cache_emit_query_rows_if_valid "${cache_file}" || true
1830
+ fi
1831
+ return
1832
+ fi
1833
+
1834
+ if [[ "${query_cache_enabled}" == "1" && -s "${cache_file}" ]]; then
1835
+ if [[ "${manager}" != "bun" ]] || cache_is_fresh_with_ttl "${key}" "${bun_cache_ttl}"; then
1836
+ cat "${cache_file}"
1837
+ return
1838
+ fi
1839
+ fi
1840
+
1841
+ output_tmp="$(mktemp "${SESSION_TMP_ROOT}/query-cache.XXXXXX")"
1842
+ manager_search_entries_uncached "${manager}" "${query}" >"${output_tmp}" || true
1843
+
1844
+ if [[ "${query_cache_enabled}" == "1" ]]; then
1845
+ cache_store_key_from_file "${key}" "${fingerprint}" "${output_tmp}"
1846
+ fi
1847
+
1848
+ cat "${output_tmp}"
1849
+ rm -f "${output_tmp}"
1850
+ }
1851
+
1020
1852
  manager_installed_entries() {
1021
1853
  local manager="$1"
1022
1854
 
@@ -1137,34 +1969,35 @@ manager_installed_entries() {
1137
1969
  ;;
1138
1970
  bun)
1139
1971
  {
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
- }
1972
+ local bun_pm_file
1973
+ bun_pm_file="$(mktemp "${SESSION_TMP_ROOT}/bun-pm.XXXXXX")"
1974
+ if bun pm ls --global >"${bun_pm_file}" 2>/dev/null; then
1975
+ awk '
1976
+ NR > 1 {
1977
+ line = $0
1978
+ gsub(/\r/, "", line)
1979
+ sub(/^[[:space:]]*[^[:alnum:]@]*/, "", line)
1980
+ sub(/^[[:space:]]+/, "", line)
1981
+ sub(/[[:space:]]+$/, "", line)
1982
+
1983
+ if (line == "") next
1984
+ if (line ~ /[[:space:]]node_modules([[:space:]]|$)/) next
1985
+
1986
+ split(line, fields, /[[:space:]]+/)
1987
+ pkg = fields[1]
1988
+ if (pkg == "") next
1989
+
1990
+ pkg_copy = pkg
1991
+ at_count = gsub(/@/, "@", pkg_copy)
1992
+ if ((substr(pkg, 1, 1) == "@" && at_count >= 2) || (substr(pkg, 1, 1) != "@" && at_count >= 1)) {
1993
+ sub(/@[^@[:space:]]*$/, "", pkg)
1994
+ }
1162
1995
 
1163
- if (pkg != "") {
1164
- print pkg "\tglobal"
1165
- }
1996
+ if (pkg != "") {
1997
+ print pkg "\tglobal"
1166
1998
  }
1167
- '
1999
+ }
2000
+ ' "${bun_pm_file}"
1168
2001
  elif command_exists npm; then
1169
2002
  npm ls -g --depth=0 --parseable 2>/dev/null |
1170
2003
  awk '
@@ -1188,6 +2021,7 @@ manager_installed_entries() {
1188
2021
  }
1189
2022
  '
1190
2023
  fi
2024
+ rm -f "${bun_pm_file}"
1191
2025
  } || true
1192
2026
  ;;
1193
2027
  esac | sort -u || true
@@ -1202,8 +2036,15 @@ manager_installed_names_cached() {
1202
2036
  local manager="$1"
1203
2037
  local output_file="$2"
1204
2038
  local cache_enabled="${FPF_DISABLE_INSTALLED_CACHE:-0}"
1205
- local cache_dir="${TMP_ROOT}/cache"
1206
- local cache_file="${cache_dir}/installed.${manager}"
2039
+ local cache_key
2040
+ local cache_file
2041
+ local cache_fingerprint_value
2042
+
2043
+ initialize_cache_root
2044
+
2045
+ cache_key="$(cache_catalog_key "${manager}")"
2046
+ cache_file="$(cache_path_for_key "${cache_key}")"
2047
+ cache_fingerprint_value="$(cache_fingerprint "${manager}" "" "installed")"
1207
2048
 
1208
2049
  if [[ "${cache_enabled}" != "1" && -s "${cache_file}" ]]; then
1209
2050
  cp "${cache_file}" "${output_file}"
@@ -1213,47 +2054,82 @@ manager_installed_names_cached() {
1213
2054
  manager_installed_names "${manager}" >"${output_file}" 2>/dev/null || true
1214
2055
 
1215
2056
  if [[ "${cache_enabled}" != "1" && -s "${output_file}" ]]; then
1216
- mkdir -p "${cache_dir}"
1217
- cp "${output_file}" "${cache_file}"
2057
+ cache_store_key_from_file "${cache_key}" "${cache_fingerprint_value}" "${output_file}"
1218
2058
  fi
1219
2059
  }
1220
2060
 
1221
2061
  mark_installed_packages() {
1222
- local manager="$1"
1223
- local source_file="$2"
1224
- local output_file="$3"
2062
+ local source_file="$1"
2063
+ local output_file="$2"
1225
2064
  local installed_file
2065
+ local installed_map_file
2066
+ local manager
1226
2067
 
1227
2068
  if [[ "${FPF_SKIP_INSTALLED_MARKERS:-0}" == "1" ]]; then
1228
2069
  awk -F'\t' '
1229
2070
  {
1230
- desc = $2
2071
+ desc = $3
1231
2072
  if (desc == "") desc = "-"
1232
- print $1 "\t " desc
2073
+ print $1 "\t" $2 "\t " desc
1233
2074
  }
1234
2075
  ' "${source_file}" >"${output_file}"
1235
2076
  return
1236
2077
  fi
1237
2078
 
1238
- installed_file="$(mktemp "${SESSION_TMP_ROOT}/installed.XXXXXX")"
1239
- manager_installed_names_cached "${manager}" "${installed_file}"
2079
+ installed_map_file="$(mktemp "${SESSION_TMP_ROOT}/installed-map.XXXXXX")"
2080
+ : >"${installed_map_file}"
2081
+
2082
+ while IFS= read -r manager; do
2083
+ [[ -n "${manager}" ]] || continue
2084
+ installed_file="$(mktemp "${SESSION_TMP_ROOT}/installed.${manager}.XXXXXX")"
2085
+ manager_installed_names_cached "${manager}" "${installed_file}"
2086
+ if [[ -s "${installed_file}" ]]; then
2087
+ awk -F'\t' -v mgr="${manager}" 'NF > 0 && $1 != "" { print mgr "\t" $1 }' "${installed_file}" >>"${installed_map_file}"
2088
+ fi
2089
+ rm -f "${installed_file}"
2090
+ done < <(awk -F'\t' 'NF >= 1 && $1 != "" { print $1 }' "${source_file}" | awk '!seen[$0]++')
1240
2091
 
1241
2092
  awk -F'\t' '
1242
2093
  FILENAME == ARGV[1] {
1243
- if ($1 != "") {
1244
- installed[$1] = 1
2094
+ key = $1 "\t" $2
2095
+ if ($1 != "" && $2 != "") {
2096
+ installed[key] = 1
1245
2097
  }
1246
2098
  next
1247
2099
  }
1248
2100
  {
1249
- mark = (installed[$1] ? "* " : " ")
1250
- desc = $2
2101
+ key = $1 "\t" $2
2102
+ mark = (installed[key] ? "* " : " ")
2103
+ desc = $3
1251
2104
  if (desc == "") desc = "-"
1252
- print $1 "\t" mark desc
2105
+ print $1 "\t" $2 "\t" mark desc
1253
2106
  }
1254
- ' "${installed_file}" "${source_file}" >"${output_file}"
2107
+ ' "${installed_map_file}" "${source_file}" >"${output_file}"
2108
+
2109
+ rm -f "${installed_map_file}"
2110
+ }
2111
+
2112
+ merge_search_display_rows() {
2113
+ local source_file="$1"
2114
+ local output_file="$2"
2115
+
2116
+ if [[ ! -s "${source_file}" ]]; then
2117
+ : >"${output_file}"
2118
+ return
2119
+ fi
1255
2120
 
1256
- rm -f "${installed_file}"
2121
+ sort -t $'\t' -k1,1 -k2,2 -k3,3 "${source_file}" |
2122
+ awk -F'\t' '
2123
+ NF >= 2 {
2124
+ key = $1 "\t" $2
2125
+ if (seen[key]++) {
2126
+ next
2127
+ }
2128
+ desc = $3
2129
+ if (desc == "") desc = "-"
2130
+ print $1 "\t" $2 "\t" desc
2131
+ }
2132
+ ' >"${output_file}"
1257
2133
  }
1258
2134
 
1259
2135
  collect_search_display_rows() {
@@ -1270,6 +2146,8 @@ collect_search_display_rows() {
1270
2146
  local manager
1271
2147
  local part_file
1272
2148
  local gather_pid
2149
+ local merged_source_file
2150
+ local merged_marked_file
1273
2151
 
1274
2152
  for manager in "${managers[@]-}"; do
1275
2153
  part_file="$(mktemp "${SESSION_TMP_ROOT}/part.XXXXXX")"
@@ -1277,19 +2155,17 @@ collect_search_display_rows() {
1277
2155
 
1278
2156
  (
1279
2157
  local_source_file="$(mktemp "${SESSION_TMP_ROOT}/source.XXXXXX")"
1280
- local_marked_file="$(mktemp "${SESSION_TMP_ROOT}/marked.XXXXXX")"
1281
2158
  manager_search_entries "${manager}" "${query}" >"${local_source_file}" || true
1282
2159
  if [[ -s "${local_source_file}" ]]; then
1283
- mark_installed_packages "${manager}" "${local_source_file}" "${local_marked_file}"
1284
2160
  awk -F'\t' -v mgr="${manager}" '
1285
2161
  NF >= 1 {
1286
2162
  desc = $2
1287
2163
  if (desc == "") desc = "-"
1288
2164
  print mgr "\t" $1 "\t" desc
1289
2165
  }
1290
- ' "${local_marked_file}" >"${part_file}"
2166
+ ' "${local_source_file}" >"${part_file}"
1291
2167
  fi
1292
- rm -f "${local_source_file}" "${local_marked_file}"
2168
+ rm -f "${local_source_file}"
1293
2169
  ) &
1294
2170
  gather_pids+=("$!")
1295
2171
  done
@@ -1298,15 +2174,19 @@ collect_search_display_rows() {
1298
2174
  wait "${gather_pid}" || true
1299
2175
  done
1300
2176
 
2177
+ merged_source_file="$(mktemp "${SESSION_TMP_ROOT}/merged-source.XXXXXX")"
2178
+ merged_marked_file="$(mktemp "${SESSION_TMP_ROOT}/merged-marked.XXXXXX")"
2179
+
1301
2180
  for part_file in "${part_files[@]-}"; do
1302
2181
  if [[ -s "${part_file}" ]]; then
1303
- cat "${part_file}" >>"${output_file}"
2182
+ cat "${part_file}" >>"${merged_source_file}"
1304
2183
  fi
1305
2184
  rm -f "${part_file}"
1306
2185
  done
1307
2186
 
1308
- if [[ -s "${output_file}" ]]; then
1309
- sort -u "${output_file}" -o "${output_file}"
2187
+ if [[ -s "${merged_source_file}" ]]; then
2188
+ merge_search_display_rows "${merged_source_file}" "${merged_marked_file}"
2189
+ mark_installed_packages "${merged_marked_file}" "${output_file}"
1310
2190
  rank_display_rows_by_query "${query}" "${output_file}"
1311
2191
 
1312
2192
  if [[ -n "${query}" && "${query_limit}" =~ ^[0-9]+$ && "${query_limit}" -gt 0 ]]; then
@@ -1314,6 +2194,8 @@ collect_search_display_rows() {
1314
2194
  mv "${output_file}.limited" "${output_file}"
1315
2195
  fi
1316
2196
  fi
2197
+
2198
+ rm -f "${merged_source_file}" "${merged_marked_file}"
1317
2199
  }
1318
2200
 
1319
2201
  build_dynamic_reload_command() {
@@ -1336,10 +2218,195 @@ build_dynamic_reload_command() {
1336
2218
  fi
1337
2219
 
1338
2220
  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}"
2221
+ 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}"
2222
+ else
2223
+ 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}"
2224
+ fi
2225
+ }
2226
+
2227
+ build_dynamic_reload_command_for_query() {
2228
+ local manager_override="$1"
2229
+ local fallback_file="$2"
2230
+ local query_value="$3"
2231
+ local script_path="${BASH_SOURCE[0]}"
2232
+ local min_chars="${FPF_RELOAD_MIN_CHARS:-2}"
2233
+ local reload_debounce="${FPF_RELOAD_DEBOUNCE:-0.12}"
2234
+
2235
+ if ! [[ "${min_chars}" =~ ^[0-9]+$ ]]; then
2236
+ min_chars=2
2237
+ fi
2238
+
2239
+ if ! [[ "${reload_debounce}" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
2240
+ reload_debounce=0.12
2241
+ fi
2242
+
2243
+ if [[ "${script_path}" != /* ]]; then
2244
+ script_path="$(pwd)/${script_path}"
2245
+ fi
2246
+
2247
+ if [[ ${#query_value} -lt ${min_chars} ]]; then
2248
+ printf 'cat %q' "${fallback_file}"
2249
+ return
2250
+ fi
2251
+
2252
+ if [[ -n "${manager_override}" ]]; then
2253
+ 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}"
2254
+ else
2255
+ 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}"
2256
+ fi
2257
+ }
2258
+
2259
+ build_dynamic_reload_ipc_command() {
2260
+ local manager_override="$1"
2261
+ local fallback_file="$2"
2262
+ local script_path="${BASH_SOURCE[0]}"
2263
+
2264
+ if [[ "${script_path}" != /* ]]; then
2265
+ script_path="$(pwd)/${script_path}"
2266
+ fi
2267
+
2268
+ printf 'FPF_IPC_MANAGER_OVERRIDE=%q FPF_IPC_FALLBACK_FILE=%q %q --ipc-reload -- "{q}"' "${manager_override}" "${fallback_file}" "${script_path}"
2269
+ }
2270
+
2271
+ build_dynamic_query_notify_ipc_command() {
2272
+ local manager_override="$1"
2273
+ local fallback_file="$2"
2274
+ local script_path="${BASH_SOURCE[0]}"
2275
+
2276
+ if [[ "${script_path}" != /* ]]; then
2277
+ script_path="$(pwd)/${script_path}"
2278
+ fi
2279
+
2280
+ printf 'FPF_IPC_MANAGER_OVERRIDE=%q FPF_IPC_FALLBACK_FILE=%q %q --ipc-query-notify -- "{q}"' "${manager_override}" "${fallback_file}" "${script_path}"
2281
+ }
2282
+
2283
+ fzf_supports_listen() {
2284
+ local fzf_help
2285
+ fzf_help="$(fzf --help 2>&1 || true)"
2286
+ [[ "${fzf_help}" == *"--listen"* ]]
2287
+ }
2288
+
2289
+ send_fzf_listen_action() {
2290
+ local action_payload="$1"
2291
+ local port="${FZF_PORT:-}"
2292
+ local host="127.0.0.1"
2293
+ local payload_size
2294
+
2295
+ if ! [[ "${port}" =~ ^[0-9]+$ ]] || [[ "${port}" -le 0 || "${port}" -gt 65535 ]]; then
2296
+ return 1
2297
+ fi
2298
+
2299
+ payload_size="$(printf "%s" "${action_payload}" | wc -c | tr -d '[:space:]')"
2300
+
2301
+ if command_exists curl; then
2302
+ curl --silent --show-error --fail --max-time 2 \
2303
+ --request POST \
2304
+ --header 'Content-Type: text/plain' \
2305
+ --data-binary "${action_payload}" \
2306
+ "http://${host}:${port}" >/dev/null 2>&1
2307
+ return
2308
+ fi
2309
+
2310
+ if command_exists nc; then
2311
+ {
2312
+ printf 'POST / HTTP/1.1\r\n'
2313
+ printf 'Host: %s:%s\r\n' "${host}" "${port}"
2314
+ printf 'Content-Type: text/plain\r\n'
2315
+ printf 'Content-Length: %s\r\n' "${payload_size}"
2316
+ printf '\r\n'
2317
+ printf '%s' "${action_payload}"
2318
+ } | nc -w 2 "${host}" "${port}" >/dev/null 2>&1
2319
+ return
2320
+ fi
2321
+
2322
+ if exec 9<>"/dev/tcp/${host}/${port}" 2>/dev/null; then
2323
+ printf 'POST / HTTP/1.1\r\n' >&9
2324
+ printf 'Host: %s:%s\r\n' "${host}" "${port}" >&9
2325
+ printf 'Content-Type: text/plain\r\n' >&9
2326
+ printf 'Content-Length: %s\r\n' "${payload_size}" >&9
2327
+ printf '\r\n' >&9
2328
+ printf '%s' "${action_payload}" >&9
2329
+ exec 9>&-
2330
+ return 0
2331
+ fi
2332
+
2333
+ return 1
2334
+ }
2335
+
2336
+ send_fzf_prompt_action() {
2337
+ local prompt_text="$1"
2338
+ local action_payload
2339
+
2340
+ action_payload="change-prompt(${prompt_text})"
2341
+ send_fzf_listen_action "${action_payload}"
2342
+ }
2343
+
2344
+ run_ipc_reload_action() {
2345
+ local query="$1"
2346
+ local manager_override="${FPF_IPC_MANAGER_OVERRIDE:-}"
2347
+ local fallback_file="${FPF_IPC_FALLBACK_FILE:-}"
2348
+ local reload_command
2349
+ local action_payload
2350
+
2351
+ if [[ -z "${fallback_file}" || ! -r "${fallback_file}" ]]; then
2352
+ return 1
2353
+ fi
2354
+
2355
+ if [[ -n "${manager_override}" ]]; then
2356
+ manager_override="$(normalize_manager "${manager_override}")"
2357
+ manager_supported "${manager_override}" || return 1
2358
+ fi
2359
+
2360
+ reload_command="$(build_dynamic_reload_command_for_query "${manager_override}" "${fallback_file}" "${query}")"
2361
+ action_payload="reload(${reload_command})+change-prompt(Search> )"
2362
+ send_fzf_listen_action "${action_payload}"
2363
+ }
2364
+
2365
+ run_ipc_query_notify_action() {
2366
+ local query="$1"
2367
+ local manager_override="${FPF_IPC_MANAGER_OVERRIDE:-}"
2368
+ local fallback_file="${FPF_IPC_FALLBACK_FILE:-}"
2369
+ local min_chars="${FPF_RELOAD_MIN_CHARS:-2}"
2370
+ local target_manager=""
2371
+
2372
+ if ! [[ "${min_chars}" =~ ^[0-9]+$ ]]; then
2373
+ min_chars=2
2374
+ fi
2375
+
2376
+ if [[ ${#query} -lt ${min_chars} ]]; then
2377
+ send_fzf_prompt_action "Search> " || true
2378
+ return 0
2379
+ fi
2380
+
2381
+ if [[ -z "${fallback_file}" || ! -r "${fallback_file}" ]]; then
2382
+ return 1
2383
+ fi
2384
+
2385
+ if [[ -n "${manager_override}" ]]; then
2386
+ manager_override="$(normalize_manager "${manager_override}")"
2387
+ manager_supported "${manager_override}" || return 1
2388
+ target_manager="${manager_override}"
1340
2389
  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}"
2390
+ target_manager="bun"
2391
+ fi
2392
+
2393
+ if [[ "${target_manager}" != "bun" ]]; then
2394
+ send_fzf_prompt_action "Search> " || true
2395
+ return 0
2396
+ fi
2397
+
2398
+ if ! manager_command_ready bun; then
2399
+ send_fzf_prompt_action "Search> " || true
2400
+ return 0
1342
2401
  fi
2402
+
2403
+ send_fzf_prompt_action "Loading> " || true
2404
+
2405
+ FPF_IPC_MANAGER_OVERRIDE="${manager_override}" \
2406
+ FPF_IPC_FALLBACK_FILE="${fallback_file}" \
2407
+ manager_search_entries "bun" "${query}" >/dev/null 2>&1 || {
2408
+ send_fzf_prompt_action "Search> " || true
2409
+ }
1343
2410
  }
1344
2411
 
1345
2412
  manager_install() {
@@ -1572,9 +2639,15 @@ run_fuzzy_selector() {
1572
2639
  local input_file="$2"
1573
2640
  local header_line="$3"
1574
2641
  local reload_cmd="${4:-}"
2642
+ local reload_ipc_cmd="${5:-}"
2643
+ local script_path="${BASH_SOURCE[0]}"
1575
2644
  local preview_cmd
1576
2645
 
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}'
2646
+ if [[ "${script_path}" != /* ]]; then
2647
+ script_path="$(pwd)/${script_path}"
2648
+ fi
2649
+
2650
+ preview_cmd="FPF_SESSION_TMP_ROOT=$(printf '%q' "${SESSION_TMP_ROOT}") ${script_path} --preview-item --manager {1} -- {2}"
1578
2651
 
1579
2652
  local -a fzf_args=()
1580
2653
  fzf_args=(-q "${query}" -m \
@@ -1597,8 +2670,14 @@ run_fuzzy_selector() {
1597
2670
  --bind=ctrl-n:next-selected,ctrl-b:prev-selected \
1598
2671
  --bind='focus:transform-preview-label:echo [{1}] {2}')
1599
2672
 
1600
- if [[ -n "${reload_cmd}" ]]; then
1601
- fzf_args+=(--bind="change:reload:${reload_cmd}")
2673
+ if [[ -n "${reload_ipc_cmd}" ]]; then
2674
+ fzf_args+=(--listen=127.0.0.1:0)
2675
+ fzf_args+=(--bind="change:execute-silent:${reload_ipc_cmd}")
2676
+ if [[ -n "${reload_cmd}" ]]; then
2677
+ fzf_args+=(--bind="ctrl-r:reload:${reload_cmd}")
2678
+ fi
2679
+ elif [[ -n "${reload_cmd}" ]]; then
2680
+ fzf_args+=(--bind="ctrl-r:reload:${reload_cmd}")
1602
2681
  fi
1603
2682
 
1604
2683
  fzf "${fzf_args[@]}" <"${input_file}"
@@ -1607,10 +2686,54 @@ run_fuzzy_selector() {
1607
2686
  main() {
1608
2687
  ensure_tmp_root
1609
2688
  initialize_session_tmp_root
1610
- trap cleanup_session_tmp_root EXIT
2689
+ if [[ -z "${FPF_SESSION_TMP_ROOT:-}" ]]; then
2690
+ trap cleanup_session_tmp_root EXIT
2691
+ fi
1611
2692
 
1612
2693
  parse_args "$@"
1613
2694
 
2695
+ if [[ "${ACTION}" == "ipc-reload" ]]; then
2696
+ run_ipc_reload_action "$(join_query)" || true
2697
+ exit 0
2698
+ fi
2699
+
2700
+ if [[ "${ACTION}" == "ipc-query-notify" ]]; then
2701
+ run_ipc_query_notify_action "$(join_query)" || true
2702
+ exit 0
2703
+ fi
2704
+
2705
+ if [[ "${ACTION}" == "preview-item" ]]; then
2706
+ local preview_manager="${MANAGER_OVERRIDE:-}"
2707
+ local preview_package
2708
+
2709
+ preview_package="$(join_query)"
2710
+ run_preview_item_action "${preview_manager}" "${preview_package}" || true
2711
+ exit 0
2712
+ fi
2713
+
2714
+ if [[ "${ACTION}" == "bun-refresh-worker" ]]; then
2715
+ local worker_manager="${MANAGER_OVERRIDE:-bun}"
2716
+ local worker_query
2717
+ local worker_flags
2718
+ local worker_key
2719
+ local worker_fingerprint
2720
+ local worker_generation
2721
+
2722
+ worker_query="$(join_query)"
2723
+ worker_flags="${FPF_BUN_REFRESH_FLAGS:-$(query_cache_flags)}"
2724
+ worker_key="${FPF_BUN_REFRESH_KEY:-$(cache_query_key "${worker_manager}" "${worker_query}" "${worker_flags}")}"
2725
+ worker_fingerprint="${FPF_BUN_REFRESH_FINGERPRINT:-$(cache_fingerprint "${worker_manager}" "${worker_query}" "${worker_flags}")}"
2726
+ worker_generation="${FPF_BUN_REFRESH_GENERATION:-0}"
2727
+
2728
+ if ! [[ "${worker_generation}" =~ ^[0-9]+$ ]] || [[ "${worker_generation}" -le 0 ]]; then
2729
+ exit 0
2730
+ fi
2731
+
2732
+ initialize_cache_root
2733
+ bun_run_refresh_worker "${worker_manager}" "${worker_query}" "${worker_flags}" "${worker_key}" "${worker_fingerprint}" "${worker_generation}" || true
2734
+ exit 0
2735
+ fi
2736
+
1614
2737
  if [[ "${ACTION}" == "version" ]]; then
1615
2738
  print_version
1616
2739
  exit 0
@@ -1739,6 +2862,13 @@ main() {
1739
2862
 
1740
2863
  if [[ ! -s "${display_file}" ]]; then
1741
2864
  rm -f "${display_file}"
2865
+
2866
+ if [[ "${ACTION}" == "search" && "${#managers[@]}" -eq 1 && "${managers[0]}" == "flatpak" ]]; then
2867
+ if ! flatpak_has_any_remotes; then
2868
+ die "Flatpak has no remotes configured. Add Flathub with: flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo"
2869
+ fi
2870
+ fi
2871
+
1742
2872
  if [[ -n "${query}" ]]; then
1743
2873
  die "No packages found for ${manager_display} matching '${query}'. Try a broader query or --manager."
1744
2874
  fi
@@ -1762,14 +2892,18 @@ main() {
1762
2892
  esac
1763
2893
 
1764
2894
  local reload_cmd=""
2895
+ local reload_ipc_cmd=""
1765
2896
  if [[ "${ACTION}" == "search" && -z "${query}" ]]; then
1766
2897
  if dynamic_reload_enabled "${#managers[@]}"; then
1767
2898
  reload_cmd="$(build_dynamic_reload_command "${MANAGER_OVERRIDE}" "${display_file}")"
2899
+ if fzf_supports_listen; then
2900
+ reload_ipc_cmd="$(build_dynamic_query_notify_ipc_command "${MANAGER_OVERRIDE}" "${display_file}")"
2901
+ fi
1768
2902
  fi
1769
2903
  fi
1770
2904
 
1771
2905
  local selected
1772
- selected="$(run_fuzzy_selector "${query}" "${display_file}" "${header}" "${reload_cmd}" || true)"
2906
+ selected="$(run_fuzzy_selector "${query}" "${display_file}" "${header}" "${reload_cmd}" "${reload_ipc_cmd}" || true)"
1773
2907
 
1774
2908
  rm -f "${display_file}"
1775
2909
 
@@ -1824,7 +2958,7 @@ main() {
1824
2958
  if confirm_action "Install ${#selected_packages[@]} package(s) with ${selected_manager_display}?"; then
1825
2959
  for manager in "${unique_managers[@]-}"; do
1826
2960
  mgr_packages=()
1827
- for idx in "${!selected_packages[@]-}"; do
2961
+ for idx in "${!selected_packages[@]}"; do
1828
2962
  if [[ "${selected_managers[$idx]}" == "${manager}" ]]; then
1829
2963
  mgr_packages+=("${selected_packages[$idx]}")
1830
2964
  fi
@@ -1842,7 +2976,7 @@ main() {
1842
2976
  if confirm_action "Remove ${#selected_packages[@]} package(s) with ${selected_manager_display}?"; then
1843
2977
  for manager in "${unique_managers[@]-}"; do
1844
2978
  mgr_packages=()
1845
- for idx in "${!selected_packages[@]-}"; do
2979
+ for idx in "${!selected_packages[@]}"; do
1846
2980
  if [[ "${selected_managers[$idx]}" == "${manager}" ]]; then
1847
2981
  mgr_packages+=("${selected_packages[$idx]}")
1848
2982
  fi
@@ -1857,7 +2991,7 @@ main() {
1857
2991
  fi
1858
2992
  ;;
1859
2993
  list)
1860
- for idx in "${!selected_packages[@]-}"; do
2994
+ for idx in "${!selected_packages[@]}"; do
1861
2995
  printf "\n=== %s (%s) ===\n" "${selected_packages[$idx]}" "$(manager_label "${selected_managers[$idx]}")"
1862
2996
  manager_show_info "${selected_managers[$idx]}" "${selected_packages[$idx]}" || true
1863
2997
  done