The first run of the suite on a laptop found five contracts asserting the desktop instead of the code. settings-system pinned DP-2 at 4500x3000 in XRGB2101010; it now asks Hyprland what is actually primary. ssh-keys hardcoded id_ed25519; it now uses whichever key exists. switcher's live half stepped a session with one window, which step() deliberately refuses. displays raced the service's revert readback -- the compositor looks restored while verification still holds busy, so an immediate apply was refused with its error already cleared; the harness now exposes settled and the contract waits for it. declared-dependencies gets an OPTIONAL list for docker: the aliases serve machines that run Docker deliberately, Panama's runtime is rootless podman, and a missing docker fails by naming the command, which is loud enough. Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
103 lines
4.7 KiB
Bash
Executable File
103 lines
4.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The Alt-Tab overlay, and getting out of it.
|
|
#
|
|
# The gesture is driven by compositor binds: SUPER+Tab steps, and a bind on
|
|
# SUPER RELEASE commits by running `qs ipc call switcher commit`. That works,
|
|
# but it has a failure mode nobody had thought about -- if the commit call ever
|
|
# fails to land, the overlay stayed up with no keyboard focus, no Escape
|
|
# handler and nothing clickable. The only way out was an IPC call typed into a
|
|
# terminal the overlay was covering.
|
|
#
|
|
# So the properties here are about recovery, not about the happy path:
|
|
#
|
|
# 1. Clicking outside the card puts it away.
|
|
# 2. Clicking a row switches to that window. It is the obvious thing to try
|
|
# with a list of windows on screen, and it did nothing.
|
|
# 3. An abandoned switch closes itself. A lost commit should cost a pause,
|
|
# not the session.
|
|
# 4. Keyboard focus stays with the compositor. Taking it mid-switch is the
|
|
# one thing that would break stepping, so the fixes above must not.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
window="$repo_dir/config/dot/quickshell/modules/switcher/WindowSwitcher.qml"
|
|
state="$repo_dir/config/dot/quickshell/services/WindowSwitcherState.qml"
|
|
|
|
findings=()
|
|
note() { findings+=("$1"); }
|
|
|
|
[[ -r "$window" ]] || { printf 'switcher contract: %s is missing\n' "$window" >&2; exit 1; }
|
|
|
|
# ── 1 & 2. It responds to a pointer ─────────────────────────────────────────
|
|
|
|
grep -q 'onClicked: WindowSwitcherState.cancel()' "$window" \
|
|
|| note 'clicking outside the switcher does not dismiss it'
|
|
grep -q 'onClicked: WindowSwitcherState.selectAt' "$window" \
|
|
|| note 'clicking a window in the list does not switch to it'
|
|
grep -q 'function selectAt' "$state" \
|
|
|| note 'the switcher cannot commit to a specific entry, only to the one stepping landed on'
|
|
|
|
# selectAt must refuse an index that is not there, or a stale click after the
|
|
# list shrinks would activate whatever happens to be at that position.
|
|
grep -q 'position >= root.windows.length' "$state" \
|
|
|| note 'selectAt does not bound-check its index'
|
|
|
|
# ── 3. An abandoned switch closes itself ────────────────────────────────────
|
|
|
|
python3 - "$window" <<'PY' || note 'there is no timeout, so a lost commit leaves the overlay up forever'
|
|
import re, sys
|
|
text = open(sys.argv[1], encoding="utf-8").read()
|
|
match = re.search(r"Timer\s*\{[^}]*running:\s*WindowSwitcherState\.open(.*?)\}", text, re.S)
|
|
if not match:
|
|
raise SystemExit(1)
|
|
interval = re.search(r"interval:\s*(\d+)", match.group(1))
|
|
if not interval:
|
|
raise SystemExit(1)
|
|
value = int(interval.group(1))
|
|
# Long enough that a deliberate pause mid-gesture is not cut short, short
|
|
# enough that a stuck overlay is an annoyance rather than a reason to reboot.
|
|
if not (3000 <= value <= 30000):
|
|
print(f"the switcher timeout is {value}ms", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
PY
|
|
|
|
grep -q 'onTriggered: WindowSwitcherState.cancel()' "$window" \
|
|
|| note 'the switcher timeout does not cancel the switch'
|
|
|
|
# ── 4. The gesture itself is untouched ──────────────────────────────────────
|
|
|
|
grep -q 'WlrKeyboardFocus.None' "$window" \
|
|
|| note 'the switcher takes keyboard focus, which breaks stepping mid-switch'
|
|
|
|
# ── Live ────────────────────────────────────────────────────────────────────
|
|
|
|
# Stepping refuses to open with fewer than two windows (see step() in the
|
|
# state service) -- that is designed behavior, not a failure, so a session
|
|
# with one window skips the live half rather than failing against it.
|
|
open_windows="$(hyprctl -j clients 2>/dev/null | jq length 2>/dev/null || echo 0)"
|
|
if command -v qs >/dev/null 2>&1 && (( open_windows >= 2 )) \
|
|
&& qs ipc call switcher cancel >/dev/null 2>&1; then
|
|
qs ipc call switcher next >/dev/null 2>&1
|
|
sleep 0.6
|
|
mapped="$(hyprctl layers -j 2>/dev/null | grep -c 'qs-switcher' || true)"
|
|
(( mapped > 0 )) || note 'stepping did not map the switcher'
|
|
|
|
qs ipc call switcher commit >/dev/null 2>&1
|
|
sleep 0.6
|
|
mapped="$(hyprctl layers -j 2>/dev/null | grep -c 'qs-switcher' || true)"
|
|
(( mapped == 0 )) || note 'committing did not close the switcher'
|
|
|
|
# Leave nothing behind whatever happened above.
|
|
qs ipc call switcher cancel >/dev/null 2>&1
|
|
fi
|
|
|
|
if (( ${#findings[@]} > 0 )); then
|
|
printf 'switcher contract: %d finding(s)\n' "${#findings[@]}" >&2
|
|
printf ' - %s\n' "${findings[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'switcher contract: PASS\n'
|