The launcher's Copy Password command, built alone and last as the plan required, because every line of it is the security design: the secret travels rbw to wl-copy through a pipe -- never argv, never a file -- and the copy carries wl-clipboard's --sensitive hint, which vicinae's clipboard history documents it ignores. That claim was not taken on faith: a plain probe landed in the live history database and a sensitive one did not, before any of this was written. A transient timer clears the clipboard after thirty seconds. An unconfigured rbw gets a setup message; a vault that locks between list and get gets an honest failure instead of an empty copy claiming success. rbw joins desktop-packages, and the contract pins the whole journey with a stub vault, including that the secret never appears on a command line.
143 lines
6.0 KiB
Bash
Executable File
143 lines
6.0 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
|
|
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 <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
|