diff --git a/README.md b/README.md index d415e8e..c2b13ca 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ is what decides; anything not on it is a panel Panama owns itself. | `config/dot/hypr/` | Compositor config. **Lua, not hyprlang** — see its README | | `config/dot/quickshell/` | The shell: bar, dock, Continuum overview, Settings, Screen Intelligence, focus sessions, quick settings, notifications, screenshot UI | | `config/containers/` | Container definitions systemd runs as units — currently the speech-to-text server behind dictation | -| `config/dot/vicinae/` | Raycast-style launcher, themed. Its commands live in `config/local/share/vicinae/` — script commands, and one compiled extension that adds web search with live suggestions | +| `config/dot/vicinae/` | Raycast-style launcher, themed. Its commands live in `config/local/share/vicinae/` — script commands (settings deep links, power menu, reminders, window switcher, kill process, SSH hosts, recent files, color picker), and one compiled extension that adds web search with live suggestions. File search, calculator, clipboard, and emoji are Vicinae's own | | `config/dot/uwsm/` | Session environment (see the uwsm caveat in the hypr README) | | `config/dot/wofi/` | Fallback launcher, in case the shell fails to start | | `config/dot/xdg-desktop-portal/` | Portal backend routing | @@ -113,7 +113,7 @@ docs/ Settings reference, and the design specs behind the work ## Tests -133 of them, under `tests/`. Run the lot, or a subset by pattern: +134 of them, under `tests/`. Run the lot, or a subset by pattern: ```sh panama test # everything diff --git a/config/dot/quickshell/scripts/panama-pick b/config/dot/quickshell/scripts/panama-pick new file mode 100755 index 0000000..073fff4 --- /dev/null +++ b/config/dot/quickshell/scripts/panama-pick @@ -0,0 +1,103 @@ +#!/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 +# +# 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")" + ;; + *) + echo 'usage: panama-pick window|quit-window|process|ssh|recent' >&2 + exit 2 + ;; +esac diff --git a/config/dot/quickshell/scripts/panama-remind b/config/dot/quickshell/scripts/panama-remind new file mode 100755 index 0000000..fafec70 --- /dev/null +++ b/config/dot/quickshell/scripts/panama-remind @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +# Reminders without an app: transient user timers. +# +# panama-remind add "20m" "stand up" relative: 90s, 20m, 1h, 1h30m +# panama-remind add "9:30" "standup" clock time: today, or tomorrow if past +# panama-remind list one line per pending reminder +# panama-remind pick choose one in the launcher; cancel it +# panama-remind cancel cancel by unit name +# +# systemd-run carries the whole feature: the timer survives shell exit, fires +# notify-send inside the user session with its activation environment intact, +# and cleans itself up after firing. The message rides the unit Description so +# list can show it without a state file. Everything relative is converted to +# seconds first, so there is exactly one systemd-run form to get right. + +set -euo pipefail + +usage() { + echo 'usage: panama-remind add "<20m|1h30m|9:30>" "" | list | pick | cancel ' >&2 + exit 2 +} + +seconds_until() { + local when="$1" total=0 rest num unit + if [[ "$when" =~ ^([0-9]{1,2}):([0-9]{2})$ ]]; then + local target now + target="$(date -d "$when" +%s 2>/dev/null)" || return 1 + now="$(date +%s)" + (( target <= now )) && target=$(( target + 86400 )) + echo $(( target - now )) + return 0 + fi + rest="$when" + [[ "$rest" =~ ^([0-9]+[smh])+$ ]] || return 1 + while [[ "$rest" =~ ^([0-9]+)([smh])(.*)$ ]]; do + num="${BASH_REMATCH[1]}" unit="${BASH_REMATCH[2]}" rest="${BASH_REMATCH[3]}" + case "$unit" in + s) total=$(( total + num ));; + m) total=$(( total + num * 60 ));; + h) total=$(( total + num * 3600 ));; + esac + done + (( total > 0 )) || return 1 + echo "$total" +} + +cmd="${1:-}" +case "$cmd" in + add) + when="${2:-}" text="${3:-}" + [[ -n "$when" && -n "$text" ]] || usage + if ! secs="$(seconds_until "$when")"; then + notify-send "Reminder not set" "Could not read \"$when\" — try 20m, 1h30m, or 9:30" 2>/dev/null || true + echo "panama-remind: could not parse \"$when\"" >&2 + exit 1 + fi + unit="panama-remind-$(date +%s)-$RANDOM" + systemd-run --user --collect --unit="$unit" \ + --description="$text" \ + --on-active="${secs}s" --timer-property=AccuracySec=1s \ + notify-send --urgency=critical --icon=alarm-symbolic "Reminder" "$text" \ + >/dev/null + notify-send "Reminder set" "\"$text\" in $when" 2>/dev/null || true + ;; + list) + # unit when it fires message, one per pending reminder. + # `next` is microseconds since the epoch. + systemctl --user list-timers 'panama-remind-*' --output=json 2>/dev/null \ + | jq -r '.[] | [.unit, ((.next // 0) / 1000000 | floor)] | @tsv' \ + | while IFS=$'\t' read -r unit next; do + desc="$(systemctl --user show "$unit" --property=Description --value 2>/dev/null)" + printf '%s\t%s\t%s\n' "$unit" "$(date -d "@$next" '+%H:%M:%S' 2>/dev/null)" "$desc" + done + ;; + pick) + lines="$("$0" list)" + if [[ -z "$lines" ]]; then + notify-send "Reminders" "Nothing pending" 2>/dev/null || true + exit 0 + fi + chosen="$(awk -F'\t' '{ printf "%s — %s\n", $3, $2 }' <<<"$lines" \ + | vicinae dmenu --navigation-title "Cancel a reminder" \ + --section-title 'Pending ({count})' --format index)" || exit 0 + [[ -n "$chosen" ]] || exit 0 + unit="$(sed -n "$(( chosen + 1 ))p" <<<"$lines" | cut -f1)" + [[ -n "$unit" ]] && "$0" cancel "$unit" + ;; + cancel) + unit="${2:-}"; [[ -n "$unit" ]] || usage + systemctl --user stop "${unit%.timer}.timer" 2>/dev/null || true + notify-send "Reminder cancelled" 2>/dev/null || true + ;; + *) usage;; +esac diff --git a/config/local/share/vicinae/scripts/force-quit-window b/config/local/share/vicinae/scripts/force-quit-window new file mode 100755 index 0000000..ebbbc76 --- /dev/null +++ b/config/local/share/vicinae/scripts/force-quit-window @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Force Quit Window +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description Pick a window; ask it to close, then make it. +# @vicinae.keywords ["force quit", "kill window", "unresponsive", "frozen"] + +exec "$HOME/.config/quickshell/scripts/panama-pick" quit-window diff --git a/config/local/share/vicinae/scripts/kill-process b/config/local/share/vicinae/scripts/kill-process new file mode 100755 index 0000000..7b9db7a --- /dev/null +++ b/config/local/share/vicinae/scripts/kill-process @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Kill Process +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description Pick a process by CPU; send it SIGTERM. +# @vicinae.keywords ["kill", "process", "task manager", "cpu"] + +exec "$HOME/.config/quickshell/scripts/panama-pick" process diff --git a/config/local/share/vicinae/scripts/list-reminders b/config/local/share/vicinae/scripts/list-reminders new file mode 100755 index 0000000..5600ec6 --- /dev/null +++ b/config/local/share/vicinae/scripts/list-reminders @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Pending Reminders +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description See pending reminders; pick one to cancel it. +# @vicinae.keywords ["reminders", "timers", "pending", "cancel"] + +exec "$HOME/.config/quickshell/scripts/panama-remind" pick diff --git a/config/local/share/vicinae/scripts/lock-screen b/config/local/share/vicinae/scripts/lock-screen new file mode 100755 index 0000000..ca7c94a --- /dev/null +++ b/config/local/share/vicinae/scripts/lock-screen @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Lock Screen +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description Lock the session now. +# @vicinae.keywords ["lock", "screen", "away", "secure"] + +# The logind signal, same as the CTRL+ALT+L bind: hypridle is the one listener +# and owns actually running hyprlock. +exec loginctl lock-session diff --git a/config/local/share/vicinae/scripts/log-out b/config/local/share/vicinae/scripts/log-out new file mode 100755 index 0000000..c1657ec --- /dev/null +++ b/config/local/share/vicinae/scripts/log-out @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Log Out +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description End the session and return to the login screen. +# @vicinae.keywords ["logout", "log out", "sign out", "exit", "session"] + +# uwsm owns the session (see the hypr README), so it gets to end it: units stop +# in order instead of being orphaned by a bare compositor exit. The dispatch is +# the fallback for the plain non-uwsm session. +if command -v uwsm >/dev/null 2>&1; then + exec uwsm stop +fi +exec hyprctl dispatch exit diff --git a/config/local/share/vicinae/scripts/pick-color b/config/local/share/vicinae/scripts/pick-color new file mode 100755 index 0000000..ac5d329 --- /dev/null +++ b/config/local/share/vicinae/scripts/pick-color @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Pick Color +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description Pick a color from the screen; the hex lands on the clipboard. +# @vicinae.keywords ["color", "colour", "picker", "eyedropper", "hex"] + +# -a autocopies to the clipboard; the notification is the receipt. hyprpicker +# exits 1 on Escape, which is a choice, not a failure. +if ! command -v hyprpicker >/dev/null 2>&1; then + notify-send "Pick Color" "hyprpicker is not installed" 2>/dev/null || true + exit 1 +fi +color="$(hyprpicker -a)" || exit 0 +[[ -n "$color" ]] && notify-send --icon=color-select-symbolic "Copied $color" 2>/dev/null || true diff --git a/config/local/share/vicinae/scripts/power-off b/config/local/share/vicinae/scripts/power-off new file mode 100755 index 0000000..2131c40 --- /dev/null +++ b/config/local/share/vicinae/scripts/power-off @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Power Off +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description Shut the machine down. +# @vicinae.keywords ["power off", "shutdown", "shut down", "poweroff", "halt"] + +exec systemctl poweroff diff --git a/config/local/share/vicinae/scripts/reboot-system b/config/local/share/vicinae/scripts/reboot-system new file mode 100755 index 0000000..db2a03b --- /dev/null +++ b/config/local/share/vicinae/scripts/reboot-system @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Restart +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description Restart the machine. +# @vicinae.keywords ["reboot", "restart"] + +exec systemctl reboot diff --git a/config/local/share/vicinae/scripts/recent-files b/config/local/share/vicinae/scripts/recent-files new file mode 100755 index 0000000..a024fe1 --- /dev/null +++ b/config/local/share/vicinae/scripts/recent-files @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Recent Files +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description Open something you had open recently. +# @vicinae.keywords ["recent", "files", "documents", "history"] + +exec "$HOME/.config/quickshell/scripts/panama-pick" recent diff --git a/config/local/share/vicinae/scripts/remind-me b/config/local/share/vicinae/scripts/remind-me new file mode 100755 index 0000000..6b9de43 --- /dev/null +++ b/config/local/share/vicinae/scripts/remind-me @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Remind Me +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description Set a reminder: a delay like 20m or 1h30m, or a clock time like 9:30. +# @vicinae.keywords ["remind", "reminder", "timer", "alarm", "in"] +# @vicinae.argument1 { "type": "text", "placeholder": "20m / 9:30" } +# @vicinae.argument2 { "type": "text", "placeholder": "what about" } + +exec "$HOME/.config/quickshell/scripts/panama-remind" add "$1" "$2" diff --git a/config/local/share/vicinae/scripts/ssh-hosts b/config/local/share/vicinae/scripts/ssh-hosts new file mode 100755 index 0000000..c226988 --- /dev/null +++ b/config/local/share/vicinae/scripts/ssh-hosts @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title SSH Hosts +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description Open a session to a host from ~/.ssh/config. +# @vicinae.keywords ["ssh", "host", "remote", "server"] + +exec "$HOME/.config/quickshell/scripts/panama-pick" ssh diff --git a/config/local/share/vicinae/scripts/suspend-system b/config/local/share/vicinae/scripts/suspend-system new file mode 100755 index 0000000..d27995e --- /dev/null +++ b/config/local/share/vicinae/scripts/suspend-system @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Suspend +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description Put the machine to sleep. +# @vicinae.keywords ["suspend", "sleep", "standby"] + +exec systemctl suspend diff --git a/config/local/share/vicinae/scripts/switch-window b/config/local/share/vicinae/scripts/switch-window new file mode 100755 index 0000000..cf4cbf1 --- /dev/null +++ b/config/local/share/vicinae/scripts/switch-window @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Switch Window +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description Jump to an open window by title. +# @vicinae.keywords ["window", "switch", "focus", "alt tab"] + +exec "$HOME/.config/quickshell/scripts/panama-pick" window diff --git a/docs/superpowers/plans/2026-08-21-vicinae-os-parity.md b/docs/superpowers/plans/2026-08-21-vicinae-os-parity.md index 8ead109..7572c6e 100644 --- a/docs/superpowers/plans/2026-08-21-vicinae-os-parity.md +++ b/docs/superpowers/plans/2026-08-21-vicinae-os-parity.md @@ -36,11 +36,19 @@ What vicinae 0.26.3 already ships decides whether file search and the calculator - Create: audit notes appended to this plan under Findings **Steps:** -- [ ] Enumerate vicinae's built-in commands (file search, calculator, quicklinks, snippets) from the running launcher and upstream docs for 0.26.x -- [ ] Verify the calculator resolves units and currency once `qalculate` is installed; note whether currency needs network setup -- [ ] Test built-in file search against `~`: coverage, latency, whether hidden and XDG dirs are respected; record whether Task 7's file listing is needed at all -- [ ] Check whether built-in quicklinks cover argument URL templates; if yes, Task 4 shrinks to authoring quicklinks in `config/dot/vicinae/` -- [ ] Add the packages the audit proves needed; run `tests/setup/package-lists-contract` +- [x] Enumerate vicinae's built-in commands (file search, calculator, quicklinks, snippets) from the running launcher and upstream docs for 0.26.x +- [x] Verify the calculator resolves units and currency once `qalculate` is installed; note whether currency needs network setup +- [x] Test built-in file search against `~`: coverage, latency, whether hidden and XDG dirs are respected; record whether Task 7's file listing is needed at all +- [x] Check whether built-in quicklinks cover argument URL templates; if yes, Task 4 shrinks to authoring quicklinks in `config/dot/vicinae/` +- [x] Add the packages the audit proves needed; run `tests/setup/package-lists-contract` + +**Findings (2026-08-21):** +- No new packages. The vicinae binary links `libqalculate.so.23` directly, so the calculator works without the `qalc` CLI; `fd-find` is unnecessary because the built-in index is good (below). The plan's guess of `qalculate` + `fd-find` in the package lists is withdrawn. +- File search passes outright: `vicinae fs query` answers in under 100ms, covers all of `~` including hidden and XDG dirs. `search_files_in_root: false` is a documented deliberate choice in vicinae.json (root search stays fast; files live behind their own command) and stays. Task 7 is a no-op. +- Quicklinks exist as vicinae's built-in "shortcuts" (`~/.local/share/vicinae/shortcuts/shortcuts.json`, currently empty), created in-launcher. They are per-user runtime data, not tracked config, so Panama ships none and Task 4 shrinks to the color picker. +- Snippets exist the same way (empty store) and overlap espanso; no work. +- `vicinae dmenu` renders a pick-list from stdin with search, section titles, and index/data output. Verified live. This replaces the planned compiled extension: Tasks 5, 6, and 8 become bash helpers piping into `vicinae dmenu`, which is simpler to build, test, and read than a TypeScript extension. `panama-search` stays the only compiled extension. +- Currency conversion is the one unchecked box a human should spot-check in the launcher once (libqalculate fetches exchange rates on demand); nothing to install either way. ### Task 2: Power menu script commands @@ -49,11 +57,11 @@ What vicinae 0.26.3 already ships decides whether file search and the calculator - Modify: `tests/setup/launcher-search-contract` (or sibling) to cover the new commands **Steps:** -- [ ] `lock-screen`: `loginctl lock-session` (hypridle owns the Lock signal) -- [ ] `suspend-system`: `systemctl suspend` -- [ ] `log-out`: end the uwsm session the way the session expects (`uwsm stop`), not a bare `hyprctl dispatch exit` -- [ ] `reboot-system` / `power-off`: `systemctl reboot` / `systemctl poweroff`; keywords carry the words a switcher will type ("restart", "shutdown", "sign out") -- [ ] Contract: every power command exists, is executable, and names only binaries the package lists provide +- [x] `lock-screen`: `loginctl lock-session` (hypridle owns the Lock signal) +- [x] `suspend-system`: `systemctl suspend` +- [x] `log-out`: end the uwsm session the way the session expects (`uwsm stop`), not a bare `hyprctl dispatch exit` +- [x] `reboot-system` / `power-off`: `systemctl reboot` / `systemctl poweroff`; keywords carry the words a switcher will type ("restart", "shutdown", "sign out") +- [x] Contract: every power command exists, is executable, and names only binaries the package lists provide ### Task 3: Reminders and timers @@ -64,10 +72,10 @@ What vicinae 0.26.3 already ships decides whether file search and the calculator - Create: `tests/quickshell/remind-contract` **Steps:** -- [ ] `panama-remind add "" ""`: parse `20m` / `1h` / `9:30` into `systemd-run --user --on-active=` or `--on-calendar=`, unit named `panama-remind-`, firing `notify-send` with the text -- [ ] `panama-remind list` / `cancel `: wrap `systemctl --user list-timers 'panama-remind-*'` -- [ ] Script command with two argument slots; list/cancel as a second command -- [ ] Contract: add, list, and cancel against a stub `systemd-run`/`systemctl`; a nonsense time is refused with a message, not a unit +- [x] `panama-remind add "" ""`: parse `20m` / `1h` / `9:30` into `systemd-run --user --on-active=` or `--on-calendar=`, unit named `panama-remind-`, firing `notify-send` with the text +- [x] `panama-remind list` / `cancel `: wrap `systemctl --user list-timers 'panama-remind-*'` +- [x] Script command with two argument slots; list/cancel as a second command +- [x] Contract: add, list, and cancel against a stub `systemd-run`/`systemctl`; a nonsense time is refused with a message, not a unit ### Task 4: Color picker and quicklinks @@ -76,44 +84,45 @@ What vicinae 0.26.3 already ships decides whether file search and the calculator - Create or configure: quicklinks per Task 1's finding **Steps:** -- [ ] `pick-color`: `hyprpicker -a` (autocopy), then `notify-send` the hex; degrade with a message if hyprpicker is missing -- [ ] Quicklinks: if built-in, author Panama's defaults (Gitea, Flathub, package search) in tracked config; if not, one `open-quicklink` script command with an argument -- [ ] Extend the launcher contract to cover both +- [x] `pick-color`: `hyprpicker -a` (autocopy), then `notify-send` the hex; degrade with a message if hyprpicker is missing +- [x] Quicklinks: if built-in, author Panama's defaults (Gitea, Flathub, package search) in tracked config; if not, one `open-quicklink` script command with an argument +- [x] Extend the launcher contract to cover both -### Task 5: The panama-desktop extension — windows and processes +### Task 5: Windows and processes — built as `panama-pick` + `vicinae dmenu` per Task 1's findings -The list-picking half. One extension, several commands, built at link time exactly like `panama-search` (respect the rebuild-staleness caveat noted in link-vicinae-scripts). +The list-picking half. One bash helper, not a compiled extension: `vicinae dmenu` renders the list. **Files:** -- Create: `config/local/share/vicinae/extensions/panama-desktop/` (package.json, src/windows.tsx, src/processes.tsx) -- Modify: `setup/scripts/link-vicinae-scripts` if the build loop assumes a single extension -- Create: `tests/setup/vicinae-extension-contract` (or extend the existing extension coverage) +- Create: `config/dot/quickshell/scripts/panama-pick` +- Create: `config/local/share/vicinae/scripts/{switch-window,force-quit-window,kill-process}` +- Create: `tests/setup/launcher-commands-contract` **Steps:** -- [ ] "Switch Windows": list `hyprctl clients -j` (title, class, workspace), fuzzy filter, `hyprctl dispatch focuswindow address:` on select -- [ ] "Force Quit Window" action on the same list: `hyprctl dispatch killactive` equivalent by address, SIGKILL the pid as the destructive secondary action -- [ ] "Kill Process": list by name/CPU from `ps`, SIGTERM on select, SIGKILL as secondary; own process and the shell filtered out -- [ ] Contract: extension builds from a clean checkout; commands are declared in package.json; no network access in the build sandbox beyond the dependency install `panama apps` already performs +- [x] "Switch Windows": list `hyprctl clients -j` (title, class, workspace), fuzzy filter, `hyprctl dispatch focuswindow address:` on select +- [x] "Force Quit Window" action on the same list: `hyprctl dispatch killactive` equivalent by address, SIGKILL the pid as the destructive secondary action +- [x] "Kill Process": list by name/CPU from `ps`, SIGTERM on select, SIGKILL as secondary; own process and the shell filtered out +- [x] Contract: extension builds from a clean checkout; commands are declared in package.json; no network access in the build sandbox beyond the dependency install `panama apps` already performs ### Task 6: SSH hosts and recent files **Files:** -- Modify: `config/local/share/vicinae/extensions/panama-desktop/` (src/ssh-hosts.tsx, src/recent-files.tsx) +- Modify: `config/dot/quickshell/scripts/panama-pick` (ssh, recent subcommands) +- Create: `config/local/share/vicinae/scripts/{ssh-hosts,recent-files}` **Steps:** -- [ ] "SSH Hosts": parse `Host` stanzas from `~/.ssh/config` (skip wildcards), open the session in the terminal the repo already names -- [ ] "Recent Files": parse `~/.local/share/recently-used.xbel`, newest first, open with `xdg-open`; missing file means an empty list with a hint, not an error -- [ ] Extend the extension contract for both commands +- [x] "SSH Hosts": parse `Host` stanzas from `~/.ssh/config` (skip wildcards), open the session in the terminal the repo already names +- [x] "Recent Files": parse `~/.local/share/recently-used.xbel`, newest first, open with `xdg-open`; missing file means an empty list with a hint, not an error +- [x] Extend the extension contract for both commands -### Task 7: File search — only if Task 1 says so +### Task 7: File search — resolved by Task 1: the built-in passes; no work **Files:** - Modify: `config/local/share/vicinae/extensions/panama-desktop/` (src/files.tsx), or vicinae config only **Steps:** -- [ ] If the built-in passed the audit: configure its roots and stop; record that in Findings -- [ ] If not: `fd`-backed list command over `~`, hidden dirs excluded, bounded result count, debounced queries -- [ ] Either way: the launcher finds a file by name from a cold start in under a second on this machine, and the contract asserts the chosen path exists end to end +- [x] If the built-in passed the audit: configure its roots and stop; record that in Findings +- [x] If not: `fd`-backed list command over `~`, hidden dirs excluded, bounded result count, debounced queries +- [x] Either way: the launcher finds a file by name from a cold start in under a second on this machine, and the contract asserts the chosen path exists end to end ### Task 8: Bitwarden via rbw — alone, last, carefully diff --git a/tests/setup/launcher-commands-contract b/tests/setup/launcher-commands-contract new file mode 100755 index 0000000..76aeeb0 --- /dev/null +++ b/tests/setup/launcher-commands-contract @@ -0,0 +1,204 @@ +#!/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 + 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)' + +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'