Skip to main content

max / alloy

3.2 KB · 81 lines History Blame Raw
1 #!/bin/sh
2 # alloy-menu — pick an installed application and run it.
3 #
4 # The launcher for a terminal-first system: a fuzzy picker in a terminal, not a
5 # graphical surface. docs/STACK.md#launcher rejects graphical launchers on
6 # thesis and names this shape; this is that.
7 #
8 # The split with $mod+Return is the whole design. Commands are typed in a
9 # shell, which is one keystroke away. This exists for the other case -- the
10 # graphical apps that have a .desktop file and an icon and a name nobody
11 # remembers the binary for. So it lists desktop entries and nothing else;
12 # adding every binary on PATH would bury the browser under coreutils.
13 #
14 # Launched apps are handed to sway rather than run as children of this script,
15 # so the terminal hosting the picker can close immediately and the app is not
16 # killed with it.
17
18 set -eu
19
20 FIELD_CODES=' ?%[fFuUickdDnNvm]'
21
22 dirs="/usr/share/applications
23 ${XDG_DATA_HOME:-$HOME/.local/share}/applications
24 /var/lib/flatpak/exports/share/applications
25 ${XDG_DATA_HOME:-$HOME/.local/share}/flatpak/exports/share/applications"
26
27 entries() {
28 printf '%s\n' "$dirs" | while IFS= read -r dir; do
29 [ -d "$dir" ] || continue
30 find "$dir" -maxdepth 2 -name '*.desktop' 2>/dev/null
31 done | while IFS= read -r file; do
32 # Only the [Desktop Entry] group. Actions ([Desktop Action ...]) carry
33 # their own Name/Exec and would otherwise be read as separate apps.
34 awk -v codes="$FIELD_CODES" '
35 /^\[Desktop Entry\]/ { group = 1; next }
36 /^\[/ { group = 0 }
37 !group { next }
38 /^Name=/ && name == "" { name = substr($0, 6) }
39 /^Exec=/ && cmd == "" { cmd = substr($0, 6) }
40 /^Type=/ { type = substr($0, 6) }
41 /^NoDisplay=[Tt]rue/ { hide = 1 }
42 /^Hidden=[Tt]rue/ { hide = 1 }
43 # Terminal=true means the entry is a TUI and has no window of its own.
44 # On a TUI-first system this is not the rare case: helix, yazi and
45 # gammastep all ship one. Launched without a terminal they exit
46 # immediately with no output, which reads as the launcher being broken.
47 /^Terminal=[Tt]rue/ { needs_term = 1 }
48 END {
49 if (hide || type != "Application" || name == "" || cmd == "") exit
50 gsub(codes, "", cmd)
51 printf "%s\t%d\t%s\n", name, needs_term, cmd
52 }
53 ' "$file"
54 done | sort -u -k1,1 -t "$(printf '\t')"
55 }
56
57 choice=$(entries | fzf \
58 --delimiter='\t' --with-nth=1 \
59 --prompt='run ' --pointer='>' --marker=' ' \
60 --layout=reverse --no-multi --height=100% --border=none --info=inline) || exit 0
61
62 [ -n "$choice" ] || exit 0
63 needs_term=$(printf '%s' "$choice" | cut -f2)
64 cmd=$(printf '%s' "$choice" | cut -f3-)
65 [ -n "$cmd" ] || exit 0
66
67 # TUI entries get a terminal to live in. $TERMINAL so this stays true if the
68 # terminal pick ever changes; shop is the STACK.md default.
69 if [ "$needs_term" = "1" ]; then
70 cmd="${TERMINAL:-shop} -e $cmd"
71 fi
72
73 # swaymsg runs it through sh, so a full Exec line keeps its arguments. Outside
74 # a sway session (a bare tty, or another compositor) fall back to detaching it
75 # here, so the picker is not silently useless.
76 if [ -n "${SWAYSOCK:-}" ] && command -v swaymsg >/dev/null 2>&1; then
77 swaymsg exec "$cmd" >/dev/null
78 else
79 setsid sh -c "$cmd" >/dev/null 2>&1 &
80 fi
81