diff options
| author | dacctal <120422854+dacctal@users.noreply.github.com> | 2026-03-05 01:21:45 +0000 |
|---|---|---|
| committer | dacctal <120422854+dacctal@users.noreply.github.com> | 2026-03-05 01:21:45 +0000 |
| commit | 69a722cfd33076a4d63f1d49e52bed427453eabe (patch) | |
| tree | 56545e93c12a8232058a0ab2b258546418656e03 /.config/otter-launcher | |
initial commit
Diffstat (limited to '.config/otter-launcher')
| -rw-r--r-- | .config/otter-launcher/config.toml | 126 | ||||
| -rwxr-xr-x | .config/otter-launcher/sway-launcher-desktop.sh | 390 |
2 files changed, 516 insertions, 0 deletions
diff --git a/.config/otter-launcher/config.toml b/.config/otter-launcher/config.toml new file mode 100644 index 0000000..a0a53ab --- /dev/null +++ b/.config/otter-launcher/config.toml @@ -0,0 +1,126 @@ +[general] +default_module = "app" # The module to run when no prefix is matched +empty_module = "a" # run with an empty prompt +exec_cmd = "sh -c" # The exec command of your shell, default to sh +# for example: "bach -c" for bash; "zsh -c" for zsh. This can also runs wm exec commands, like hyprctl dispatch exec +vi_mode = true # set true to use vi keybinds, false to use emacs keybinds; default to emacs +esc_to_abort = true # allow to quit with esc keypress; a useful option for vi users +cheatsheet_entry = "?" # when entered, otter-launcher will show a list of configured modules +cheatsheet_viewer = "less -R; clear" # the program that otter-launcher will pipe cheatsheet into +clear_screen_after_execution = false # useful when chafa image flash back after module execution +loop_mode = false # in loop mode, otter-launcher won't quit after running a module, useful when using scratchpad +external_editor = "" # if set, pressing ctrl+e (or pressing v in vi normal mode) will edit the input field in the specified program; default to no external editor +#callback = "" # if set, otter-launcher will run the command after a module is executed; for example, it can call swaymsg to adjust window size + +# ASCII color codes are allowed with these options. However, \x1b should be replaced with \u001B (unicode escape) because the rust toml crate cannot read \x as an escaped character... +[interface] +# use three quotes to write longer commands +header = """ + \u001B[34;1m >\u001B[0m $USER@$(echo $HOSTNAME) \u001B[31m\u001B[0m $(cat /proc/loadavg | cut -d ' ' -f 1) \u001B[33m\u001B[0m $(free -h | awk 'FNR == 2 {print $3}' | sed 's/i//') + \u001B[34;1m>\u001B[0;1m """ +# Run a shell command and make the stdout printed above the header +header_cmd = "" +header_cmd_trimmed_lines = 0 # Remove a number of lines from header_cmd output, in case of some programs printing excessive empty lines at the end of its output +header_concatenate = false # print header and header_cmd output at the same line, default to false +list_prefix = " " +selection_prefix = " \u001B[31;1m> " +place_holder = "type and search" +default_module_message = " \u001B[33msearch\u001B[0m the internet" # if set, the text will be shown when the default module is in use +empty_module_message = "" # the text to show when empty module is in use +suggestion_mode = "list" # available options: list, hint +suggestion_lines = 12 # length of the suggestion list, set to 0 to disable suggestions and tab completion +indicator_with_arg_module = "\u001B[31m^\u001B[0m " # a sign showing whether the module should run with an argument +indicator_no_arg_module = "\u001B[31m$\u001B[0m " +prefix_padding = 3 # format prefixes to have a uniformed width; prefixes will be padded with spaces to have a least specified number of chars +# below color options affect all modules; per-module coloring is allowed by using ascii color codes at each module's configurations +prefix_color = "\u001B[33m" +description_color = "\u001B[39m" +place_holder_color = "\u001B[30m" +hint_color = "\u001B[30m" # the color of hint mode suggestions +# move the whole interface rightward or upward, easier for styling with chafa image +move_right = 0 +move_up = 0 + + +[[modules]] +description = "search with brave" +prefix = "br" +cmd = "setsid -f xdg-open 'https://search.brave.com/search?q={}'" +with_argument = true +url_encode = true + +[[modules]] +description = "kill a runing app" +prefix = "k" +cmd = 'ps -u "$USER" -o comm= | sort -u | fsel --dmenu | xargs -r pkill -9' +with_argument = true +url_encode = true + +[[modules]] +description = "launch apps with fsel" +prefix = "a" +cmd = "fsel -vv -r -d -ss \"{}\"" +with_argument = true + +[[modules]] +description = "launch apps instantly" +prefix = "app" +cmd = "fsel -vv -r -d -p {}" +with_argument = true + +[[modules]] +description = "manage clipboard with fsel" +prefix = "cl" +cmd = """ +fsel --cclip +""" + +[[modules]] +description = "find pkgs" +prefix = "pm" +cmd = "pmux -SD {}" +with_argument = true + +[[modules]] +description = "install pkgs" +prefix = "i" +cmd = "pmux -S {}" +with_argument = true + +[[modules]] +description = "power menu with fzf" +prefix = "p" +cmd = """ +function power { +if [[ -n $1 ]]; then +case $1 in +"logout") session=`loginctl session-status | head -n 1 | awk '{print $1}'`; loginctl terminate-session $session ;; +"suspend") systemctl suspend ;; +"hibernate") systemctl hibernate ;; +"reboot") systemctl reboot ;; +"shutdown") systemctl poweroff ;; +esac fi } +power $(echo -e 'reboot\nshutdown\nlogout\nsuspend\nhibernate' | fzf --reverse --no-scrollbar --padding 1,3 --prompt 'Power Menu: ' | tail -1) +""" + +[[modules]] +description = "run command in terminal" +prefix = "s" +cmd = """ +setsid -f "$(echo $TERM | sed 's/xterm-//g')" -e {} +""" +with_argument = true + +[[modules]] +description = "search archwiki" +prefix = "w" +cmd = "setsid -f xdg-open https://wiki.archlinux.org/index.php?search='{}'" +with_argument = true +url_encode = true + +[[modules]] +description = "cambridge dictionary" +prefix = "dc" +cmd = "setsid -f xdg-open 'https://dictionary.cambridge.org/dictionary/english/{}'" +with_argument = true +url_encode = true diff --git a/.config/otter-launcher/sway-launcher-desktop.sh b/.config/otter-launcher/sway-launcher-desktop.sh new file mode 100755 index 0000000..0153175 --- /dev/null +++ b/.config/otter-launcher/sway-launcher-desktop.sh @@ -0,0 +1,390 @@ +#!/usr/bin/env bash +# terminal application launcher for sway, using fzf +# Based on: https://gitlab.com/FlyingWombat/my-scripts/blob/master/sway-launcher +# https://gist.github.com/Biont/40ef59652acf3673520c7a03c9f22d2a +shopt -s nullglob globstar +set -o pipefail +if ! { exec 0>&3; } 1>/dev/null 2>&1; then + exec 3>/dev/null # If file descriptor 3 is unused in parent shell, output to /dev/null +fi +# shellcheck disable=SC2154 +trap 's=$?; echo "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR +IFS=$'\n\t' +DEL=$'\34' + +FZF_COMMAND="${FZF_COMMAND:=fzf}" +TERMINAL_COMMAND="${TERMINAL_COMMAND:="$TERMINAL -e"}" +GLYPH_COMMAND="${GLYPH_COMMAND- }" +GLYPH_DESKTOP="${GLYPH_DESKTOP- }" +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/sway-launcher-desktop" +PROVIDERS_FILE="${PROVIDERS_FILE:=providers.conf}" +if [[ "${PROVIDERS_FILE#/}" == "${PROVIDERS_FILE}" ]]; then + # $PROVIDERS_FILE is a relative path, prepend $CONFIG_DIR + PROVIDERS_FILE="${CONFIG_DIR}/${PROVIDERS_FILE}" +fi +if [[ ! -v PREVIEW_WINDOW ]]; then + PREVIEW_WINDOW=up:2:noborder +fi + +# Provider config entries are separated by the field separator \034 and have the following structure: +# list_cmd,preview_cmd,launch_cmd,purge_cmd +declare -A PROVIDERS +if [ -f "${PROVIDERS_FILE}" ]; then + eval "$(awk -F= ' + BEGINFILE{ provider=""; } + /^\[.*\]/{sub("^\\[", "");sub("\\]$", "");provider=$0} + /^(launch|list|preview|purge)_cmd/{st = index($0,"=");providers[provider][$1] = substr($0,st+1)} + ENDFILE{ + for (key in providers){ + if(!("list_cmd" in providers[key])){continue;} + if(!("launch_cmd" in providers[key])){continue;} + if(!("preview_cmd" in providers[key])){continue;} + if(!("purge_cmd" in providers[key])){providers[key]["purge_cmd"] = "exit 0";} + for (entry in providers[key]){ + gsub(/[\x27,\047]/,"\x27\"\x27\"\x27", providers[key][entry]) + } + print "PROVIDERS[\x27" key "\x27]=\x27" providers[key]["list_cmd"] "\034" providers[key]["preview_cmd"] "\034" providers[key]["launch_cmd"] "\034" providers[key]["purge_cmd"] "\x27\n" + } + }' "${PROVIDERS_FILE}")" + if [[ ! -v HIST_FILE ]]; then + HIST_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/${0##*/}-${PROVIDERS_FILE##*/}-history.txt" + fi +else + PROVIDERS['desktop']="${0} list-entries${DEL}${0} describe-desktop \"{1}\"${DEL}${0} run-desktop '{1}' {2}${DEL}test -f '{1}' || exit 43" + PROVIDERS['command']="${0} list-commands${DEL}${0} describe-command \"{1}\"${DEL}${TERMINAL_COMMAND} {1}${DEL}command -v '{1}' || exit 43" + if [[ ! -v HIST_FILE ]]; then + HIST_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/${0##*/}-history.txt" + fi +fi +PROVIDERS['user']="exit${DEL}exit${DEL}{1}" # Fallback provider that simply executes the exact command if there were no matches + +if [[ -n "${HIST_FILE}" ]]; then + mkdir -p "${HIST_FILE%/*}" && touch "$HIST_FILE" + readarray HIST_LINES <"$HIST_FILE" +fi + +function describe() { + # shellcheck disable=SC2086 + readarray -d ${DEL} -t PROVIDER_ARGS <<<${PROVIDERS[${1}]} + # shellcheck disable=SC2086 + [ -n "${PROVIDER_ARGS[1]}" ] && eval "${PROVIDER_ARGS[1]//\{1\}/${2}}" +} +function describe-desktop() { + description=$(sed -ne '/^Comment=/{s/^Comment=//;p;q}' "$1") + echo -e "\033[33m$(sed -ne '/^Name=/{s/^Name=//;p;q}' "$1")\033[0m" + echo "${description:-No description}" +} +function describe-command() { + readarray arr < <(whatis -l "$1" 2>/dev/null) + description="${arr[0]}" + description="${description#* - }" + echo -e "\033[33m${1}\033[0m" + echo "${description:-No description}" +} + +function provide() { + # shellcheck disable=SC2086 + readarray -d ${DEL} -t PROVIDER_ARGS <<<${PROVIDERS[$1]} + eval "${PROVIDER_ARGS[0]}" +} +#function list-commands() { +# 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 +#} +function list-commands() { + # Add your path + CUSTOM_BIN_DIR="$HOME/.local/share/uspm/bin/" + + # Og PATH directories + IFS=: read -ra path <<<"$PATH" + + # directory + if [[ -d "$CUSTOM_BIN_DIR" ]]; then + path+=("$CUSTOM_BIN_DIR") + fi + + 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 +} +function list-entries() { + # Get locations of desktop application folders according to spec + # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + IFS=':' read -ra DIRS <<<"${XDG_DATA_HOME-${HOME}/.local/share}:${XDG_DATA_DIRS-/usr/local/share:/usr/share}" + for i in "${!DIRS[@]}"; do + if [[ ! -d "${DIRS[i]}" ]]; then + unset -v 'DIRS[$i]' + else + DIRS[$i]="${DIRS[i]}/applications/**/*.desktop" + fi + done + + # shellcheck disable=SC2068 + entries ${DIRS[@]} | sort -k2 +} +function entries() { + # shellcheck disable=SC2068 + awk -v pre="$GLYPH_DESKTOP" -F= ' + function desktopFileID(filename){ + sub("^.*applications/", "", filename); + sub("/", "-", filename); + return filename + } + BEGINFILE{ + application=0; + hidden=0; + block=""; + a=0 + + id=desktopFileID(FILENAME) + if(id in fileIds){ + nextfile; + }else{ + fileIds[id]=0 + } + } + /^\[Desktop Entry\]/{block="entry"} + /^Type=Application/{application=1} + /^\[Desktop Action/{ + sub("^\\[Desktop Action ", ""); + sub("\\]$", ""); + block="action"; + a++; + actions[a,"key"]=$0 + } + /^\[X-/{ + sub("^\\[X-", ""); + sub("\\]$", ""); + block="action"; + a++; + actions[a,"key"]=$0 + } + /^Name=/{ (block=="action")? actions[a,"name"]=$2 : name=$2 } + /^NoDisplay=true/{ (block=="action")? actions[a,"hidden"]=1 : hidden=1 } + ENDFILE{ + if (application){ + if (!hidden) + print FILENAME "\034desktop\034\033[33m" pre name "\033[0m"; + if (a>0) + for (i=1; i<=a; i++) + if (!actions[i, "hidden"]) + print FILENAME "\034desktop\034\033[33m" pre name "\033[0m (" actions[i, "name"] ")\034" actions[i, "key"] + } + }' \ + $@ </dev/null + # the empty stdin is needed in case no *.desktop files +} +function run-desktop() { + CMD="$("${0}" generate-command "$@" 2>&3)" + echo "Generated Launch command from .desktop file: ${CMD}" >&3 + eval "${CMD}" +} +function generate-command() { + # Define the search pattern that specifies the block to search for within the .desktop file + PATTERN="^\\\\[Desktop Entry\\\\]" + if [[ -n $2 ]]; then + PATTERN="^\\\\[Desktop Action ${2}\\\\]" + fi + echo "Searching for pattern: ${PATTERN}" >&3 + # 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 + awk -v pattern="${PATTERN}" -v terminal_cmd="${TERMINAL_COMMAND}" -F= ' + BEGIN{a=0;exec=0;path=0} + /^\[Desktop/{ + if(a){ a=0 } + } + $0 ~ pattern{ a=1 } + /^Terminal=/{ + sub("^Terminal=", ""); + if ($0 == "true") { terminal=1 } + } + /^Exec=/{ + if(a && !exec){ + sub("^Exec=", ""); + gsub(" ?%[cDdFfikmNnUuv]", ""); + exec=$0; + } + } + /^Path=/{ + if(a && !path){ path=$2 } + } + END{ + if(path){ printf "cd " path " && " } + printf "exec " + if (terminal){ printf terminal_cmd " " } + print exec + }' "$1" +} + +function shouldAutostart() { + local condition="$(cat $1 | grep "AutostartCondition" | cut -d'=' -f2)" + local filename="${XDG_CONFIG_HOME-${HOME}/.config}/${condition#* }" + case $condition in + if-exists*) + [[ -e $filename ]] + ;; + unless-exists*) + [[ ! -e $filename ]] + ;; + *) + return 0 + ;; + esac +} + +function autostart() { + for application in $(list-autostart); do + if shouldAutostart "$application"; then + (exec setsid /bin/sh -c "$(run-desktop "${application}")" &>/dev/null &) + fi + done +} + +function list-autostart() { + # Get locations of desktop application folders according to spec + # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + IFS=':' read -ra DIRS <<<"${XDG_CONFIG_HOME-${HOME}/.config}:${XDG_CONFIG_DIRS-/etc/xdg}" + for i in "${!DIRS[@]}"; do + if [[ ! -d "${DIRS[i]}" ]]; then + unset -v 'DIRS[$i]' + else + DIRS[$i]="${DIRS[i]}/autostart/*.desktop" + fi + done + + # shellcheck disable=SC2068 + awk -v pre="$GLYPH_DESKTOP" -F= ' + function desktopFileID(filename){ + sub("^.*autostart/", "", filename); + sub("/", "-", filename); + return filename + } + BEGINFILE{ + application=0; + block=""; + disabled=0; + a=0 + + id=desktopFileID(FILENAME) + if(id in fileIds){ + nextfile; + }else{ + fileIds[id]=0 + } + } + /^\[Desktop Entry\]/{block="entry"} + /^Type=Application/{application=1} + /^Name=/{ iname=$2 } + /^Hidden=true/{disabled=1} + ENDFILE{ + if (application && !disabled){ + print FILENAME; + } + }' \ + ${DIRS[@]} </dev/null +} + +purge() { + # shellcheck disable=SC2188 + >"${HIST_FILE}" + declare -A PURGE_CMDS + for PROVIDER_NAME in "${!PROVIDERS[@]}"; do + readarray -td ${DEL} PROVIDER_ARGS <<<${PROVIDERS[${PROVIDER_NAME}]} + PURGE_CMD=${PROVIDER_ARGS[3]} + [ -z "${PURGE_CMD}" ] && PURGE_CMD='test -f "{1}" || exit 43' + PURGE_CMDS[$PROVIDER_NAME]="${PURGE_CMD%$'\n'}" + done + for HIST_LINE in "${HIST_LINES[@]#*' '}"; do + readarray -td $'\034' HIST_ENTRY <<<${HIST_LINE} + ENTRY=${HIST_ENTRY[1]} + readarray -td ' ' FILTER <<<${PURGE_CMDS[$ENTRY]//\{1\}/${HIST_ENTRY[0]}} + (eval "${FILTER[@]}" 1>/dev/null) # Run filter command discarding output. We only want the exit status + if [[ $? -ne 43 ]]; then + echo "1 ${HIST_LINE[@]%$'\n'}" >>"${HIST_FILE}" + fi + done +} + +case "$1" in +describe | describe-desktop | describe-command | entries | list-entries | list-commands | list-autostart | generate-command | autostart | run-desktop | provide | purge) + "$@" + exit + ;; +esac +echo "Starting launcher instance with the following providers:" "${!PROVIDERS[@]}" >&3 + +FZFPIPE=$(mktemp -u) +mkfifo "$FZFPIPE" +trap 'rm "$FZFPIPE"' EXIT INT + +# Append Launcher History, removing usage count +(printf '%s' "${HIST_LINES[@]#* }" >>"$FZFPIPE") & + +# Iterate over providers and run their list-command +for PROVIDER_NAME in "${!PROVIDERS[@]}"; do + (bash -c "${0} provide ${PROVIDER_NAME}" >>"$FZFPIPE") & +done + +readarray -t COMMAND_STR <<<$( + ${FZF_COMMAND} --ansi +s -x -d '\034' --nth ..3 --with-nth 3 \ + --print-query \ + --preview "$0 describe {2} {1}" \ + --preview-window="${PREVIEW_WINDOW}" \ + --no-multi --cycle \ + --prompt="${GLYPH_PROMPT-# }" \ + --header='' --no-info --margin='1,2' \ + --color='16,gutter:-1' \ + <"$FZFPIPE" +) || exit 1 +# Get the last line of the fzf output. If there were no matches, it contains the query which we'll treat as a custom command +# If there were matches, it contains the selected item +COMMAND_STR=$(printf '%s\n' "${COMMAND_STR[@]: -1}") +# We still need to format the query to conform to our fallback provider. +# We check for the presence of field separator character to determine if we're dealing with a custom command +if [[ $COMMAND_STR != *$'\034'* ]]; then + COMMAND_STR="${COMMAND_STR}"$'\034user\034'"${COMMAND_STR}"$'\034' + SKIP_HIST=1 # I chose not to include custom commands in the history. If this is a bad idea, open an issue please +fi + +[ -z "$COMMAND_STR" ] && exit 1 + +if [[ -n "${HIST_FILE}" && ! "$SKIP_HIST" ]]; then + # 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" +fi + +# shellcheck disable=SC2086 +readarray -d $'\034' -t PARAMS <<<${COMMAND_STR} +# shellcheck disable=SC2086 +readarray -d ${DEL} -t PROVIDER_ARGS <<<${PROVIDERS[${PARAMS[1]}]} +# Substitute {1}, {2} etc with the correct values +COMMAND=${PROVIDER_ARGS[2]//\{1\}/${PARAMS[0]}} +COMMAND=${COMMAND//\{2\}/${PARAMS[3]}} +COMMAND=${COMMAND%%[[:space:]]} + +if [ -t 1 ]; then + echo "Launching command: ${COMMAND}" >&3 + setsid /bin/sh -c "${COMMAND}" >&/dev/null </dev/null & + sleep 0.01 +else + echo "${COMMAND}" +fi |
