#!/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" <>"$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" <>"$calls" STUB chmod +x "$stub_dir/hyprctl" sleep 300 & victim=$! cat >"$work/clients.json" <"$calls" DMENU_ANSWER=1 PATH="$stub_dir:$PATH" "$pick" window >/dev/null 2>&1 grep -q 'hyprctl dispatch focuswindow address:0xbbb' "$calls" \ || note 'picking the second window did not focus the second address' # 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" </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" <>"$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' XBEL cat >"$stub_dir/xdg-open" <>"$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" <>"$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" <>"$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'