-
-
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" |
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
\034character. This ensuresfzfwill 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
awkto search, falling back to/^\[Desktop Entryif 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
fzfkeybindings - 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.
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 %uto 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.