Making the plain application keys focus an existing window was the wrong call. It reads well in a demo and it is what macOS does, but it made "give me another terminal" the awkward case -- and on a tiling desktop a second terminal beside the first is the normal way to work, not an edge case. Reaching for the launcher to open a second file manager is not an improvement on anything. So the plain keys do what they always did, and SUPER+ALT is the new capability rather than a tax on the old one: go to the terminal, editor, browser, files, calculator or mail you already have, wherever it is, and start one only if there is none. ALT rather than SHIFT because SUPER+SHIFT is already the window-manipulation space -- Files, Neovim and Settings would have collided with Focus session, Taller and Shorter, and breaking two keys out of the eight-key resize set to make room is the worse trade. Also fixes a real trap found while using it. The Alt-Tab overlay commits on SUPER release, which is a compositor bind running an IPC call; if that call ever fails to land, the overlay stayed up with no keyboard focus, no Escape handler and nothing clickable, so the only way out was an IPC call typed into a terminal it was covering. Clicking outside now dismisses it, clicking a row switches to that window -- which is the obvious thing to try and did nothing -- and an abandoned switch closes itself after ten seconds. Keyboard focus still stays with the compositor, because taking it mid-switch is what would break stepping.
140 lines
6.2 KiB
Bash
Executable File
140 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Going to the window you already have, without giving up opening another.
|
|
#
|
|
# SUPER opens a new one; SUPER+ALT goes to the one that exists. That order was
|
|
# chosen after trying the reverse: making the plain key focus reads well in a
|
|
# demo, but it makes "give me another terminal" the awkward case, and on a
|
|
# tiling desktop a second terminal beside the first is the normal way to work
|
|
# rather than an edge case.
|
|
#
|
|
# The properties worth pinning:
|
|
#
|
|
# 1. A window that is already open is focused, not duplicated.
|
|
# 2. A window that is not open is launched.
|
|
# 3. Patterns are anchored. An unanchored "mail" matches gmail-notifier, and
|
|
# the mail key would raise somebody's notifier.
|
|
# 4. Class alone is not always enough. The terminal and the editor are both
|
|
# kitty on this desktop; only the title tells them apart, and a
|
|
# class-only match would make the editor key raise a terminal.
|
|
# 5. The focus dispatch is the Lua form. On Hyprland 0.56 `hyprctl dispatch
|
|
# focuswindow address:0x...` is parsed as Lua source and fails, which is
|
|
# a failure the window switcher shipped with because its own contract
|
|
# stubbed hyprctl.
|
|
#
|
|
# Driven against a stub hyprctl, so no real window is touched.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
launcher="$repo_dir/bin/panama-launch"
|
|
keybinds="$repo_dir/config/dot/hypr/keybinds.lua"
|
|
pick="$repo_dir/config/dot/quickshell/scripts/panama-pick"
|
|
|
|
findings=()
|
|
note() { findings+=("$1"); }
|
|
|
|
[[ -x "$launcher" ]] || { printf 'launch-or-focus contract: %s is not executable\n' "$launcher" >&2; exit 1; }
|
|
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$work"' EXIT
|
|
calls="$work/calls"
|
|
stub="$work/bin"
|
|
mkdir -p "$stub"
|
|
|
|
# Two windows, both kitty: a terminal and an editor. This is the fixture that
|
|
# matters, because it is the case a class-only match gets wrong.
|
|
cat >"$work/clients.json" <<'JSON'
|
|
[
|
|
{"address":"0xaaa","class":"kitty","title":"gib@desktop:~","mapped":true,"focusHistoryID":1},
|
|
{"address":"0xbbb","class":"kitty","title":"nvim ~/src","mapped":true,"focusHistoryID":0},
|
|
{"address":"0xccc","class":"org.gnome.Nautilus","title":"docs","mapped":true,"focusHistoryID":2},
|
|
{"address":"0xddd","class":"gmail-notifier","title":"Mail","mapped":true,"focusHistoryID":3}
|
|
]
|
|
JSON
|
|
|
|
cat >"$stub/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/hyprctl"
|
|
|
|
run() { : >"$calls"; PATH="$stub:$PATH" "$launcher" "$@" >/dev/null 2>&1; }
|
|
|
|
# ── 1 & 5. An open window is focused, through the Lua dispatch form ─────────
|
|
|
|
run --class '^org\.gnome\.Nautilus$' -- some-file-manager
|
|
grep -q 'hl.dsp.focus' "$calls" \
|
|
|| note 'an already-open window was not focused'
|
|
grep -q 'address:0xccc' "$calls" \
|
|
|| note 'the wrong window was focused'
|
|
grep -q 'dispatch focuswindow' "$calls" \
|
|
&& note 'the legacy dispatch form reached hyprctl; Hyprland 0.56 parses it as Lua and fails'
|
|
|
|
# ── 2. An absent window is launched ─────────────────────────────────────────
|
|
|
|
run --class '^not-running$' -- true
|
|
grep -q 'hl.dsp.focus' "$calls" \
|
|
&& note 'a window that is not open was focused anyway'
|
|
|
|
# ── 3. Anchoring ────────────────────────────────────────────────────────────
|
|
#
|
|
# The fixture includes gmail-notifier for exactly this. An unanchored pattern
|
|
# for the mail client matches it.
|
|
|
|
run --class '^org\.mozilla\.thunderbird' -- some-mail-client
|
|
grep -q 'address:0xddd' "$calls" \
|
|
&& note 'the mail pattern matched gmail-notifier'
|
|
grep -q 'hl.dsp.focus' "$calls" \
|
|
&& note 'a mail client that is not running was treated as running'
|
|
|
|
# Every shipped pattern is anchored at the start.
|
|
while read -r pattern; do
|
|
[[ -n "$pattern" ]] || continue
|
|
[[ "$pattern" == \^* ]] \
|
|
|| note "the launch pattern '$pattern' is not anchored, so it can match an unrelated window"
|
|
done < <(grep -oE "go_to\(\"[^\"]+\"" "$keybinds" | sed 's/go_to("//; s/"$//')
|
|
|
|
# ── 4. Class plus title ─────────────────────────────────────────────────────
|
|
|
|
run --class '^kitty$' --title 'nvim' -- kitty nvim .
|
|
grep -q 'address:0xbbb' "$calls" \
|
|
|| note 'a title-narrowed match focused the wrong kitty window, so the editor key would raise a terminal'
|
|
|
|
run --class '^kitty$' -- kitty
|
|
grep -q 'address:0xaaa' "$calls" \
|
|
|| note 'a class-only match did not focus the most recently used window'
|
|
|
|
# The editor bind must narrow by title, or it is indistinguishable from the
|
|
# terminal bind.
|
|
grep -q 'go_to("\^kitty\$", editor, "nvim")' "$keybinds" \
|
|
|| note 'the go-to-editor bind does not narrow by title, so it would raise whatever terminal is open'
|
|
|
|
# The plain application keys must still open a new window. This is the whole
|
|
# point of the split, and the easiest thing to lose by accident.
|
|
for app in terminal editor browser files calculator mail; do
|
|
grep -qE "bind\(mod \.\. \" \+ [A-Z]\", hl\.dsp\.exec_cmd\($app\)" "$keybinds" \
|
|
|| note "the plain $app key no longer opens a new window"
|
|
done
|
|
|
|
# And every go-to bind is on SUPER+ALT, not somewhere that collides with the
|
|
# window-manipulation space.
|
|
count="$(grep -c 'bind(mod .. " + ALT + ' "$keybinds" || true)"
|
|
(( count >= 6 )) || note "only $count go-to binds are on SUPER+ALT; there should be one per application key"
|
|
|
|
# ── The same dispatch bug must not come back elsewhere ──────────────────────
|
|
|
|
# Comments stripped first: both files explain the legacy form in prose in order
|
|
# to warn against it, and matching that is matching documentation.
|
|
grep -v '^[[:space:]]*#' "$pick" | grep -q 'dispatch focuswindow' \
|
|
&& note 'panama-pick uses the legacy dispatch form, which fails on Hyprland 0.56'
|
|
|
|
if (( ${#findings[@]} > 0 )); then
|
|
printf 'launch-or-focus contract: %d finding(s)\n' "${#findings[@]}" >&2
|
|
printf ' - %s\n' "${findings[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'launch-or-focus contract: PASS\n'
|