Skip to content

Instantly share code, notes, and snippets.

@Biont
Last active August 31, 2026 07:49
Show Gist options
  • Select an option

  • Save Biont/40ef59652acf3673520c7a03c9f22d2a to your computer and use it in GitHub Desktop.

Select an option

Save Biont/40ef59652acf3673520c7a03c9f22d2a to your computer and use it in GitHub Desktop.
sway-launcher-desktop
#!/usr/bin/env bash
# terminal application launcher for sway, using fzf
# Based on: https://gitlab.com/FlyingWombat/my-scripts/blob/master/sway-launcher
shopt -s nullglob
if [[ "$1" == 'describe' ]]; then
shift
if [[ $2 == 'command' ]]; then
title=$1
readarray arr < <(whatis -l "$1" 2>/dev/null)
description="${arr[0]}"
description="${description%*-}"
else
title=$(sed -ne '/^Name=/{s/^Name=//;p;q}' "$1")
description=$(sed -ne '/^Comment=/{s/^Comment=//;p;q}' "$1")
fi
echo -e "\033[33m$title\033[0m"
echo "${description:-No description}"
exit
fi
HIST_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/${0##*/}-history.txt"
DIRS=(
/usr/share/applications
"$HOME/.local/share/applications"
/usr/local/share/applications
)
GLYPH_COMMAND=""
GLYPH_DESKTOP=""
touch "$HIST_FILE"
readarray HIST_LINES <"$HIST_FILE"
FZFPIPE=$(mktemp)
PIDFILE=$(mktemp)
trap 'rm "$FZFPIPE" "$PIDFILE"' EXIT INT
# Append Launcher History, removing usage count
(printf '%s' "${HIST_LINES[@]#* }" >>"$FZFPIPE") &
# Load and append Desktop entries
(
for dir in "${DIRS[@]}"; do
[[ -d "$dir" ]] || continue
awk -v pre="$GLYPH_DESKTOP" -F= '
BEGINFILE{application=0;block="";a=0}
/^\[Desktop Entry\]/{block="entry"}
/^Type=Application/{application=1}
/^\[Desktop Action/{
sub("^\\[Desktop Action ", "");
sub("\\]$", "");
block="action";
a++;
actions[a,"key"]=$0
}
/^Name=/{
if(block=="action") {
actions[a,"name"]=$2;
} else {
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"]
}
}' \
"$dir/"*.desktop </dev/null >>"$FZFPIPE"
# the empty stdin is needed in case no *.desktop files
done
) &
# Load and append command list
(
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 >>"$FZFPIPE"
) &
COMMAND_STR=$(
(
tail -n +0 -f "$FZFPIPE" &
echo $! >"$PIDFILE"
) |
fzf +s -x -d '\034' --nth ..3 --with-nth 3 \
--preview "$0 describe {1} {2}" \
--preview-window=up:3:wrap --ansi
kill -9 "$(<"$PIDFILE")" | tail -n1
) || 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"
command='echo "nope"'
# shellcheck disable=SC2086
readarray -d $'\034' -t PARAMS <<<${COMMAND_STR}
# COMMAND_STR is "<string>\034<type>"
case ${PARAMS[1]} in
desktop)
# Define the search pattern that specifies the block to search for within the .desktop file
PATTERN="^\\\\[Desktop Entry\\\\]"
if [[ -n ${PARAMS[3]} ]]; then
PATTERN="^\\\\[Desktop Action ${PARAMS[3]%?}\\\\]"
fi
# 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
command=$(awk -v pattern="${PATTERN}" -F= '
BEGIN{a=0;exec=0; path=0}
/^\[Desktop/{
if(a){
a=0
}
}
$0 ~ pattern{
a=1
}
/^Exec=/{
if(a && !exec){
sub("^Exec=", "");
gsub(" ?%[cDdFfikmNnUuv]", "");
exec=$0;
}
}
/^Path=/{
if(a && !path){
path=$2
}
}
END{
if(path){
print "cd " path " &&"
}
print exec
}' "${PARAMS[0]}")
;;
command)
command="${PARAMS[0]}"
;;
esac
swaymsg -t command exec "$command"
@Biont

Biont commented Oct 15, 2019

Copy link
Copy Markdown
Author

@DanielVoogsgerd @nstickney Is this on the very latest revision and with a cleared history file?

@DanielVoogsgerd

DanielVoogsgerd commented Oct 15, 2019 •

Copy link
Copy Markdown

Correct, I'm also running into a new issue with firefox where it tries to reopen firefox for a second time now. Most programs are fine with that or prefer it even. But Firefox wants --new-window %u to be added if it's already running.

EDIT:
Hmm, the issue seems to have vanished. I'm almost certain I updated it but it seems to be working now. The issue with the desktop actions remains, unfortunately. I would love to help out a bit, but I'm swamped for at least the next week or two.

@Biont

Biont commented Oct 15, 2019 •

Copy link
Copy Markdown
Author

I have also pushed a small update which made the error disappear once I was able to reproduce it.

I'm not sure how to deal with the multiple desktop actions though. I guess the safe thing would be to extract multiple starter items instead of attempting to sport the right one.

@DanielVoogsgerd

Copy link
Copy Markdown

That is what things like i3-dmenu-desktop and alike do. If I can be as bold as to suggest something. I would personally go for something like Firefox (New Window) so that would be <application name> (<action name>). I'm no AWK expert in the slightest. but that might be a hard task to accomplish, but this is gearing more and more towards full fledged ini parsing, which might not be a bad idea after all, but is probably not the best fit for a shell only solution.
I must admit. I kinda feel inspired to do build something in Rust or C if I can find the time in a couple of weeks.

@DanielVoogsgerd

DanielVoogsgerd commented Oct 15, 2019 •

Copy link
Copy Markdown

Okay I might have gotten a little excited. I'm terrible at AWK and learned quite a bit writing this, but this might be an okay start. If it's crap, feel free to ignore it ;)

awk -v pre="$GLYPH_DESKTOP" -F= '
            BEGINFILE{application=0;block="";application_name=""}
            /^\[Desktop Entry\]/{block="entry"}
            /^Type=Application/{application=1}
            /^\[Desktop Action/{block="action";a++}
            /^Name=/{
            if(block=="action") {
                actions[a,"name"]=$2;
            } else {
                name=$2
            }
            }
            ENDFILE{
            if (application)
                if (a>0)
                    for (i=1; i<=a; i++)
                        print FILENAME "|desktop|\033[33m" pre name " (" actions[i, "name"] ")\033[0m"
                else
                    print FILENAME "|desktop|\033[33m" pre name "\033[0m";}' \

@Biont

Biont commented Oct 16, 2019 •

Copy link
Copy Markdown
Author

Awesome, thank you very much! This is pretty much what I had in mind, but I too have a lot to learn using awk. You still need to reset a in BEGINFILE and as far as I can tell the application_name is unused. But this works great as far as extracting the launcher items goes.
We still need to filter out the correct Exec= command when the item is actually run though.

I have just pushed a large update inspired by your post. However, I had to make quite a few changes:

  • For executing a specific entry, we need a machine-friendly way to pass that information, so I added a new column in the line structure
  • This column contains the action specifier (instead of the human-friendly Name= field)
  • Fields are now separated by the non-printable \034 character. This ensures fzf will not print the delimiter
  • It also prevents any potential problems from the previous | character appearing in Exec= statements
  • Using the new action specifier, we can craft a specific pattern for awk to search, falling back to /^\[Desktop Entry if it's not present
  • Then command and working dir are extracted and poof: I can now open a new private tab in Firefox

cc @DanielVoogsgerd @nstickney

@DanielVoogsgerd

DanielVoogsgerd commented Oct 16, 2019 •

Copy link
Copy Markdown

Legend! This is working incredibly well. I'll test it out and see what happens. Again, thanks for all the hard work. This was precisely what I was looking for!

EDIT: Now I'm thinking for it. Maybe listing the GenericName and or Categories in the describe might be a fun extra.

@Biont

Biont commented Oct 17, 2019

Copy link
Copy Markdown
Author

@DanielVoogsgerd
Absolutely. I just did not work on that part yet because it's low-hanging fruit. But I realized that there is more useful info to find in those desktop files and in most cases, there is free space where we can put it.

Other things I am thinking about:

  • Think about how this script could use and benefit from external configuration
  • Implement file search and pass paths to xdg-open. I usually hate when launchers include a file search, but if it's optional and fully configurable (->by passing), I might use it myself.
  • Come up with fun usages of fzf keybindings
  • Implement a function that can be called externally and decrements all history usage entries and deletes a line if it reaches 0. Users could put the command in a cron/systemd-timer and then is would gradually phase out entries that you only rarely need and prevent your history from becoming a mess over time
  • If the above proves to be a useful addition, you might then want to be able to select favourites that never get cleaned up ( and are probably are excluded from the history anyway )

We'll see how much time and motivation I find for these things. If things get serious, this gist should turn into a proper repository, though.

@nstickney

Copy link
Copy Markdown

Wow, this started out good and got way, way good.... Thanks @Biont (and @DanielVoogsgerd)!

@DanielVoogsgerd

DanielVoogsgerd commented Oct 18, 2019 •

Copy link
Copy Markdown

Maybe it's an idea to create a repository from this snippets so issues can be multi-threaded and contributions can be made more easily. Also I'm curious as to which license you want to use to publish to code.
Edit: Whoops, I missed your last remark about the repository.

@nstickney

nstickney commented Oct 19, 2019 •

Copy link
Copy Markdown

So, I've found another issue, and I believe I have a solution, but I'm a n00b here, so check me.

PROBLEM:
Some *.desktop files include not just the command, but also environment variables, in the Exec line, i.e. "Exec=env GDK_BACKEND=x11 /opt/minecraft-launcher/minecraft-launcher". This is apparently a standard pattern, but swaymsg -t command "$command" leads to Error: Unknown/invalid command 'env'. I tried changing the Exec line to /usr/bin/env, but I get a similar message: Error: Unknown/invalid command '/usr/bin/env'.

SOLUTION:
I changed the way the $command variable is formatted by removing the line break and replacing it with a space. Then, when running the command, I cut out env using bash string substitution.

EDIT:
Note, I also changed the ordering of the $DIRS hoping to make the ~/.local entry override the others, but I don't believe it has any effect.

@joefiorini

Copy link
Copy Markdown

@Biont I updated the script to handle Terminal=true in desktop files so they open in a terminal (my original use case for this was opening ranger). Seems like this could be useful in the core script, but it's currently hardcoded to use termite. I could extract it into an environment variable for now, probably the simplest way to solve the problem without having to search for different terminals. Seems like +1 use case for configuration.

@Biont

Biont commented Oct 22, 2019

Copy link
Copy Markdown
Author

@nstickney @joefiorini Thank you both! I have created an actual repository for this project so we can use issues and PRs in the future: https://github.com/Biont/sway-launcher-desktop/tree/master

I will look at your suggestions asap.

@joefiorini

Copy link
Copy Markdown

@Biont I just updated mine to make the terminal command an env var so it's easier to change. I'll send a PR with this update to make it easier for you to review and pull in if you want it.

@Emaleth

Emaleth commented Jul 15, 2021

Copy link
Copy Markdown

is there any way i can make it look into /opt? adding it to DIRS doesn't seem to do the trick... or read .desktop files, that would work as well.

@Biont

Biont commented Jul 15, 2021

Copy link
Copy Markdown
Author

Hi @Emaleth Can you please open an issue over here?

This script has evolved into its own repo and I should probably delete this gist..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment