Files
Panama/tests/setup/launcher-commands-contract
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

263 lines
9.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# The launcher's OS-parity commands: power menu, reminders, and the dmenu
# pick-lists (windows, processes, SSH hosts, recent files).
#
# What must stay true:
#
# 1. Every shipped command is one Vicinae accepts. `vicinae script check`
# exits 0 even on rejection, so its output is what counts.
# 2. Power commands reach the session's own doors: logind's lock signal,
# uwsm for logout, systemctl for the rest. No bespoke session teardown.
# 3. A reminder is a transient user timer: a parseable delay becomes exactly
# one systemd-run call, nonsense is refused without creating anything.
# 4. A pick-list acts on the line that was picked -- the index math between
# dmenu's output and the hidden columns is exactly the kind of off-by-one
# that survives a reading.
# 5. Escape is a choice: every list exits 0 and does nothing.
#
# Backends are stubbed on PATH; the process test kills its own sleep, never a
# real one.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
scripts="$repo_dir/config/local/share/vicinae/scripts"
remind="$repo_dir/config/dot/quickshell/scripts/panama-remind"
pick="$repo_dir/config/dot/quickshell/scripts/panama-pick"
findings=()
note() { findings+=("$1"); }
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
calls="$work/calls"
stub_dir="$work/bin"
mkdir -p "$stub_dir"
# ── 1. Vicinae accepts every new command ─────────────────────────────────────
commands=(lock-screen suspend-system log-out reboot-system power-off
remind-me list-reminders pick-color copy-password
switch-window force-quit-window kill-process ssh-hosts recent-files)
for command in "${commands[@]}"; do
path="$scripts/$command"
[[ -x "$path" ]] || { note "$command is missing or not executable"; continue; }
grep -qE '@vicinae\.mode silent' "$path" \
|| note "$command is not a silent command"
if command -v vicinae >/dev/null 2>&1; then
output="$(vicinae script check "$path" 2>&1)"
[[ "$output" == *Error* ]] && note "Vicinae rejects $command: $output"
fi
done
# ── 2. Power commands use the session's own doors ────────────────────────────
grep -q 'loginctl lock-session' "$scripts/lock-screen" \
|| note 'lock-screen does not use the logind lock signal hypridle listens for'
grep -q 'uwsm stop' "$scripts/log-out" \
|| note 'log-out does not end the session through uwsm'
grep -q 'systemctl suspend' "$scripts/suspend-system" \
|| note 'suspend-system does not use systemctl'
grep -q 'systemctl reboot' "$scripts/reboot-system" \
|| note 'reboot-system does not use systemctl'
grep -q 'systemctl poweroff' "$scripts/power-off" \
|| note 'power-off does not use systemctl'
# ── 3. Reminders ─────────────────────────────────────────────────────────────
for stub in systemd-run systemctl notify-send; do
cat >"$stub_dir/$stub" <<STUB
#!/usr/bin/env bash
printf '%s %s\n' "$stub" "\$*" >>"$calls"
STUB
chmod +x "$stub_dir/$stub"
done
: >"$calls"
PATH="$stub_dir:$PATH" "$remind" add "1h30m" "stretch" >/dev/null 2>&1 \
|| note 'a valid relative reminder failed'
grep -q -- '--on-active=5400s' "$calls" \
|| note '1h30m did not become a 5400s timer'
grep -q -- '--description=stretch' "$calls" \
|| note 'the reminder text does not ride the unit description'
: >"$calls"
PATH="$stub_dir:$PATH" "$remind" add "soonish" "x" >/dev/null 2>&1 \
&& note 'a nonsense delay was accepted'
grep -q 'systemd-run' "$calls" \
&& note 'a nonsense delay still created a timer'
: >"$calls"
PATH="$stub_dir:$PATH" "$remind" cancel "panama-remind-1-2" >/dev/null 2>&1
grep -q 'systemctl --user stop panama-remind-1-2.timer' "$calls" \
|| note 'cancel does not stop the named timer'
# ── 4 & 5. Pick-lists act on the picked line ─────────────────────────────────
# dmenu stub: record the list it was shown, answer with $DMENU_ANSWER
# (unset = Escape).
cat >"$stub_dir/vicinae" <<'STUB'
#!/usr/bin/env bash
cat >"$DMENU_SEEN"
[[ -n "${DMENU_ANSWER:-}" ]] || exit 1
printf '%s\n' "$DMENU_ANSWER"
STUB
chmod +x "$stub_dir/vicinae"
export DMENU_SEEN="$work/dmenu-seen"
# hyprctl stub: serves a two-window fixture, records dispatches.
cat >"$stub_dir/hyprctl" <<STUB
#!/usr/bin/env bash
if [[ "\$1" == "clients" ]]; then cat "$work/clients.json"; exit 0; fi
printf 'hyprctl %s\n' "\$*" >>"$calls"
STUB
chmod +x "$stub_dir/hyprctl"
sleep 300 &
victim=$!
cat >"$work/clients.json" <<FIXTURE
[
{ "address": "0xaaa", "pid": 999999, "mapped": true, "title": "First", "class": "one", "workspace": { "id": 1 } },
{ "address": "0xbbb", "pid": $victim, "mapped": true, "title": "Second", "class": "two", "workspace": { "id": 2 } }
]
FIXTURE
# Picking the second window focuses the second address.
: >"$calls"
DMENU_ANSWER=1 PATH="$stub_dir:$PATH" "$pick" window >/dev/null 2>&1
grep -q 'hl.dsp.focus.*address:0xbbb' "$calls" \
|| note 'picking the second window did not focus the second address (Hyprland 0.56 needs the Lua dispatch form)'
# Escape does nothing.
: >"$calls"
PATH="$stub_dir:$PATH" "$pick" window >/dev/null 2>&1 \
|| note 'Escape from the window list is treated as a failure'
grep -q 'dispatch' "$calls" && note 'Escape still dispatched a focus'
# Force quit kills the picked pid -- our own sleep, which must die.
DMENU_ANSWER=1 PATH="$stub_dir:$PATH" "$pick" quit-window >/dev/null 2>&1
sleep 0.2
kill -0 "$victim" 2>/dev/null \
&& { note 'force quit did not terminate the picked window process'; kill -9 "$victim" 2>/dev/null; }
# Kill process: the list is ps output; picking line one signals that pid.
sleep 300 &
victim2=$!
cat >"$stub_dir/ps" <<STUB
#!/usr/bin/env bash
printf '%s 99.0 fake-hog\n' "$victim2"
STUB
chmod +x "$stub_dir/ps"
DMENU_ANSWER=0 PATH="$stub_dir:$PATH" "$pick" process >/dev/null 2>&1
sleep 0.2
kill -0 "$victim2" 2>/dev/null \
&& { note 'kill-process did not signal the picked pid'; kill -9 "$victim2" 2>/dev/null; }
# SSH hosts come from ~/.ssh/config, wildcards excluded, session in a terminal.
fake_home="$work/home"
mkdir -p "$fake_home/.ssh" "$fake_home/.local/share"
cat >"$fake_home/.ssh/config" <<'SSH'
Host alpha
HostName a.example
Host *
User git
Host beta gamma
SSH
cat >"$stub_dir/kitty" <<STUB
#!/usr/bin/env bash
printf 'kitty %s\n' "\$*" >>"$calls"
STUB
chmod +x "$stub_dir/kitty"
: >"$calls"
DMENU_ANSWER=1 HOME="$fake_home" PATH="$stub_dir:$PATH" "$pick" ssh >/dev/null 2>&1
grep -qx '\*' "$DMENU_SEEN" && note 'a wildcard Host is offered as a session'
grep -q 'kitty --detach ssh beta' "$calls" \
|| note 'picking the second host did not open a session to it'
# Recent files decode their URIs; a %20 must come back as a space.
cat >"$fake_home/.local/share/recently-used.xbel" <<'XBEL'
<?xml version="1.0"?>
<xbel version="1.0">
<bookmark href="file:///tmp/older%20file.txt" modified="2026-08-20T10:00:00Z"/>
<bookmark href="file:///tmp/newer.txt" modified="2026-08-21T10:00:00Z"/>
</xbel>
XBEL
cat >"$stub_dir/xdg-open" <<STUB
#!/usr/bin/env bash
printf 'xdg-open %s\n' "\$*" >>"$calls"
STUB
chmod +x "$stub_dir/xdg-open"
: >"$calls"
DMENU_ANSWER=1 HOME="$fake_home" PATH="$stub_dir:$PATH" "$pick" recent >/dev/null 2>&1
grep -q 'xdg-open /tmp/older file.txt' "$calls" \
|| note 'the picked recent file was not opened with its URI decoded (newest-first order, %20 as space)'
# ── 6. Passwords: the secret's whole journey is a pipe ───────────────────────
#
# The rules that make Copy Password safe to ship at all:
# * the secret is never an argument to anything (argv is world-readable)
# * it reaches wl-copy with --sensitive, the hint vicinae's clipboard
# history documents it ignores -- without it, passwords land in a
# plaintext FTS database
# * a clipboard clear is scheduled the moment the copy happens
# * an unconfigured rbw degrades to a setup message, never an error
secret='s3cr3t-fixture-value'
cat >"$stub_dir/rbw" <<STUB
#!/usr/bin/env bash
printf 'rbw %s\n' "\$*" >>"$calls"
case "\$1" in
list) printf 'github\tgib\nrouter\t\n';;
get) printf '%s\n' '$secret';;
esac
STUB
chmod +x "$stub_dir/rbw"
cat >"$stub_dir/wl-copy" <<STUB
#!/usr/bin/env bash
printf 'wl-copy %s\n' "\$*" >>"$calls"
cat >"$work/wl-copy-stdin"
STUB
chmod +x "$stub_dir/wl-copy"
: >"$calls"; : >"$work/wl-copy-stdin"
DMENU_ANSWER=0 PATH="$stub_dir:$PATH" "$pick" password >/dev/null 2>&1 \
|| note 'copying the first password entry failed'
grep -q "$secret" "$calls" \
&& note 'the secret appears on a command line'
grep -qx "$secret" "$work/wl-copy-stdin" \
|| note 'the secret does not reach wl-copy on stdin'
grep -q 'wl-copy.*--sensitive' "$calls" \
|| note 'the copy is not marked sensitive, so it lands in clipboard history'
grep -q 'rbw get -- github gib' "$calls" \
|| note 'the picked entry name and user do not reach rbw get, flag-safe'
grep -qE 'systemd-run .*--on-active=30s .*wl-copy --clear' "$calls" \
|| note 'no clipboard clear is scheduled after the copy'
# Escape copies nothing.
: >"$calls"
PATH="$stub_dir:$PATH" "$pick" password >/dev/null 2>&1
grep -q 'wl-copy' "$calls" && note 'Escape from the password list still copied something'
# Unconfigured rbw is a setup hint, not a failure.
cat >"$stub_dir/rbw" <<'STUB'
#!/usr/bin/env bash
exit 1
STUB
chmod +x "$stub_dir/rbw"
: >"$calls"
PATH="$stub_dir:$PATH" "$pick" password >/dev/null 2>&1 \
|| note 'an unconfigured rbw is treated as a crash instead of a setup hint'
grep -q 'not set up' "$calls" \
|| note 'an unconfigured rbw does not explain how to set it up'
if (( ${#findings[@]} > 0 )); then
printf 'launcher commands contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'launcher commands contract: PASS\n'