Teach the launcher what an operating system knows

The OS-parity batch from the vicinae plan, tasks 1 through 7. The audit
came back better than the plan guessed: the calculator already links
libqalculate, the built-in file index answers in under 100ms across all
of home, quicklinks and snippets ship as built-in stores -- so zero new
packages, and `vicinae dmenu` replaces the planned compiled extension
outright. What was missing gets built: a power menu (lock, suspend, log
out through uwsm, restart, power off), reminders as transient systemd
timers with a pick-to-cancel list, a color picker over hyprpicker, and
dmenu pick-lists for window switching, force quit, kill process, SSH
hosts, and recent files -- all through one panama-pick helper. The
launcher commands contract exercises the reminder parsing and every
pick-list against stubs, including killing its own sacrificial sleep.
This commit is contained in:
Gabriel Brown
2026-08-21 19:32:11 -04:00
parent 1e430881e5
commit 4e978bf3b7
18 changed files with 581 additions and 36 deletions
+2 -2
View File
@@ -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/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/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/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/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/wofi/` | Fallback launcher, in case the shell fails to start |
| `config/dot/xdg-desktop-portal/` | Portal backend routing | | `config/dot/xdg-desktop-portal/` | Portal backend routing |
@@ -113,7 +113,7 @@ docs/ Settings reference, and the design specs behind the work
## Tests ## 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 ```sh
panama test # everything panama test # everything
+103
View File
@@ -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
+95
View File
@@ -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 <unit> 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>" "<text>" | list | pick | cancel <unit>' >&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 <TAB> when it fires <TAB> 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
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+11
View File
@@ -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
+15
View File
@@ -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
+16
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+11
View File
@@ -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"
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
@@ -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 - Create: audit notes appended to this plan under Findings
**Steps:** **Steps:**
- [ ] Enumerate vicinae's built-in commands (file search, calculator, quicklinks, snippets) from the running launcher and upstream docs for 0.26.x - [x] 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 - [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 - [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
- [ ] Check whether built-in quicklinks cover argument URL templates; if yes, Task 4 shrinks to authoring quicklinks in `config/dot/vicinae/` - [x] 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] 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 ### 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 - Modify: `tests/setup/launcher-search-contract` (or sibling) to cover the new commands
**Steps:** **Steps:**
- [ ] `lock-screen`: `loginctl lock-session` (hypridle owns the Lock signal) - [x] `lock-screen`: `loginctl lock-session` (hypridle owns the Lock signal)
- [ ] `suspend-system`: `systemctl suspend` - [x] `suspend-system`: `systemctl suspend`
- [ ] `log-out`: end the uwsm session the way the session expects (`uwsm stop`), not a bare `hyprctl dispatch exit` - [x] `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") - [x] `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] Contract: every power command exists, is executable, and names only binaries the package lists provide
### Task 3: Reminders and timers ### 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` - Create: `tests/quickshell/remind-contract`
**Steps:** **Steps:**
- [ ] `panama-remind add "<when>" "<text>"`: parse `20m` / `1h` / `9:30` into `systemd-run --user --on-active=` or `--on-calendar=`, unit named `panama-remind-<id>`, firing `notify-send` with the text - [x] `panama-remind add "<when>" "<text>"`: parse `20m` / `1h` / `9:30` into `systemd-run --user --on-active=` or `--on-calendar=`, unit named `panama-remind-<id>`, firing `notify-send` with the text
- [ ] `panama-remind list` / `cancel <id>`: wrap `systemctl --user list-timers 'panama-remind-*'` - [x] `panama-remind list` / `cancel <id>`: wrap `systemctl --user list-timers 'panama-remind-*'`
- [ ] Script command with two argument slots; list/cancel as a second command - [x] 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] 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 ### 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 - Create or configure: quicklinks per Task 1's finding
**Steps:** **Steps:**
- [ ] `pick-color`: `hyprpicker -a` (autocopy), then `notify-send` the hex; degrade with a message if hyprpicker is missing - [x] `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 - [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
- [ ] Extend the launcher contract to cover both - [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:** **Files:**
- Create: `config/local/share/vicinae/extensions/panama-desktop/` (package.json, src/windows.tsx, src/processes.tsx) - Create: `config/dot/quickshell/scripts/panama-pick`
- Modify: `setup/scripts/link-vicinae-scripts` if the build loop assumes a single extension - Create: `config/local/share/vicinae/scripts/{switch-window,force-quit-window,kill-process}`
- Create: `tests/setup/vicinae-extension-contract` (or extend the existing extension coverage) - Create: `tests/setup/launcher-commands-contract`
**Steps:** **Steps:**
- [ ] "Switch Windows": list `hyprctl clients -j` (title, class, workspace), fuzzy filter, `hyprctl dispatch focuswindow address:` on select - [x] "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 - [x] "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 - [x] "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] 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 ### Task 6: SSH hosts and recent files
**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:** **Steps:**
- [ ] "SSH Hosts": parse `Host` stanzas from `~/.ssh/config` (skip wildcards), open the session in the terminal the repo already names - [x] "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 - [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
- [ ] Extend the extension contract for both commands - [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:** **Files:**
- Modify: `config/local/share/vicinae/extensions/panama-desktop/` (src/files.tsx), or vicinae config only - Modify: `config/local/share/vicinae/extensions/panama-desktop/` (src/files.tsx), or vicinae config only
**Steps:** **Steps:**
- [ ] If the built-in passed the audit: configure its roots and stop; record that in Findings - [x] 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 - [x] 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] 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 ### Task 8: Bitwarden via rbw — alone, last, carefully
+204
View File
@@ -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" <<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 '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" <<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)'
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'