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.
145 lines
6.2 KiB
Bash
Executable File
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
|