#!/usr/bin/env bash # The cheatsheet: every shortcut, on one keypress. # # It answers a different question from the Shortcuts settings page. That page # is "how do I change this" and is worth opening a window for; this is "what # can I press", asked with your hands already on the keyboard. So the # properties that matter are that it is one key away, that it closes on the # same key, and above all that it cannot drift from the real keymap. # # That last one is why it reads Keybinds.grouped() rather than a written-down # list: a shortcut rebound in Settings has to show its new chord here without # anything being kept in sync. A cheatsheet that lies is worse than none, # because it is consulted precisely when somebody does not already know. # # The live half opens the real surface and checks it maps. The layout is not # asserted -- how many columns look right is a judgement, not a contract. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" module="$repo_dir/config/dot/quickshell/modules/cheatsheet" shell_qml="$repo_dir/config/dot/quickshell/shell.qml" state="$repo_dir/config/dot/quickshell/services/ShellState.qml" keybinds="$repo_dir/config/dot/hypr/keybinds.lua" rules="$repo_dir/config/dot/hypr/rules.lua" command_file="$repo_dir/config/local/share/vicinae/scripts/keyboard-shortcuts" findings=() note() { findings+=("$1"); } # ── The module ─────────────────────────────────────────────────────────────── for file in Cheatsheet.qml CheatsheetGroup.qml qmldir; do [[ -r "$module/$file" ]] || note "the cheatsheet module has no $file" done grep -q '^Cheatsheet 1.0 Cheatsheet.qml$' "$module/qmldir" \ || note 'Cheatsheet is not registered in its qmldir, so the shell would fail to load entirely' grep -q '^CheatsheetGroup 1.0 CheatsheetGroup.qml$' "$module/qmldir" \ || note 'CheatsheetGroup is not registered in its qmldir' # ── It reads the live keymap ───────────────────────────────────────────────── grep -q 'Keybinds.grouped()' "$module/Cheatsheet.qml" \ || note 'the cheatsheet does not read the live keymap, so it can drift from what the keys actually do' grep -q 'Keybinds.refresh()' "$module/Cheatsheet.qml" \ || note 'the cheatsheet does not re-read the keymap when opened, so a rebind would not show until the shell restarted' # ── Wiring ─────────────────────────────────────────────────────────────────── grep -q 'import qs.modules.cheatsheet' "$shell_qml" \ || note 'shell.qml does not import the cheatsheet module' grep -qE '^\s*Cheatsheet \{\}' "$shell_qml" \ || note 'shell.qml never instantiates the cheatsheet' grep -q 'target: "cheatsheet"' "$shell_qml" \ || note 'there is no cheatsheet IPC target' grep -q 'cheatsheetOpen: activeOverlay === "cheatsheet"' "$state" \ || note 'ShellState does not track the cheatsheet, so it would not close when another overlay opens' grep -q '"cheatsheet"' <(grep 'Exactly one of these' -A2 "$state") \ || note 'the ShellState overlay comment does not list the cheatsheet' # The namespace has to match a layer rule or the surface gets no blur. The # popover rule matches by prefix, which is why this name was chosen. grep -q 'qs-popover-cheatsheet' "$module/Cheatsheet.qml" \ || note 'the cheatsheet does not use a namespace beginning qs-popover, so it would be drawn without blur' grep -q "namespace = \"^qs-popover\"" "$rules" \ || note 'the qs-popover layer rule no longer matches by prefix, so the cheatsheet lost its blur' # ── One key, and the same key closes it ────────────────────────────────────── grep -q 'qs("cheatsheet", "toggle")' "$keybinds" \ || note 'no keybind opens the cheatsheet, or it does not toggle so the same key cannot close it' grep -qE 'bind\(mod \.\. " \+ slash".*description' "$keybinds" \ || note 'the cheatsheet bind carries no description, which drops it from the keymap it is meant to show' [[ -x "$command_file" ]] || note 'there is no launcher command for the cheatsheet' # ── Live ───────────────────────────────────────────────────────────────────── if ! command -v qs >/dev/null 2>&1 || ! qs ipc call cheatsheet status >/dev/null 2>&1; then if (( ${#findings[@]} > 0 )); then printf 'cheatsheet contract: %d finding(s)\n' "${#findings[@]}" >&2 printf ' - %s\n' "${findings[@]}" >&2 exit 1 fi printf 'cheatsheet contract: PASS (static; no running shell)\n' exit 0 fi was_open="$(qs ipc call cheatsheet status | jq -r .open)" qs ipc call cheatsheet open >/dev/null sleep 1 [[ "$(qs ipc call cheatsheet status | jq -r .open)" == "true" ]] \ || note 'the cheatsheet did not open' hyprctl layers -j | grep -q 'qs-popover-cheatsheet' \ || note 'the cheatsheet reports open but its layer never mapped, so nothing is on screen' # Toggling closes it, which is what makes the opening chord also the closing one. qs ipc call cheatsheet toggle >/dev/null sleep 1 [[ "$(qs ipc call cheatsheet status | jq -r .open)" == "false" ]] \ || note 'toggling an open cheatsheet did not close it' status="$(qs ipc call cheatsheet status)" groups="$(jq -r '.groups | length' <<<"$status")" (( groups >= 4 )) || note "the cheatsheet shows $groups categories; the keymap has six" (( groups <= 8 )) || note "the cheatsheet shows $groups categories, which suggests grouping fell back to guesswork" # Every bind is categorised by hypr/keybinds.lua. One that is not falls back to # substring derivation and lands in a group of its own, which is how the # modifier-only switcher bind was found sitting in a seventh category by itself. uncategorised="$(jq -r '.uncategorised | join(", ")' <<<"$status")" [[ -z "$uncategorised" ]] \ || note "these binds carry no authored category: $uncategorised" [[ "$was_open" == "true" ]] && qs ipc call cheatsheet open >/dev/null if (( ${#findings[@]} > 0 )); then printf 'cheatsheet contract: %d finding(s)\n' "${#findings[@]}" >&2 printf ' - %s\n' "${findings[@]}" >&2 exit 1 fi printf 'cheatsheet contract: PASS\n'