-
-
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" |
Thanks @nstickney! Your changes make sense and I added them in.
Amazing work, the awking on the desktop files might need some improvement though. It fails on desktop files with multiple actions, like Firefox. For me it only parses the last action and does not show the application name itself, only the name of the action. Which is quite confusing for everything that has the action "New Window", "New tab", etc. Below an example:
[Desktop Entry]
Version=1.0
Name=Firefox
GenericName=Web Browser
Comment=Browse the Web
Exec=/usr/lib/firefox/firefox %u
Icon=firefox
Type=Application
Actions=new-window;new-private-window;
[Desktop Action new-window]
Name=New Window
Exec=/usr/lib/firefox/firefox --new-window %u
[Desktop Action new-private-window]
Name=New Private Window
Exec=/usr/lib/firefox/firefox --private-window %u
Hey @DanielVoogsgerd I noticed that as well. Please check if the updated script works better for you
@DanielVoogsgerd @nstickney Is this on the very latest revision and with a cleared history file?
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.
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.
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.
Revisions here: https://gist.github.com/nstickney/1fef842bb9786284284a9549959553b1 (if you copy, I will delete).