#!/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'