Skip to content

Instantly share code, notes, and snippets.

@Biont
Last active March 9, 2024 23:50
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save Biont/40ef59652acf3673520c7a03c9f22d2a to your computer and use it in GitHub Desktop.
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"
@DanielVoogsgerd
Copy link

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
Copy link

DanielVoogsgerd commented Oct 15, 2019

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
Copy link
Author

Biont commented Oct 16, 2019

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
Copy link

DanielVoogsgerd commented Oct 16, 2019

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
Copy link
Author

Biont commented Oct 17, 2019

@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

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

@DanielVoogsgerd
Copy link

DanielVoogsgerd commented Oct 18, 2019

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
Copy link

nstickney commented Oct 19, 2019

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

@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
Copy link
Author

Biont commented Oct 22, 2019

@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

@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
Copy link

Emaleth commented Jul 15, 2021

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
Copy link
Author

Biont commented Jul 15, 2021

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