#!/bin/sh
# alloy-menu — pick an installed application and run it.
#
# The launcher for a terminal-first system: a fuzzy picker in a terminal, not a
# graphical surface. docs/STACK.md#launcher rejects graphical launchers on
# thesis and names this shape; this is that.
#
# The split with $mod+Return is the whole design. Commands are typed in a
# shell, which is one keystroke away. This exists for the other case -- the
# graphical apps that have a .desktop file and an icon and a name nobody
# remembers the binary for. So it lists desktop entries and nothing else;
# adding every binary on PATH would bury the browser under coreutils.
#
# Launched apps are handed to sway rather than run as children of this script,
# so the terminal hosting the picker can close immediately and the app is not
# killed with it.

set -eu

FIELD_CODES=' ?%[fFuUickdDnNvm]'

dirs="/usr/share/applications
${XDG_DATA_HOME:-$HOME/.local/share}/applications
/var/lib/flatpak/exports/share/applications
${XDG_DATA_HOME:-$HOME/.local/share}/flatpak/exports/share/applications"

entries() {
  printf '%s\n' "$dirs" | while IFS= read -r dir; do
    [ -d "$dir" ] || continue
    find "$dir" -maxdepth 2 -name '*.desktop' 2>/dev/null
  done | while IFS= read -r file; do
    # Only the [Desktop Entry] group. Actions ([Desktop Action ...]) carry
    # their own Name/Exec and would otherwise be read as separate apps.
    awk -v codes="$FIELD_CODES" '
      /^\[Desktop Entry\]/ { group = 1; next }
      /^\[/                { group = 0 }
      !group               { next }
      /^Name=/      && name == "" { name = substr($0, 6) }
      /^Exec=/      && cmd  == "" { cmd  = substr($0, 6) }
      /^Type=/                    { type = substr($0, 6) }
      /^NoDisplay=[Tt]rue/        { hide = 1 }
      /^Hidden=[Tt]rue/           { hide = 1 }
      # Terminal=true means the entry is a TUI and has no window of its own.
      # On a TUI-first system this is not the rare case: helix, yazi and
      # gammastep all ship one. Launched without a terminal they exit
      # immediately with no output, which reads as the launcher being broken.
      /^Terminal=[Tt]rue/         { needs_term = 1 }
      END {
        if (hide || type != "Application" || name == "" || cmd == "") exit
        gsub(codes, "", cmd)
        printf "%s\t%d\t%s\n", name, needs_term, cmd
      }
    ' "$file"
  done | sort -u -k1,1 -t "$(printf '\t')"
}

choice=$(entries | fzf \
  --delimiter='\t' --with-nth=1 \
  --prompt='run ' --pointer='>' --marker=' ' \
  --layout=reverse --no-multi --height=100% --border=none --info=inline) || exit 0

[ -n "$choice" ] || exit 0
needs_term=$(printf '%s' "$choice" | cut -f2)
cmd=$(printf '%s' "$choice" | cut -f3-)
[ -n "$cmd" ] || exit 0

# TUI entries get a terminal to live in. $TERMINAL so this stays true if the
# terminal pick ever changes; shop is the STACK.md default.
if [ "$needs_term" = "1" ]; then
  cmd="${TERMINAL:-shop} -e $cmd"
fi

# swaymsg runs it through sh, so a full Exec line keeps its arguments. Outside
# a sway session (a bare tty, or another compositor) fall back to detaching it
# here, so the picker is not silently useless.
if [ -n "${SWAYSOCK:-}" ] && command -v swaymsg >/dev/null 2>&1; then
  swaymsg exec "$cmd" >/dev/null
else
  setsid sh -c "$cmd" >/dev/null 2>&1 &
fi
