fpf-cli 1.0.0

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 (4) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +104 -0
  3. package/fpf +962 -0
  4. package/package.json +44 -0
package/fpf ADDED
@@ -0,0 +1,962 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ SCRIPT_NAME="fpf"
6
+ TMP_ROOT="${TMPDIR:-/tmp}/fpf"
7
+ HELP_FILE="${TMP_ROOT}/help"
8
+ KBINDS_FILE="${TMP_ROOT}/keybinds"
9
+
10
+ ACTION="search"
11
+ MANAGER_OVERRIDE=""
12
+ declare -a QUERY_PARTS=()
13
+
14
+ log() {
15
+ printf "%s\n" "$*" >&2
16
+ }
17
+
18
+ die() {
19
+ log "Error: $*"
20
+ exit 1
21
+ }
22
+
23
+ command_exists() {
24
+ command -v "$1" >/dev/null 2>&1
25
+ }
26
+
27
+ ensure_tmp_root() {
28
+ mkdir -p "${TMP_ROOT}"
29
+ }
30
+
31
+ run_as_root() {
32
+ if [[ "${EUID}" -eq 0 ]]; then
33
+ "$@"
34
+ return
35
+ fi
36
+
37
+ if command_exists sudo; then
38
+ sudo "$@"
39
+ return
40
+ fi
41
+
42
+ die "Root privileges are required for: $*"
43
+ }
44
+
45
+ manager_list() {
46
+ printf "%s\n" "apt dnf pacman zypper emerge brew snap flatpak npm bun"
47
+ }
48
+
49
+ manager_supported() {
50
+ local manager="$1"
51
+ case "${manager}" in
52
+ apt|dnf|pacman|zypper|emerge|brew|snap|flatpak|npm|bun)
53
+ return 0
54
+ ;;
55
+ *)
56
+ return 1
57
+ ;;
58
+ esac
59
+ }
60
+
61
+ manager_command_ready() {
62
+ local manager="$1"
63
+ case "${manager}" in
64
+ apt)
65
+ command_exists apt-cache && command_exists apt-get && command_exists dpkg-query
66
+ ;;
67
+ dnf)
68
+ command_exists dnf
69
+ ;;
70
+ pacman)
71
+ command_exists pacman
72
+ ;;
73
+ zypper)
74
+ command_exists zypper
75
+ ;;
76
+ emerge)
77
+ command_exists emerge
78
+ ;;
79
+ brew)
80
+ command_exists brew
81
+ ;;
82
+ snap)
83
+ command_exists snap
84
+ ;;
85
+ flatpak)
86
+ command_exists flatpak
87
+ ;;
88
+ npm)
89
+ command_exists npm
90
+ ;;
91
+ bun)
92
+ command_exists bun
93
+ ;;
94
+ *)
95
+ return 1
96
+ ;;
97
+ esac
98
+ }
99
+
100
+ normalize_manager() {
101
+ printf "%s" "$1" | tr '[:upper:]' '[:lower:]'
102
+ }
103
+
104
+ os_release_field() {
105
+ local file_path="$1"
106
+ local field_name="$2"
107
+
108
+ awk -v key="${field_name}" '
109
+ index($0, key "=") == 1 {
110
+ value = substr($0, index($0, "=") + 1)
111
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
112
+ if (value ~ /^".*"$/ || value ~ /^\047.*\047$/) {
113
+ value = substr(value, 2, length(value) - 2)
114
+ }
115
+ print tolower(value)
116
+ exit
117
+ }
118
+ ' "${file_path}"
119
+ }
120
+
121
+ manager_label() {
122
+ local manager="$1"
123
+ case "${manager}" in
124
+ apt) printf "APT" ;;
125
+ dnf) printf "DNF" ;;
126
+ pacman) printf "Pacman" ;;
127
+ zypper) printf "Zypper" ;;
128
+ emerge) printf "Portage (emerge)" ;;
129
+ brew) printf "Homebrew" ;;
130
+ snap) printf "Snap" ;;
131
+ flatpak) printf "Flatpak" ;;
132
+ npm) printf "npm" ;;
133
+ bun) printf "bun" ;;
134
+ *) printf "%s" "${manager}" ;;
135
+ esac
136
+ }
137
+
138
+ detect_default_manager() {
139
+ local os
140
+ os="$(uname -s)"
141
+
142
+ if [[ "${os}" == "Darwin" ]]; then
143
+ if command_exists brew; then
144
+ printf "brew"
145
+ return
146
+ fi
147
+ fi
148
+
149
+ if [[ "${os}" == "Linux" ]]; then
150
+ local distro_id=""
151
+ local distro_like=""
152
+ local os_release_file="${FPF_OS_RELEASE_FILE:-}"
153
+
154
+ if [[ -n "${os_release_file}" && ! -r "${os_release_file}" ]]; then
155
+ die "FPF_OS_RELEASE_FILE is set but not readable: ${os_release_file}"
156
+ fi
157
+
158
+ if [[ -z "${os_release_file}" && -r /etc/os-release ]]; then
159
+ os_release_file="/etc/os-release"
160
+ elif [[ -z "${os_release_file}" && -r /usr/lib/os-release ]]; then
161
+ os_release_file="/usr/lib/os-release"
162
+ fi
163
+
164
+ if [[ -n "${os_release_file}" ]]; then
165
+ distro_id="$(os_release_field "${os_release_file}" "ID")"
166
+ distro_like="$(os_release_field "${os_release_file}" "ID_LIKE")"
167
+ fi
168
+
169
+ case "${distro_id} ${distro_like}" in
170
+ *arch*|*manjaro*)
171
+ if command_exists pacman; then printf "pacman"; return; fi
172
+ ;;
173
+ *ubuntu*|*debian*|*linuxmint*|*pop*|*elementary*)
174
+ if command_exists apt-get; then printf "apt"; return; fi
175
+ ;;
176
+ *fedora*|*rhel*|*centos*|*rocky*|*alma*)
177
+ if command_exists dnf; then printf "dnf"; return; fi
178
+ ;;
179
+ *opensuse*|*suse*|*sles*)
180
+ if command_exists zypper; then printf "zypper"; return; fi
181
+ ;;
182
+ *gentoo*)
183
+ if command_exists emerge; then printf "emerge"; return; fi
184
+ ;;
185
+ esac
186
+
187
+ if command_exists apt-get; then printf "apt"; return; fi
188
+ if command_exists dnf; then printf "dnf"; return; fi
189
+ if command_exists pacman; then printf "pacman"; return; fi
190
+ if command_exists zypper; then printf "zypper"; return; fi
191
+ if command_exists emerge; then printf "emerge"; return; fi
192
+ if command_exists snap; then printf "snap"; return; fi
193
+ if command_exists flatpak; then printf "flatpak"; return; fi
194
+ if command_exists npm; then printf "npm"; return; fi
195
+ if command_exists bun; then printf "bun"; return; fi
196
+ fi
197
+
198
+ if command_exists brew; then printf "brew"; return; fi
199
+ if command_exists npm; then printf "npm"; return; fi
200
+ if command_exists bun; then printf "bun"; return; fi
201
+
202
+ die "Unable to auto-detect a supported package manager. Use --manager."
203
+ }
204
+
205
+ build_help_file() {
206
+ local default_manager="$1"
207
+
208
+ cat >"${HELP_FILE}" <<EOF
209
+ ${SCRIPT_NAME} - ultimate fuzzy package finder
210
+
211
+ Syntax:
212
+ ${SCRIPT_NAME} [manager option] [action option] [query]
213
+ ${SCRIPT_NAME} -m|--manager <name> [action option] [query]
214
+
215
+ Default behavior:
216
+ Fuzzy-search available packages and install selected items.
217
+
218
+ Detected default manager:
219
+ $(manager_label "${default_manager}") (${default_manager})
220
+
221
+ Action options:
222
+ -l, --list-installed Fuzzy-search installed packages and show details
223
+ -R, --remove Fuzzy-search installed packages and remove selected
224
+ -U, --update Run manager update/upgrade flow
225
+ -h, --help Show this help
226
+
227
+ Manager options (one or two-letter style):
228
+ -ap, --apt Use APT
229
+ -dn, --dnf Use DNF
230
+ -pm, --pacman Use Pacman
231
+ -zy, --zypper Use Zypper
232
+ -em, --emerge Use Portage (emerge)
233
+ -br, --brew Use Homebrew
234
+ -sn, --snap Use Snap
235
+ -fp, --flatpak Use Flatpak
236
+ -np, --npm Use npm (global packages)
237
+ -bn, --bun Use bun (global packages)
238
+ -ad, --auto Force auto-detection mode
239
+
240
+ Examples:
241
+ ${SCRIPT_NAME} docker
242
+ ${SCRIPT_NAME} -dn nginx
243
+ ${SCRIPT_NAME} -ap -l openssl
244
+ ${SCRIPT_NAME} -br -R wget
245
+ ${SCRIPT_NAME} -sn firefox
246
+ ${SCRIPT_NAME} -fp org.gimp.GIMP
247
+ ${SCRIPT_NAME} -np eslint
248
+ ${SCRIPT_NAME} -m apt ripgrep
249
+
250
+ Supported managers:
251
+ $(manager_list)
252
+ EOF
253
+ }
254
+
255
+ build_keybind_file() {
256
+ cat >"${KBINDS_FILE}" <<'EOF'
257
+ Keybinds:
258
+
259
+ ctrl-h Show help in preview pane
260
+ ctrl-k Show keybinds in preview pane
261
+ ctrl-/ Toggle preview pane
262
+ ctrl-n Move to next selected package
263
+ ctrl-b Move to previous selected package
264
+ EOF
265
+ }
266
+
267
+ print_help() {
268
+ cat "${HELP_FILE}"
269
+ }
270
+
271
+ parse_args() {
272
+ while (($#)); do
273
+ case "$1" in
274
+ -h|--help)
275
+ ACTION="help"
276
+ ;;
277
+ -l|--list-installed)
278
+ ACTION="list"
279
+ ;;
280
+ -R|--remove)
281
+ ACTION="remove"
282
+ ;;
283
+ -U|--update)
284
+ ACTION="update"
285
+ ;;
286
+ -ap|--apt)
287
+ MANAGER_OVERRIDE="apt"
288
+ ;;
289
+ -dn|--dnf)
290
+ MANAGER_OVERRIDE="dnf"
291
+ ;;
292
+ -pm|--pacman)
293
+ MANAGER_OVERRIDE="pacman"
294
+ ;;
295
+ -zy|--zypper)
296
+ MANAGER_OVERRIDE="zypper"
297
+ ;;
298
+ -em|--emerge)
299
+ MANAGER_OVERRIDE="emerge"
300
+ ;;
301
+ -br|--brew)
302
+ MANAGER_OVERRIDE="brew"
303
+ ;;
304
+ -sn|--snap)
305
+ MANAGER_OVERRIDE="snap"
306
+ ;;
307
+ -fp|--flatpak)
308
+ MANAGER_OVERRIDE="flatpak"
309
+ ;;
310
+ -np|--npm)
311
+ MANAGER_OVERRIDE="npm"
312
+ ;;
313
+ -bn|--bun)
314
+ MANAGER_OVERRIDE="bun"
315
+ ;;
316
+ -ad|--auto)
317
+ MANAGER_OVERRIDE=""
318
+ ;;
319
+ -m|--manager)
320
+ shift
321
+ [[ $# -gt 0 ]] || die "Missing value for --manager"
322
+ MANAGER_OVERRIDE="$(normalize_manager "$1")"
323
+ ;;
324
+ --)
325
+ shift
326
+ while (($#)); do
327
+ QUERY_PARTS+=("$1")
328
+ shift
329
+ done
330
+ break
331
+ ;;
332
+ -*)
333
+ die "Invalid option: $1"
334
+ ;;
335
+ *)
336
+ QUERY_PARTS+=("$1")
337
+ ;;
338
+ esac
339
+ shift
340
+ done
341
+ }
342
+
343
+ join_query() {
344
+ local query=""
345
+ local part
346
+
347
+ if (( ${#QUERY_PARTS[@]} == 0 )); then
348
+ printf ""
349
+ return
350
+ fi
351
+
352
+ for part in "${QUERY_PARTS[@]}"; do
353
+ if [[ -z "${query}" ]]; then
354
+ query="${part}"
355
+ else
356
+ query+=" ${part}"
357
+ fi
358
+ done
359
+ printf "%s" "${query}"
360
+ }
361
+
362
+ manager_search_entries() {
363
+ local manager="$1"
364
+ local query="$2"
365
+ local effective_query="${query}"
366
+
367
+ if [[ -z "${effective_query}" ]]; then
368
+ case "${manager}" in
369
+ apt|dnf|pacman|zypper|emerge|snap|flatpak|npm|bun)
370
+ effective_query="a"
371
+ ;;
372
+ brew)
373
+ effective_query=""
374
+ ;;
375
+ esac
376
+ fi
377
+
378
+ case "${manager}" in
379
+ apt)
380
+ apt-cache search -- "${effective_query}" 2>/dev/null |
381
+ awk -F' - ' '{ name=$1; desc=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", name); if (desc == "") desc="-"; print name "\t" desc }'
382
+ ;;
383
+ dnf)
384
+ local pattern="*"
385
+ if [[ -n "${effective_query}" ]]; then
386
+ pattern="*${effective_query}*"
387
+ fi
388
+ dnf -q list available "${pattern}" 2>/dev/null |
389
+ awk 'NR > 1 && $1 !~ /^(Available|Last|Installed)/ { name=$1; sub(/\.[^.]+$/, "", name); print name "\t" $2 }'
390
+ ;;
391
+ pacman)
392
+ pacman -Ss -- "${effective_query}" 2>/dev/null |
393
+ awk '
394
+ NR % 2 == 1 {
395
+ split($1, parts, "/")
396
+ pkg = parts[2]
397
+ next
398
+ }
399
+ NR % 2 == 0 {
400
+ line = $0
401
+ sub(/^[[:space:]]+/, "", line)
402
+ if (pkg != "") print pkg "\t" line
403
+ }
404
+ '
405
+ ;;
406
+ zypper)
407
+ zypper --non-interactive --quiet search --details --type package "${effective_query}" 2>/dev/null |
408
+ awk -F'|' '
409
+ /^[[:space:]]*[ivp ][[:space:]]*\|/ {
410
+ name=$3
411
+ ver=$5
412
+ repo=$7
413
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
414
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", ver)
415
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", repo)
416
+ if (name != "") print name "\tversion " ver " from " repo
417
+ }
418
+ '
419
+ ;;
420
+ emerge)
421
+ emerge --searchdesc --color=n "${effective_query}" 2>/dev/null |
422
+ awk '
423
+ /^\* / {
424
+ atom=$2
425
+ desc="-"
426
+ next
427
+ }
428
+ /^[[:space:]]+Description:/ {
429
+ line=$0
430
+ sub(/^[[:space:]]+Description:[[:space:]]*/, "", line)
431
+ desc=line
432
+ if (atom != "") print atom "\t" desc
433
+ atom=""
434
+ }
435
+ '
436
+ ;;
437
+ brew)
438
+ brew search "${effective_query}" 2>/dev/null |
439
+ awk 'NF > 0 { print $1 "\t-" }'
440
+ ;;
441
+ snap)
442
+ snap find "${effective_query}" 2>/dev/null |
443
+ awk '
444
+ NR == 1 { next }
445
+ NF > 0 {
446
+ name=$1
447
+ $1=""
448
+ sub(/^[[:space:]]+/, "", $0)
449
+ if ($0 == "") $0 = "-"
450
+ print name "\t" $0
451
+ }
452
+ '
453
+ ;;
454
+ flatpak)
455
+ if [[ -z "${effective_query}" ]]; then
456
+ {
457
+ flatpak remote-ls --app --columns=application,description flathub 2>/dev/null ||
458
+ flatpak remote-ls --app --columns=application,description 2>/dev/null
459
+ } |
460
+ awk 'NR > 1 { name=$1; $1=""; sub(/^[[:space:]]+/, "", $0); if ($0 == "") $0="-"; print name "\t" $0 }'
461
+ else
462
+ {
463
+ flatpak search --columns=application,description "${effective_query}" 2>/dev/null ||
464
+ flatpak search "${effective_query}" 2>/dev/null
465
+ } |
466
+ awk 'NR > 1 { name=$1; $1=""; sub(/^[[:space:]]+/, "", $0); if ($0 == "") $0="-"; print name "\t" $0 }'
467
+ fi
468
+ ;;
469
+ npm)
470
+ npm search "${effective_query}" --searchlimit=500 --parseable 2>/dev/null |
471
+ awk -F'\t' 'NF >= 2 { print $1 "\t" $2 }'
472
+ ;;
473
+ bun)
474
+ {
475
+ if bun search "${effective_query}" >/dev/null 2>&1; then
476
+ bun search "${effective_query}" 2>/dev/null |
477
+ awk 'NR > 1 && NF > 0 { name=$1; $1=""; sub(/^[[:space:]]+/, "", $0); if ($0 == "") $0="-"; print name "\t" $0 }'
478
+ fi
479
+ if command_exists npm; then
480
+ npm search "${effective_query}" --searchlimit=500 --parseable 2>/dev/null |
481
+ awk -F'\t' 'NF >= 2 { print $1 "\t" $2 }'
482
+ fi
483
+ } || true
484
+ ;;
485
+ esac | awk -F'\t' 'NF >= 1 { if ($2 == "") $2 = "-"; print $1 "\t" $2 }' | sort -u || true
486
+ }
487
+
488
+ manager_installed_entries() {
489
+ local manager="$1"
490
+
491
+ case "${manager}" in
492
+ apt)
493
+ dpkg-query -W -f='${binary:Package}\t${Version}\n' 2>/dev/null
494
+ ;;
495
+ dnf)
496
+ dnf -q list installed 2>/dev/null |
497
+ awk 'NR > 1 && $1 !~ /^(Installed|Last)/ { name=$1; sub(/\.[^.]+$/, "", name); print name "\t" $2 }'
498
+ ;;
499
+ pacman)
500
+ pacman -Q 2>/dev/null |
501
+ awk '{ print $1 "\t" $2 }'
502
+ ;;
503
+ zypper)
504
+ zypper --non-interactive --quiet search --installed-only --details --type package 2>/dev/null |
505
+ awk -F'|' '
506
+ /^[[:space:]]*i[[:space:]]*\|/ {
507
+ name=$3
508
+ ver=$5
509
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
510
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", ver)
511
+ if (name != "") print name "\t" ver
512
+ }
513
+ '
514
+ ;;
515
+ emerge)
516
+ if command_exists qlist; then
517
+ qlist -ICv 2>/dev/null |
518
+ awk '{ print $1 "\tinstalled" }'
519
+ else
520
+ local pkg_dir
521
+ for pkg_dir in /var/db/pkg/*/*; do
522
+ [[ -d "${pkg_dir}" ]] || continue
523
+ printf "%s\tinstalled\n" "$(basename "${pkg_dir}")"
524
+ done
525
+ fi
526
+ ;;
527
+ brew)
528
+ brew list --versions 2>/dev/null |
529
+ awk '{ name=$1; $1=""; sub(/^[[:space:]]+/, "", $0); if ($0 == "") $0="installed"; print name "\t" $0 }'
530
+ ;;
531
+ snap)
532
+ snap list 2>/dev/null |
533
+ awk 'NR > 1 { print $1 "\t" $2 }'
534
+ ;;
535
+ flatpak)
536
+ flatpak list --app --columns=application,version 2>/dev/null |
537
+ awk 'NR > 1 { print $1 "\t" $2 }'
538
+ ;;
539
+ npm)
540
+ npm ls -g --depth=0 --parseable 2>/dev/null |
541
+ awk -F'/' 'NR > 1 { print $NF "\tglobal" }'
542
+ ;;
543
+ bun)
544
+ {
545
+ if bun pm ls --global >/dev/null 2>&1; then
546
+ bun pm ls --global 2>/dev/null |
547
+ awk 'NR > 1 && NF > 0 { print $1 "\tglobal" }'
548
+ elif bun pm ls >/dev/null 2>&1; then
549
+ bun pm ls 2>/dev/null |
550
+ awk 'NR > 1 && NF > 0 { print $1 "\tglobal" }'
551
+ fi
552
+ if command_exists npm; then
553
+ npm ls -g --depth=0 --parseable 2>/dev/null |
554
+ awk -F'/' 'NR > 1 { print $NF "\tglobal" }'
555
+ fi
556
+ } || true
557
+ ;;
558
+ esac | sort -u || true
559
+ }
560
+
561
+ manager_installed_names() {
562
+ local manager="$1"
563
+ manager_installed_entries "${manager}" | awk -F'\t' 'NF > 0 { print $1 }'
564
+ }
565
+
566
+ mark_installed_packages() {
567
+ local manager="$1"
568
+ local source_file="$2"
569
+ local output_file="$3"
570
+ local installed_file
571
+
572
+ installed_file="$(mktemp "${TMP_ROOT}/installed.XXXXXX")"
573
+ manager_installed_names "${manager}" >"${installed_file}" 2>/dev/null || true
574
+
575
+ awk -F'\t' '
576
+ FILENAME == ARGV[1] {
577
+ if ($1 != "") {
578
+ installed[$1] = 1
579
+ }
580
+ next
581
+ }
582
+ {
583
+ mark = (installed[$1] ? "* " : " ")
584
+ desc = $2
585
+ if (desc == "") desc = "-"
586
+ print $1 "\t" mark desc
587
+ }
588
+ ' "${installed_file}" "${source_file}" >"${output_file}"
589
+
590
+ rm -f "${installed_file}"
591
+ }
592
+
593
+ manager_preview_command() {
594
+ local manager="$1"
595
+
596
+ case "${manager}" in
597
+ apt)
598
+ printf '%s' 'pkg={1}; apt-cache show "$pkg" 2>/dev/null; printf "\n"; dpkg -L "$pkg" 2>/dev/null'
599
+ ;;
600
+ dnf)
601
+ printf '%s' 'pkg={1}; dnf info "$pkg" 2>/dev/null; printf "\n"; rpm -ql "$pkg" 2>/dev/null'
602
+ ;;
603
+ pacman)
604
+ printf '%s' 'pkg={1}; pacman -Si "$pkg" 2>/dev/null; printf "\n"; pacman -Fl "$pkg" 2>/dev/null | awk "{print \$2}"'
605
+ ;;
606
+ zypper)
607
+ printf '%s' 'pkg={1}; zypper --non-interactive info "$pkg" 2>/dev/null'
608
+ ;;
609
+ emerge)
610
+ printf '%s' 'pkg={1}; emerge --search --color=n "$pkg" 2>/dev/null'
611
+ ;;
612
+ brew)
613
+ printf '%s' 'pkg={1}; brew info "$pkg" 2>/dev/null'
614
+ ;;
615
+ snap)
616
+ printf '%s' 'pkg={1}; snap info "$pkg" 2>/dev/null'
617
+ ;;
618
+ flatpak)
619
+ printf '%s' 'pkg={1}; flatpak info "$pkg" 2>/dev/null || flatpak remote-info flathub "$pkg" 2>/dev/null'
620
+ ;;
621
+ npm)
622
+ printf '%s' 'pkg={1}; npm view "$pkg" 2>/dev/null'
623
+ ;;
624
+ bun)
625
+ printf '%s' 'pkg={1}; bun info "$pkg" 2>/dev/null || npm view "$pkg" 2>/dev/null'
626
+ ;;
627
+ esac
628
+ }
629
+
630
+ manager_install() {
631
+ local manager="$1"
632
+ shift
633
+
634
+ case "${manager}" in
635
+ apt)
636
+ run_as_root apt-get install -y "$@"
637
+ ;;
638
+ dnf)
639
+ run_as_root dnf install -y "$@"
640
+ ;;
641
+ pacman)
642
+ run_as_root pacman -S --needed "$@"
643
+ ;;
644
+ zypper)
645
+ run_as_root zypper --non-interactive install --auto-agree-with-licenses "$@"
646
+ ;;
647
+ emerge)
648
+ run_as_root emerge --ask=n --verbose "$@"
649
+ ;;
650
+ brew)
651
+ brew install "$@"
652
+ ;;
653
+ snap)
654
+ local pkg
655
+ for pkg in "$@"; do
656
+ run_as_root snap install "${pkg}" 2>/dev/null || run_as_root snap install --classic "${pkg}"
657
+ done
658
+ ;;
659
+ flatpak)
660
+ local pkg
661
+ for pkg in "$@"; do
662
+ flatpak install -y --user flathub "${pkg}" 2>/dev/null || flatpak install -y --user "${pkg}"
663
+ done
664
+ ;;
665
+ npm)
666
+ npm install -g "$@"
667
+ ;;
668
+ bun)
669
+ bun add -g "$@"
670
+ ;;
671
+ esac
672
+ }
673
+
674
+ manager_remove() {
675
+ local manager="$1"
676
+ shift
677
+
678
+ case "${manager}" in
679
+ apt)
680
+ run_as_root apt-get remove -y "$@"
681
+ ;;
682
+ dnf)
683
+ run_as_root dnf remove -y "$@"
684
+ ;;
685
+ pacman)
686
+ run_as_root pacman -Rsn "$@"
687
+ ;;
688
+ zypper)
689
+ run_as_root zypper --non-interactive remove "$@"
690
+ ;;
691
+ emerge)
692
+ run_as_root emerge --ask=n --deselect "$@"
693
+ run_as_root emerge --ask=n --depclean "$@"
694
+ ;;
695
+ brew)
696
+ brew uninstall "$@"
697
+ ;;
698
+ snap)
699
+ run_as_root snap remove "$@"
700
+ ;;
701
+ flatpak)
702
+ flatpak uninstall -y --user "$@"
703
+ ;;
704
+ npm)
705
+ npm uninstall -g "$@"
706
+ ;;
707
+ bun)
708
+ bun remove --global "$@" 2>/dev/null || bun remove "$@"
709
+ ;;
710
+ esac
711
+ }
712
+
713
+ manager_show_info() {
714
+ local manager="$1"
715
+ local package="$2"
716
+
717
+ case "${manager}" in
718
+ apt)
719
+ cat <(apt-cache show "${package}" 2>/dev/null) <(printf "\n") <(dpkg -L "${package}" 2>/dev/null)
720
+ ;;
721
+ dnf)
722
+ cat <(dnf info "${package}" 2>/dev/null) <(printf "\n") <(rpm -ql "${package}" 2>/dev/null)
723
+ ;;
724
+ pacman)
725
+ cat <(pacman -Qi "${package}" 2>/dev/null || pacman -Si "${package}" 2>/dev/null) <(printf "\n") <(pacman -Ql "${package}" 2>/dev/null)
726
+ ;;
727
+ zypper)
728
+ zypper --non-interactive info "${package}" 2>/dev/null
729
+ ;;
730
+ emerge)
731
+ emerge --search --color=n "${package}" 2>/dev/null
732
+ ;;
733
+ brew)
734
+ brew info "${package}" 2>/dev/null
735
+ ;;
736
+ snap)
737
+ snap info "${package}" 2>/dev/null
738
+ ;;
739
+ flatpak)
740
+ flatpak info "${package}" 2>/dev/null || flatpak remote-info flathub "${package}" 2>/dev/null
741
+ ;;
742
+ npm)
743
+ npm view "${package}" 2>/dev/null
744
+ ;;
745
+ bun)
746
+ bun info "${package}" 2>/dev/null || npm view "${package}" 2>/dev/null
747
+ ;;
748
+ esac
749
+ }
750
+
751
+ manager_update() {
752
+ local manager="$1"
753
+
754
+ case "${manager}" in
755
+ apt)
756
+ run_as_root apt-get update
757
+ run_as_root apt-get upgrade -y
758
+ ;;
759
+ dnf)
760
+ run_as_root dnf upgrade -y
761
+ ;;
762
+ pacman)
763
+ run_as_root pacman -Syu
764
+ ;;
765
+ zypper)
766
+ run_as_root zypper --non-interactive refresh
767
+ run_as_root zypper --non-interactive update
768
+ ;;
769
+ emerge)
770
+ run_as_root emerge --sync
771
+ run_as_root emerge --ask=n --update --deep --newuse @world
772
+ ;;
773
+ brew)
774
+ brew update
775
+ brew upgrade
776
+ ;;
777
+ snap)
778
+ run_as_root snap refresh
779
+ ;;
780
+ flatpak)
781
+ flatpak update -y --user
782
+ ;;
783
+ npm)
784
+ npm update -g
785
+ ;;
786
+ bun)
787
+ bun update --global 2>/dev/null || bun update
788
+ ;;
789
+ esac
790
+ }
791
+
792
+ confirm_action() {
793
+ local prompt="$1"
794
+ local reply=""
795
+
796
+ printf "%s [y/N]: " "${prompt}" >&2
797
+ read -r reply || true
798
+
799
+ case "${reply}" in
800
+ y|Y|yes|YES)
801
+ return 0
802
+ ;;
803
+ *)
804
+ return 1
805
+ ;;
806
+ esac
807
+ }
808
+
809
+ run_fuzzy_selector() {
810
+ local manager="$1"
811
+ local query="$2"
812
+ local input_file="$3"
813
+ local header_line="$4"
814
+ local preview_cmd
815
+
816
+ preview_cmd="$(manager_preview_command "${manager}")"
817
+
818
+ fzf -q "${query}" -e -m \
819
+ --delimiter=$'\t' \
820
+ --with-nth=1,2 \
821
+ --preview="${preview_cmd}" \
822
+ --preview-window=55%:wrap:border-sharp \
823
+ --layout=reverse \
824
+ --marker='>>' \
825
+ --header="${header_line}" \
826
+ --info=hidden \
827
+ --margin="2%,1%,2%,1%" \
828
+ --cycle \
829
+ --bind=ctrl-k:preview:"cat ${KBINDS_FILE}" \
830
+ --bind=ctrl-h:preview:"cat ${HELP_FILE}" \
831
+ --bind='ctrl-/:change-preview-window(hidden|)' \
832
+ --bind=ctrl-n:next-selected,ctrl-b:prev-selected \
833
+ --bind='focus:transform-preview-label:echo package: {1}' \
834
+ <"${input_file}"
835
+ }
836
+
837
+ collect_selected_packages() {
838
+ local selected_lines="$1"
839
+ printf "%s\n" "${selected_lines}" | awk -F'\t' 'NF > 0 { print $1 }'
840
+ }
841
+
842
+ main() {
843
+ ensure_tmp_root
844
+
845
+ parse_args "$@"
846
+
847
+ local detected_manager
848
+ detected_manager="$(detect_default_manager)"
849
+
850
+ build_help_file "${detected_manager}"
851
+ build_keybind_file
852
+
853
+ if [[ "${ACTION}" == "help" ]]; then
854
+ print_help
855
+ exit 0
856
+ fi
857
+
858
+ local manager="${detected_manager}"
859
+ if [[ -n "${MANAGER_OVERRIDE}" ]]; then
860
+ manager="$(normalize_manager "${MANAGER_OVERRIDE}")"
861
+ fi
862
+
863
+ manager_supported "${manager}" || die "Unsupported manager: ${manager}"
864
+ manager_command_ready "${manager}" || die "Manager command(s) for '${manager}' not found on this system"
865
+
866
+ local query
867
+ query="$(join_query)"
868
+
869
+ if [[ "${ACTION}" == "update" ]]; then
870
+ log "Using manager: $(manager_label "${manager}")"
871
+ if confirm_action "Run update/upgrade for ${manager}?"; then
872
+ manager_update "${manager}"
873
+ else
874
+ log "Update canceled"
875
+ fi
876
+ exit 0
877
+ fi
878
+
879
+ command_exists fzf || die "fzf is required"
880
+
881
+ local source_file
882
+ local display_file
883
+ source_file="$(mktemp "${TMP_ROOT}/source.XXXXXX")"
884
+ display_file="$(mktemp "${TMP_ROOT}/display.XXXXXX")"
885
+
886
+ if [[ "${ACTION}" == "list" || "${ACTION}" == "remove" ]]; then
887
+ manager_installed_entries "${manager}" >"${source_file}"
888
+ cp "${source_file}" "${display_file}"
889
+ else
890
+ manager_search_entries "${manager}" "${query}" >"${source_file}"
891
+ mark_installed_packages "${manager}" "${source_file}" "${display_file}"
892
+ fi
893
+
894
+ if [[ ! -s "${display_file}" ]]; then
895
+ rm -f "${source_file}" "${display_file}"
896
+ die "No packages found for manager '${manager}' and query '${query}'"
897
+ fi
898
+
899
+ local header
900
+ case "${ACTION}" in
901
+ search)
902
+ header="Select package(s) to install with ${manager} (TAB to multi-select)"
903
+ ;;
904
+ list)
905
+ header="Select installed package(s) to inspect"
906
+ ;;
907
+ remove)
908
+ header="Select installed package(s) to remove"
909
+ ;;
910
+ *)
911
+ header="Select package(s)"
912
+ ;;
913
+ esac
914
+
915
+ local selected
916
+ selected="$(run_fuzzy_selector "${manager}" "${query}" "${display_file}" "${header}" || true)"
917
+
918
+ rm -f "${source_file}" "${display_file}"
919
+
920
+ if [[ -z "${selected}" ]]; then
921
+ log "No package selected"
922
+ exit 0
923
+ fi
924
+
925
+ local packages=()
926
+ local selected_pkg
927
+ while IFS= read -r selected_pkg; do
928
+ [[ -n "${selected_pkg}" ]] || continue
929
+ packages+=("${selected_pkg}")
930
+ done < <(collect_selected_packages "${selected}")
931
+
932
+ if [[ "${#packages[@]}" -eq 0 ]]; then
933
+ log "No package selected"
934
+ exit 0
935
+ fi
936
+
937
+ case "${ACTION}" in
938
+ search)
939
+ if confirm_action "Install ${#packages[@]} package(s) with ${manager}?"; then
940
+ manager_install "${manager}" "${packages[@]}"
941
+ else
942
+ log "Install canceled"
943
+ fi
944
+ ;;
945
+ remove)
946
+ if confirm_action "Remove ${#packages[@]} package(s) with ${manager}?"; then
947
+ manager_remove "${manager}" "${packages[@]}"
948
+ else
949
+ log "Remove canceled"
950
+ fi
951
+ ;;
952
+ list)
953
+ local pkg
954
+ for pkg in "${packages[@]}"; do
955
+ printf "\n=== %s (%s) ===\n" "${pkg}" "$(manager_label "${manager}")"
956
+ manager_show_info "${manager}" "${pkg}" || true
957
+ done
958
+ ;;
959
+ esac
960
+ }
961
+
962
+ main "$@"