23 June, 2024
Edit on 16 Oct, 2025 - I have updated launcher from a shell script to a compiled program (rust). reasons were performance. it did not make sense to keep reading so many files again and again, and this approach does not scale well. there would be a updated article sometime in future, but lets just say, if this approach scaled well till about 10,000 entries, newer one scales to 500,000+.
where * refers to applications(or programs), executing commands/scripts, clipboard entries, copying and typing out long/specific/repetitive things, opening files, playing media, bookmarks, searching online, with default and specified search engine with “bangs”, calculations and conversions, and much more
Disclaimer :- This is my first article, and I don’t really know how to write, please forgive me for poor structuring or style or content.
It is essentially a “dmenu” like approach to launch/perform various things. My idea is to “package” it neatly such that it is easy to extend, and easy to use, be flexible in what programs you choose to perform various things, and not require much knowledge of programming to extend
Yes it has been done many times, even my aim was to mimic , basically all “twm users” do it, because these menus are easy and fast, and keyboard driven, my idea is not to replace anything, heck, I would not even call this a program, it is a “script”, it is just easy to use, and just a template where others can mold it there way.
I have written the launcher script in Posix Shellscript
. Honestly speaking I have not checked the Posix spec, I have made it such that it works with dash
(my /bin/sh
). I only know Python, shellscripting, Rust, and Java, and between python and shellscripts, I chose shellscript because it was faster (I have not included benchmarks, but for certain sections, the difference was nearly 20-50 times)
You can find the scripts here.
The idea is setup/operating system agnostic, but my scripts are not, so you likely have to make changes.
I am including the launcher script here because i am going to reference it a lot
#!/usr/bin/env sh
modelsdir="$HOME/data/local-installations/models"
savedir="$HOME/data/save"
tmpoutput=$(mktemp --dry-run)
payload="$(cliphist list | head -n15 | sed 's/^/clipboard\t/' ; \
cat -n "$savedir"/notes | sed 's/^/note\t/' ; \
printf "note\tadd as a note\nunnote\tremove nth note\nexecb\texecute in background\nsave web\tnormal bookmark\nsave webp\tpublic bookmark\nsave contact\tsome number or email with a description\nsave exec\tutable command\nsave command\tsome shell command\nsave random\tstuff to be typed but general\nimport\tbookmarks from html file\nrssadd\nytsub\tsubscribe youtube channel\ncompress\ta file\nyts\tsearch youtube and play video\nplay\tsearch youtube and play audio\nmpv\tplay video\nmpva\tplay audio only\nytdl\tdownload video from youtube\nytdla\tdownload audio from youtube\nexpand\ta shortened url\nupload\ta file and get a linkqr\tgenerate qr with input text\ndownload\tsome link with aria2c\nmdhtml\tconvert md file to html with pandoc\ncalc\tdo calculations\n\nsxpkg\tsearch file in arch packages\nddg\tusing duck duck go to search packages\nwiki\tsearch wik(ipedia or tionary)\n" ; \
sed '/^$/d' "$savedir"/web "$savedir"/web-public | sed 's/^/web\t/' ; \
sed '/^$/d' "$savedir"/contact-details | sed 's/^/type\t/' | sed "s|$|\tcontact-details|" ; \
for i in "$savedir"/snippets-autocomplete/* ; do sed '/^$/d' "$i" | sed 's/^/type\t/' | sed "s|$|\t$(basename ${i})|" ; done ; \
sed '/^$/d' "$savedir"/exec | sed 's/^/exec\t/' ; \
find "$HOME"/games/linux/ -maxdepth 2 -type f -executable | sed 's/^/exec\t/' ; \
sed '/^$/d' "$savedir"/execcopy | sed 's/^/execcopy\t/' ; \
sed '/^$/d' "$savedir"/execsu | sed 's/^/execsu\t/' ; \
sed '/^$/d' "$savedir"/execterm | sed 's/^/execterm\t/' ; \
sed '/^$/d' "$savedir"/exectype | sed 's/^/exectype\t/' ; \
sed '/^$/d' "$savedir"/vpns | sed 's/^/vpn\t/' ; \
sed '/^$/d' "$savedir"/play | sed 's/^/play\t/' ; \
# sed '/^$/d' "$savedir"/bluetooth-devices | sed 's/^/bluetooth\t/' ; \
sed '/^$/d' "$savedir"/recording | sed 's/^/record\t/' ; \
find "$HOME"/data/password-store/ -name "*.gpg" -type f -printf "%P\n" | sed 's/^/pass\t/' ; \
# find "$modelsdir" -maxdepth 1 -name "*.gguf" -type f -printf "%f\n" | sed 's/^/gguf\t/' ; \
find "$HOME"/games/windows/ -maxdepth 3 -type f -name '*.exe' | grep -v 'unins\|UnityCrashHandler\|dxwebsetup\|vcredist' | sed 's/^/wine\t/' ; \
find "$HOME"/games/gameboys/ -type f -name '*.gba' | sed 's/^/gba\t/' ; \
(find "$savedir" "$HOME"/music "$HOME"/.local/bin/scripts "$HOME"/study/books "$HOME"/study/projects "$HOME"/study/sem-8 "$HOME"/downloads -type f -name "*" && find "$HOME"/.config/ -type f -name "*" && sed '/^$/d' "$savedir"/files) | sed 's/^/file\t/' ; \
ls /usr/share/applications | sed 's/^/application\t/')"
payload="$(cat /tmp/launchrs_payload)"
# in most of the script, i am using printf "%s" instead of echo or echo -E, because in some of my bookmarks, i have escape codes, or such thing, which break and printf "%s" seems to preserve it the best
selection=$(printf "%s" "$payload" | tv)
if [ -z "${selection}" ]; then exit ; fi
# uncomment the printf statements for debugging
identifier=$(printf "%s" "$selection" | cut -f 1 | sed "s/^'//") # && printf "%s" "$identifier" && printf "\n"
selected_option=$(printf "%s" "$selection" | cut -f 2 | sed "s/^'//") # && printf "%s" "$selected_option" && printf "\n"
# Perform actions based on the identifier
case "$identifier" in
"application") gtk-launch "$(echo "$selection" | awk 'NF>1{print $NF}')" ;; # instead of using gtk-launch, you can also use exec, and include the exec line in application cache
"type") wtype "$selected_option" ;; # stuff to be auto-typed anywhere you want (currently using to write long commands)
"wine") cd "$(dirname "$selected_option")" && SDL_VIDEODRIVER=windows wine "$selected_option" ;;
"switch") suyu "$selected_option" ;;
"gba") $HOME/games/gameboys/mgba "$selected_option" ;;
"wiiu") Cemu "$selected_option" ;;
"drag-file") dragon-drop "$selected_option" ;;
"exec") output="$(eval "$selected_option" 2>&1)"
notify.sh " " "$output" ;;
"execsu") output="$(pass.sh local/logs | head -n 1 | sudo -SE sh -c "$selected_option" 2>&1)"
notify.sh " " "$output" ;;
"execcopy") eval "$selected_option" | wl-copy --trim-newline ;;
"exectype") eval "$selected_option" | wtype - ;;
"execterm") alacritty msg create-window -e sh -c "$selected_option" ;;
"clipboard") printf "%s" "$selection" | cut -f 2- | cliphist decode | wl-copy --trim-newline ;;
"web") qb-open-url-in-instance.sh "$selected_option" ;; # replace qb-open-url-in-instance.sh, with default browser, or your preferred browser
"man") alacritty msg create-window -e sh -c "zcat $selected_option | groff -t -e -mandoc -Tascii | sed -e 's/\x1b\[[0-9;]*m//g' | hx" ;;
"html") alacritty msg create-window -e sh -c "w3m $selected_option" ;;
"mail") editor-terminal "$XDG_DATA_HOME/mail/$selected_option" ;;
"edit") editor-terminal "$selected_option" ;;
"file") xdg-open "$selected_option" ;;
"play") mpv "$selected_option" ;;
"pass") action=$(printf "view\nsave\ncopy otp\ncopy login id\ncopy password\nedit" | term-dmenu)
file="${selected_option%.gpg}"
case "$action" in
"view") tmpoutput=$(mktemp --dry-run) && pass.sh show "$file" >| "$tmpoutput" && xdg-open "$tmpoutput" && \rm -rf "$tmpoutput";;
# "save") pass.sh show "$file" >| "$HOME/downloads/$(basename $file)" ;;
"copy otp") pass.sh otp "$file" | tr -d '\n' | wl-copy ;;
"copy login id") pass.sh "$file" | head -2 | tail -1 | tr -d '\n' | cut -d' ' -f 2- | wl-copy ;;
"copy password") pass.sh "$file" | head -1 | tr -d '\n' | wl-copy ;;
"edit") alacritty msg create-window -e pass.sh edit "$file" ;;
esac ;;
"record")
case "$selected_option" in
"stop-screen-recording") pkill wl-screenrec ;;
"stop-audio-recording") pkill parec ;;
"screen-only") notify.sh "Recording Stats" "$(wl-screenrec --low-power=off --codec hevc -f ~/downloads/screen-only-recording-$(date "+%Y%m%d-%H%M%S").mkv)" ;;
"screen-section-only") notify.sh "Recording Stats" "$(wl-screenrec --low-power=off --codec hevc --geometry "$(slurp)" -f ~/downloads/screen-section-only-recording-$(date "+%Y%m%d-%H%M%S").mkv)" ;;
"screen-with-audio") notify.sh "Recording Stats" "$(wl-screenrec --low-power=off --codec hevc --audio --audio-codec opus -f ~/downloads/screen-recording-$(date "+%Y%m%d-%H%M%S").mkv)" ;;
"screen-section-with-audio") notify.sh "Recording Stats" "$(wl-screenrec --low-power=off --codec hevc --audio --audio-codec opus --geometry "$(slurp)" -f ~/downloads/screen-section-recording-$(date "+%Y%m%d-%H%M%S").mkv)" ;;
"audio-only") notify.sh "Recording Stats" "$(parec --file-format=oga ~/downloads/voice-recording-$(date "+%Y%m%d-%H%M%S").oga)" ;;
esac ;;
"gguf") # prefix="Your are a smart and efficient assistant, give answers in a structured format, and try to keep your replies as short as possible"
# prompt="remember to be precise and correct always, and no emojis"
alacritty msg create-window -e sh -c "cd /tmp && llama-cli -m $modelsdir/$selected_option --multiline-input --temp 0.6 --ctx-size 4096 --conversation" ;;
"vpn") VPN_NAME="$selected_option" && current_status=$(nmcli connection show --active | grep -q "$VPN_NAME" && echo "connected" || echo "disconnected") && case $current_status in connected) notify.sh "VPN Status for $VPN_NAME" "$(nmcli connection down id $VPN_NAME)" ;; disconnected) notify.sh "VPN Status for $VPN_NAME" "$(nmcli connection up id $VPN_NAME)" ;; esac ;;
# "bluetooth") bluetoothctl show | grep -q "Powered: yes" && notify.sh "Bluetooth turned off" "$(bluetoothctl power off)" || notify.sh "Bluetooth turned on" "$(bluetoothctl power on; bluetoothctl connect $selected_option && pactl set-sink-volume @DEFAULT_SINK@ 20%)" ;;
*) # these are used when no matches happen (if anything matches, then match is returned, if not what was typed), we input the identifier ourselves, and the remaining becomes the selected_option_1, for example search the selected_option_1
# can be used as a way to easily use ddg "bangs", but more flexible, and browser/search engine agnostic
identifier_1="$(printf "%s" "$selection" | cut -d' ' -f 1 | sed "s/^'//")" # && printf "%s" "$identifier_1" && printf "\n"
selected_option_1="$(printf "%s" "$selection" | cut -d' ' -f 2- | sed "s/^'//")" # && printf "%s" "$selected_option_1" && printf "\n"
wl-copy "$selected_option_1" # copying the selection, helps in rerunning the command (helpful in debugging, or for example with calculator or music, where you slightly messed it up)
case "$identifier_1" in
"unnote") del=$(($selected_option_1)) && sed -i "${del}d" "$savedir"/notes ;;
"note") (printf '%s' "- $selected_option_1" && printf "\n") >> "$savedir"/notes ;;
"execb") eval "$selected_option_1" ;;
"save") category="$(printf "%s" "$selected_option_1" | cut -d' ' -f 1)"
tobesaved="$(printf "%s" "$selected_option_1" | cut -d' ' -f 2)"
description=""
if [ "$(printf "%s" "$selected_option_1" | wc -w)" -gt 2 ]; then description="$(printf "%s" "$selected_option_1" | cut -d' ' -f 3-)" ; fi
sniptobesaved="$(printf "%s" "$selected_option_1" | cut -d' ' -f 2-)"
case "$category" in
"web") title="$(curl -s "$url" | grep -oP '(?<=<title>).*?(?=</title>)')"
printf "$tobesaved\t$title $description\n" >> "$savedir"/web
notify.sh "$tobesaved" "$title $description" ;;
"webp") title="$(curl -s "$tobesaved" | grep -oP '(?<=<title>).*?(?=</title>)')"
printf "$tobesaved\t$title $description\n" >> "$savedir"/webp
notify.sh "$tobesaved" "$title $description" ;;
"contact") printf "$tobesaved\t$description\n" >> "$savedir"/contact-details
notify.sh "$tobesaved" "$description" ;;
"file") printf "$tobesaved\t$description\n" >> "$savedir"/files
notify.sh "$tobesaved" "$description" ;;
"exec") printf "$sniptobesaved\n" >> "$savedir"/exec
notify.sh "$savedir/exec" "$sniptobesaved" ;;
"command") printf "$sniptobesaved\n" >> "$savedir"/snippets-autocomplete/commands-and-snippets
notify.sh "$savedir/snippets-autocomplete/commands-and-snippets" "$sniptobesaved" ;;
"random") printf "$sniptobesaved\n" >> "$savedir"/snippets-autocomplete/random-stuff
notify.sh "$savedir/snippets-autocomplete/random-stuff" "$sniptobesaved" ;;
*) notify.sh "Usage of save is changed" "do save [web/webp/contact/exec/command/random]"
esac ;;
"import") grep -o '<DT><A HREF="[^"]\+"[^>]\+>[^<]\+</A>' "$selected_option_1" |
sed 's/<DT><A HREF="\([^"]\+\)"[^>]*>\([^<]\+\)<\/A>/web\t\1\t\2/' >> "$savedir"/web.txt ;;
"rssadd") name="$( curl -s $selected_option_1 | grep -oP '(?<=<title>).*?(?=</title>)' | head -n1 )"
printf "$selected_option_1 \"$name\"\n" >> "$HOME"/.config/newsraft/feeds
notify.sh "Subscribed to rss feed $name" "$selected_option_1";;
"ytsub") channel_id="$(printf "%s" "$selected_option_1" | grep -oP '(?<=channel/)\K.*')"
name="$( curl -s https://www.youtube.com/channel/$channel_id | grep -oP '(?<=<title>).*?(?=</title>)' | sed -z 's/\ \-\ YouTube//g')"
printf "https://www.youtube.com/feeds/videos.xml?channel_id=$channel_id \"$name\"\n" >> "$HOME"/.config/newsraft/feeds
notify.sh "Subscribed to $name" "$channel_id";;
"compress") "$HOME"/.local/bin/compress "$selected_option_1" ;;
"mpv") mpv "$selected_option_1" ;;
"mpva") mpv --no-video --player-operation-mode=pseudo-gui --speed=1 "$selected_option_1" ;;
"ytdl") yt-dlp "$selected_option_1" && notify.sh "Download Completed" "$selected_option_1" ;;
"ytdla") yt-dlp -x "$selected_option_1" && notify.sh "Download Completed" "$selected_option_1" ;;
"expand") url="$(redive "$selected_option_1" 2>&1 | tail -n1 | cut -d'>' -f 2)"
wl-copy "$url" && notify.sh "Url expands to $url" " " ;;
"upload") url=$(curl -F'file=@'"$selected_option_1" -Fsecret= "https://envs.sh/")
wl-copy "$url" && notify.sh "File uploaded to $url" " " ;;
"qr") printf '%s' "$selected_option_1" | qrencode -o "$tmpoutput"
xdg-open "$tmpoutput" ;;
"download") aria2c "$selected_option_1" ;;
"rename") rename-bib.sh "$selected_option_1" ;;
"mdhtml") input_file_name="${selected_option_1%.*}" && pandoc -s --css="style.css" --highlight-style=haddock -f markdown -t html5 -o "$input_file_name".html "$selected_option_1" ;;
"calc") result=$(fish -c "math $selected_option_1")
wl-copy "$result" && notify.sh "$selected_option_1 = $result" " " ;;
"ytl") qb-open-url-in-instance.sh https://www.youtube.com/results?search_query="$selected_option_1" ;;
"ddg") qb-open-url-in-instance.sh https://html.duckduckgo.com/html?q="$selected_option_1" ;;
"sxpkg") qb-open-url-in-instance.sh https://my-fake-search-engine.sga/search?q="site:archlinux.org/packages/ bin/$selected_option_1" ;; # searching a file in arch packages
"wiki") qb-open-url-in-instance.sh https://wikipedia.org/wiki/Special:Search?search="$selected_option_1" ;;
*) if [ -n "$selection" ]; then qb-open-url-in-instance.sh https://my-fake-search-engine.sga/search?q="$selection" ; fi ;; # for some weird reason, i can not use the qb-open-url-in-instance.sh --search flag, it works, but it will always open a new window, something i do not like, so have to hard code the search engine (--new-tab does not work)
# *) if [ -n "$selection" ]; then qb-open-url-in-instance.sh http://127.0.0.1:8888/search?q="$selection" ; fi ;; # for some weird reason, i can not use the qb-open-url-in-instance.sh --search flag, it works, but it will always open a new window, something i do not like, so have to hard code the search engine (--new-tab does not work)
esac ;;
esac
we are first making a “payload” - the thing which will search in
cliphist
to get my clipboard entries, quick access to clipboard is great for stupidly copying to things, I am even using it save some results (search queries, links to “media” being played,calculation results, etc)Application About Xfce Information about the Xfce Desktop Environment xfce4-about.desktop
bookmarks
this is the everything file, if you want, you can make one for each category, or just separate the web bookmarks, here is the structureIdentifier \t Entry \t (Comments or Tags(optional))
I use “t” tabs as to separate Identifier
and Entry
. All examples for each kind are in this format. If you want to use something else, then change it, just update the cut -f
lines in script with some other delimiter -d' '
I now use it for practically everything
Files
to be opened with xdg-open
, I have put some of the common config files, or my notes file, the following line is very similar, where I include all the files in certain dirs, like music (so the launcher is interface to my local media)
find $HOME/Music $HOME/Videos/Movies $HOME/.local/bin -name "*" | while read -r line ; do printf "File\t$line\n" ; done
Type
is to type out long stuff, I am now using it for long commands, that I earlier was using aliases, cheatsheets, navi
for, and there is no major enhancement, other than the fact that it is almost as fast, but within one same menu. I am using wtype to type out stuff, ydotool had more options, but breaks in printing some stuff. Here is a command to print a fancy gradient rainbow with awk
Type awk 'BEGIN{s="________________________________";s=s s s s s s s s;for(colnum=0;colnum<77;colnum++){r=255-(colnum*255/76);g=(colnum*510/76);b=(colnum*255/76);if(g>255)g=510-g;printf "\033[48;2;%d;%d;%dm",r,g,b;printf "\033[38;2;%d;%d;%dm",255-r,255-g,255-b;printf "%s\033[0m",substr(s,colnum+1,1);}printf "\n";}' rainbow text with awk terminal, check color capacity of terminal
Exec
to execute any command, this is the singe best thing, and the reason why I have 20-30 less keyboard shortcuts in my sway config, I use it toggle my bluetooth connection, change vpns, clean system, because I have made scripts for them or included relevant commands, or launching windows games with long wine params. Here is example command to clear clipboard (useful if the command you want to execute is also in your clipboard). This has subset ExecCopy
(an example to copy the colour code) and ExecType
(I use this and kpxcpc
to somewhat safely (without copying to/from clipboard) type long passwords from KeepassXC
outside browser)
Exec cliphist wipe Clear Clipboard
ExecCopy hyprpicker Color Picker
Web
for your browser bookmarks. Now you need not give up having bookmarks in your browser, you can keep your bookmarks, just export to html, and convert to required format with parse-bookmarks-file
, but I moved all bookmarks, and i do not want to move back 600+
lines back, the benefit of using it is much better search algorithm, and much easier to share some sections of bookmarks with others (if you have categorized then correctly, with some sed
magic) (but you dont have to move your bookmarks for this). A great video if you would like an intro to using sed
Web https://www.youtube.com/watch?v=dQw4w9WgXcQ
I then select stuff with term-dmenu
(adapted from Seirdy/term-dmenu, changes include hard coding the use of Alacritty as terminal, and ensuring input stays intact (including escape characters, which for me were broken in the original one)), which is basically television
(a rust alternative to fzf
) wrapped in a floating terminal window
tofi
, it is great and fast, but less customisability (specifically no way to ellipsise the middle, since I want to see the comments)rofi
is great with that, but not great (compared to fzf
) searching algorithmfzf
is great, but way too flexible, but you can use ' or ! or * or .
to get great refinement, and especially useful for later section of script where we have essentially no matches and perform actions based on inputtelevision
is a new fuzzy finder application, with nucleo
matcher from helix
text editor. A nice thing about it is that by default it has a preview pane (you can also have it in fzf
), and I found its matching marginally better than fzf
cases
to perform actionsfor cases with no matches, we have new set of identifiers, which we type, to either
yt-dlp, and mpv
fend
(I earlier used bc
and python
, bc is worse, and python is much more flexible, but harder to use in this setup, used qalc
before fend, but moved to fend
because rust) orposix
cliphist
and wlcip
gtk-launch
browser
libnotify
wtype
(or xdotool or ydotool (breaks in some cases with certain commands with escape codes))yt-dlp, mpv
fend
(or dc/dc or python)