-
-
Save Biont/40ef59652acf3673520c7a03c9f22d2a to your computer and use it in GitHub 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" |
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.
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";}' \
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 ensuresfzf
will not print the delimiter - It also prevents any potential problems from the previous
|
character appearing inExec=
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
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.
@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.
Wow, this started out good and got way, way good.... Thanks @Biont (and @DanielVoogsgerd)!
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.
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.
@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.
@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.
@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.
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.
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.