#!/usr/bin/env bash # terminal application launcher for sway, using fzf # Based on: https://gitlab.com/FlyingWombat/my-scripts/blob/master/sway-launcher # https://gist.github.com/Biont/40ef59652acf3673520c7a03c9f22d2a shopt -s nullglob globstar set -o pipefail if ! { exec 0>&3; } 1>/dev/null 2>&1; then exec 3>/dev/null # If file descriptor 3 is unused in parent shell, output to /dev/null fi # shellcheck disable=SC2154 trap 's=$?; echo "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR IFS=$'\n\t' DEL=$'\34' TERMINAL_COMMAND="${TERMINAL_COMMAND:="$TERM -e"}" GLYPH_COMMAND=" " GLYPH_DESKTOP=" " CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/sway-launcher-desktop" PROVIDERS_FILE="${PROVIDERS_FILE:=providers.conf}" if [[ "${PROVIDERS_FILE#/}" == "${PROVIDERS_FILE}" ]]; then # $PROVIDERS_FILE is a relative path, prepend $CONFIG_DIR PROVIDERS_FILE="${CONFIG_DIR}/${PROVIDERS_FILE}" fi # Provider config entries are separated by the field separator \034 and have the following structure: # list_cmd,preview_cmd,launch_cmd declare -A PROVIDERS if [ -f "${PROVIDERS_FILE}" ]; then eval "$(awk -F= ' BEGINFILE{ provider=""; } /^\[.*\]/{sub("^\\[", "");sub("\\]$", "");provider=$0} /^(launch|list|preview)_cmd/{st = index($0,"=");providers[provider][$1] = substr($0,st+1)} ENDFILE{ for (key in providers){ if(!("list_cmd" in providers[key])){continue;} if(!("launch_cmd" in providers[key])){continue;} if(!("preview_cmd" in providers[key])){continue;} for (entry in providers[key]){ gsub(/[\x27,\047]/,"\x27\"\x27\"\x27", providers[key][entry]) } print "PROVIDERS[\x27" key "\x27]=\x27" providers[key]["list_cmd"] "\034" providers[key]["preview_cmd"] "\034" providers[key]["launch_cmd"] "\x27\n" } }' "${PROVIDERS_FILE}")" HIST_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/${0##*/}-${PROVIDERS_FILE##*/}-history.txt" else PROVIDERS['desktop']="${0} list-entries${DEL}${0} describe-desktop \"{1}\"${DEL}${0} run-desktop '{1}' {2}" PROVIDERS['command']="${0} list-commands${DEL}${0} describe-command \"{1}\"${DEL}${TERMINAL_COMMAND} {1}" HIST_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/${0##*/}-history.txt" fi mkdir -p "${HIST_FILE%/*}" && touch "$HIST_FILE" readarray HIST_LINES <"$HIST_FILE" function describe() { # shellcheck disable=SC2086 readarray -d ${DEL} -t PROVIDER_ARGS <<<${PROVIDERS[${1}]} # shellcheck disable=SC2086 [ -n "${PROVIDER_ARGS[1]}" ] && eval "${PROVIDER_ARGS[1]//\{1\}/${2}}" } function describe-desktop() { description=$(sed -ne '/^Comment=/{s/^Comment=//;p;q}' "$1") echo -e "\033[33m$(sed -ne '/^Name=/{s/^Name=//;p;q}' "$1")\033[0m" echo "${description:-No description}" } function describe-command() { readarray arr < <(whatis -l "$1" 2>/dev/null) description="${arr[0]}" description="${description#* - }" echo -e "\033[33m${1}\033[0m" echo "${description:-No description}" } function provide() { # shellcheck disable=SC2086 readarray -d ${DEL} -t PROVIDER_ARGS <<<${PROVIDERS[$1]} eval "${PROVIDER_ARGS[0]}" } function list-commands() { IFS=: read -ra path <<<"$PATH" for dir in "${path[@]}"; do printf '%s\n' "$dir/"* | awk -F / -v pre="$GLYPH_COMMAND" '{print $NF "\034command\034\033[31m" pre "\033[0m" $NF;}' done | sort -u } function list-entries() { # Get locations of desktop application folders according to spec # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html IFS=':' read -ra DIRS <<<"${XDG_DATA_HOME-${HOME}/.local/share}:${XDG_DATA_DIRS-/usr/local/share:/usr/share}" for i in "${!DIRS[@]}"; do if [[ ! -d "${DIRS[i]}" ]]; then unset -v 'DIRS[$i]' else DIRS[$i]="${DIRS[i]}/applications/**/*.desktop" fi done # shellcheck disable=SC2068 entries ${DIRS[@]} } function entries() { # shellcheck disable=SC2068 awk -v pre="$GLYPH_DESKTOP" -F= ' function desktopFileID(filename){ sub("^.*applications/", "", filename); sub("/", "-", filename); return filename } BEGINFILE{ application=0; block=""; a=0 id=desktopFileID(FILENAME) if(id in fileIds){ nextfile; }else{ fileIds[id]=0 } } /^\[Desktop Entry\]/{block="entry"} /^Type=Application/{application=1} /^\[Desktop Action/{ sub("^\\[Desktop Action ", ""); sub("\\]$", ""); block="action"; a++; actions[a,"key"]=$0 } /^Name=/{ (block=="action")? actions[a,"name"]=$2 : name=$2 } ENDFILE{ if (application){ print FILENAME "\034desktop\034\033[33m" pre name "\033[0m"; if (a>0) for (i=1; i<=a; i++) print FILENAME "\034desktop\034\033[33m" pre name "\033[0m (" actions[i, "name"] ")\034" actions[i, "key"] } }' \ $@ &3)" echo "Generated Launch command from .desktop file: ${CMD}" >&3 bash -c "${CMD}" } function generate-command() { # Define the search pattern that specifies the block to search for within the .desktop file PATTERN="^\\\\[Desktop Entry\\\\]" if [[ -n $2 ]]; then PATTERN="^\\\\[Desktop Action ${2}\\\\]" fi echo "Searching for pattern: ${PATTERN}" >&3 # 1. We see a line starting [Desktop, but we're already searching: deactivate search again # 2. We see the specified pattern: start search # 3. We see an Exec= line during search: remove field codes and set variable # 3. We see a Path= line during search: set variable # 4. Finally, build command line awk -v pattern="${PATTERN}" -v terminal_cmd="${TERMINAL_COMMAND}" -F= ' BEGIN{a=0;exec=0;path=0} /^\[Desktop/{ if(a){ a=0 } } $0 ~ pattern{ a=1 } /^Terminal=/{ sub("^Terminal=", ""); if ($0 == "true") { terminal=1 } } /^Exec=/{ if(a && !exec){ sub("^Exec=", ""); gsub(" ?%[cDdFfikmNnUuv]", ""); exec=$0; } } /^Path=/{ if(a && !path){ path=$2 } } END{ if(path){ printf "cd " path " && " } if (terminal){ printf terminal_cmd " " } print exec }' "$1" } function autostart() { for application in $(list-autostart); do (exec setsid /bin/sh -c "$(run-desktop "${application}")" &>/dev/null &) done } function list-autostart() { # Get locations of desktop application folders according to spec # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html IFS=':' read -ra DIRS <<<"${XDG_CONFIG_HOME-${HOME}/.config}:${XDG_CONFIG_DIRS-/etc/xdg}" for i in "${!DIRS[@]}"; do if [[ ! -d "${DIRS[i]}" ]]; then unset -v 'DIRS[$i]' else DIRS[$i]="${DIRS[i]}/autostart/*.desktop" fi done # shellcheck disable=SC2068 awk -v pre="$GLYPH_DESKTOP" -F= ' function desktopFileID(filename){ sub("^.*autostart/", "", filename); sub("/", "-", filename); return filename } BEGINFILE{ application=0; block=""; a=0 id=desktopFileID(FILENAME) if(id in fileIds){ nextfile; }else{ fileIds[id]=0 } } /^\[Desktop Entry\]/{block="entry"} /^Type=Application/{application=1} /^Name=/{ iname=$2 } ENDFILE{ if (application){ print FILENAME; } }' \ ${DIRS[@]} &3 FZFPIPE=$(mktemp -u) mkfifo "$FZFPIPE" trap 'rm "$FZFPIPE"' EXIT INT # Append Launcher History, removing usage count (printf '%s' "${HIST_LINES[@]#* }" >>"$FZFPIPE") & # Iterate over providers and run their list-command for PROVIDER_NAME in "${!PROVIDERS[@]}"; do (bash -c "${0} provide ${PROVIDER_NAME}" >>"$FZFPIPE") & done COMMAND_STR=$( fzf +s -x -d '\034' --nth ..3 --with-nth 3 \ --preview "$0 describe {2} {1}" \ --preview-window=up:3:wrap --ansi \ <"$FZFPIPE" ) || exit 1 [ -z "$COMMAND_STR" ] && exit 1 # update history for i in "${!HIST_LINES[@]}"; do if [[ "${HIST_LINES[i]}" == *" $COMMAND_STR"$'\n' ]]; then HIST_COUNT=${HIST_LINES[i]%% *} HIST_LINES[$i]="$((HIST_COUNT + 1)) $COMMAND_STR"$'\n' match=1 break fi done if ! ((match)); then HIST_LINES+=("1 $COMMAND_STR"$'\n') fi printf '%s' "${HIST_LINES[@]}" | sort -nr >"$HIST_FILE" # shellcheck disable=SC2086 readarray -d $'\034' -t PARAMS <<<${COMMAND_STR} # shellcheck disable=SC2086 readarray -d ${DEL} -t PROVIDER_ARGS <<<${PROVIDERS[${PARAMS[1]}]} # Substitute {1}, {2} etc with the correct values COMMAND=${PROVIDER_ARGS[2]//\{1\}/${PARAMS[0]}} COMMAND=${COMMAND//\{2\}/${PARAMS[3]}} COMMAND=${COMMAND%%[[:space:]]} echo "Launching command: ${COMMAND}" >&3 setsid /bin/sh -c "${COMMAND}" >& /dev/null < /dev/null & sleep 0.01