#!/usr/bin/env bash # List-driven launcher commands, through `vicinae dmenu`. # # panama-pick window fuzzy-switch to an open window # panama-pick quit-window pick a window; SIGTERM it, SIGKILL on repeat # panama-pick process pick a process by CPU; SIGTERM it # panama-pick ssh pick a Host from ~/.ssh/config; open a session # panama-pick recent pick a recently used file; open it # panama-pick password pick a Bitwarden entry; copy it, concealed # # One helper rather than five scripts because every subcommand is the same # sentence: build a list, let dmenu pick a line, act on the index. dmenu is # vicinae's own list view, so these read as launcher commands without a # compiled extension behind them. set -uo pipefail # Prints the picked index for the lines on stdin, or nothing on Escape. menu() { vicinae dmenu --navigation-title "$1" --section-title "$2" \ --format index --no-section 2>/dev/null } # Selected line (1-indexed by dmenu's 0-indexed output) from a saved list. line_at() { sed -n "$(( $1 + 1 ))p" } case "${1:-}" in window|quit-window) clients="$(hyprctl clients -j | jq -r ' [.[] | select(.mapped and .workspace.id > 0)] | sort_by(.workspace.id)[] | [.address, .pid, "\(.title) (\(.class), workspace \(.workspace.id))"] | @tsv')" [[ -n "$clients" ]] || exit 0 title="Switch to window"; [[ "$1" == quit-window ]] && title="Force quit window" idx="$(cut -f3 <<<"$clients" | menu "$title" 'Windows ({count})')" || exit 0 [[ -n "$idx" ]] || exit 0 picked="$(line_at "$idx" <<<"$clients")" address="$(cut -f1 <<<"$picked")" pid="$(cut -f2 <<<"$picked")" if [[ "$1" == window ]]; then exec hyprctl dispatch focuswindow "address:$address" fi # TERM first; the window still being mapped a moment later means the app # ignored it, which is what force quit exists for. kill "$pid" 2>/dev/null || true sleep 1 if hyprctl clients -j | jq -e --arg a "$address" '.[] | select(.address == $a)' >/dev/null 2>&1; then kill -9 "$pid" 2>/dev/null || true fi ;; process) procs="$(ps -eo pid=,pcpu=,comm= --sort=-pcpu | awk -v self=$$ '$1 != self { printf "%s\t%5.1f%% %s\n", $1, $2, $3 }' | head -60)" [[ -n "$procs" ]] || exit 0 idx="$(cut -f2 <<<"$procs" | menu "Kill process" 'By CPU ({count})')" || exit 0 [[ -n "$idx" ]] || exit 0 pid="$(line_at "$idx" <<<"$procs" | cut -f1)" kill "$pid" 2>/dev/null \ && notify-send "Sent SIGTERM to $pid" 2>/dev/null || true ;; ssh) config="$HOME/.ssh/config" hosts="$( [[ -r "$config" ]] && awk '/^[Hh]ost / { for (i = 2; i <= NF; i++) if ($i !~ /[*?]/) print $i }' "$config" | sort -u )" if [[ -z "$hosts" ]]; then notify-send "SSH Hosts" "No hosts in ~/.ssh/config" 2>/dev/null || true exit 0 fi idx="$(menu "Open SSH session" 'Hosts ({count})' <<<"$hosts")" || exit 0 [[ -n "$idx" ]] || exit 0 host="$(line_at "$idx" <<<"$hosts")" # kitty, matching hypr/keybinds.lua -- the terminal is written down in a # few places and this one keeps the same value until one owner exists. exec kitty --detach ssh "$host" ;; recent) xbel="$HOME/.local/share/recently-used.xbel" entries="$( [[ -r "$xbel" ]] && python3 - "$xbel" <<'PY' import sys, urllib.parse, xml.etree.ElementTree as ET try: root = ET.parse(sys.argv[1]).getroot() except ET.ParseError: sys.exit(0) marks = [(b.get("modified") or "", b.get("href") or "") for b in root.iter("bookmark")] for _, href in sorted(marks, reverse=True)[:40]: if href.startswith("file://"): path = urllib.parse.unquote(href[7:]) print(f"{path}") PY )" if [[ -z "$entries" ]]; then notify-send "Recent Files" "Nothing recorded yet" 2>/dev/null || true exit 0 fi idx="$(sed "s|^$HOME|~|" <<<"$entries" | menu "Open recent file" 'Recent ({count})')" || exit 0 [[ -n "$idx" ]] || exit 0 exec xdg-open "$(line_at "$idx" <<<"$entries")" ;; password) # The security shape, in one place: # * the secret travels rbw -> pipe -> wl-copy; it is never an argument # and never a file (argv is world-readable, files outlive intentions) # * --sensitive offers the x-kde-passwordManagerHint MIME, which # vicinae's clipboard history documents it ignores -- verified against # the live history before this was written # * the clipboard clears itself after 30 seconds, from a transient timer # so it happens even if this shell is long gone if ! command -v rbw >/dev/null 2>&1; then notify-send "Passwords" "rbw is not installed" 2>/dev/null || true exit 1 fi if ! entries="$(rbw list --fields name,user 2>/dev/null)" || [[ -z "$entries" ]]; then notify-send "Passwords" \ "rbw is not set up. Run: rbw config set email ; rbw register; rbw login" \ 2>/dev/null || true exit 0 fi idx="$(awk -F'\t' '{ if ($2 != "") printf "%s (%s)\n", $1, $2; else print $1 }' <<<"$entries" \ | menu "Copy password" 'Bitwarden ({count})')" || exit 0 [[ -n "$idx" ]] || exit 0 picked="$(line_at "$idx" <<<"$entries")" name="$(cut -f1 <<<"$picked")" user="$(cut -f2 <<<"$picked")" # `--` keeps an entry named like a flag from parsing as one; the pipeline # status catches a vault that locked between list and get, so a failed # read is reported instead of an empty copy claiming success. if ! rbw get -- "$name" ${user:+"$user"} | wl-copy --trim-newline --sensitive; then notify-send "Passwords" "Could not read \"$name\" — is the vault locked?" 2>/dev/null || true exit 1 fi systemd-run --user --collect --on-active=30s \ --unit="panama-clip-clear-$(date +%s)-$RANDOM" \ wl-copy --clear >/dev/null 2>&1 || true notify-send --icon=dialog-password-symbolic \ "Password copied" "\"$name\" — the clipboard clears in 30 seconds" 2>/dev/null || true ;; *) echo 'usage: panama-pick window|quit-window|process|ssh|recent|password' >&2 exit 2 ;; esac