Files
Panama/config/dot/quickshell/scripts/panama-pick
T
Gabriel Brown 333ab9f6b7 Press the browser key twice, get one browser
The application keys ran their command unconditionally, so pressing the
browser key twice gave you two browsers. That is the single most common
"Linux feels wrong" moment and it is a twenty-line fix: match the
window class, focus it if it is there, launch if it is not.

Two details the obvious version gets wrong. Patterns are anchored,
because an unanchored "mail" matches gmail-notifier and the mail key
would raise somebody's notifier. And class alone is not enough here:
the terminal and the editor are both kitty, so the editor bind narrows
by title or it raises whatever terminal happened to be open. The
contract's fixture contains both traps.

Found and fixed a bug this shipped with earlier today. Hyprland 0.56
dispatches through Lua, so `hyprctl dispatch focuswindow address:0x...`
is parsed as Lua source and fails with a syntax error -- which is what
the launcher's Switch Window command had been doing since it landed.
Its own contract stubbed hyprctl and recorded the arguments, so the
call looked correct and never ran. Both now use the selector form
hl.focus actually accepts, and the contract asserts the shape rather
than only that something was dispatched.
2026-08-22 06:02:47 -04:00

145 lines
6.2 KiB
Bash
Executable File

#!/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
# The Lua dispatch form. `hyprctl dispatch focuswindow address:0x...`
# is read as Lua source on 0.56 and fails with a parse error.
exec hyprctl dispatch "hl.dsp.focus({ window = \"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 <you>; 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