Answer "what can I press" in one keypress

The Shortcuts settings page answers "how do I change this", which is
worth opening a window for. This answers the other question, the one
you have with your hands already on the keyboard, so it is an overlay
on SUPER + / and the same key closes it.

It reads Keybinds.grouped() rather than a written-down list, so a
shortcut rebound in Settings shows its new chord here with nothing kept
in sync. A cheatsheet that lies is worse than none: it gets consulted
exactly when somebody does not already know.

Three columns, balanced by how many shortcuts each category holds. The
first attempt used a Flow, which wraps into as many columns as it likes
and made 120 binds across six uneven categories unreadable; it also
sized the card from a child that filled it, which is a circular binding
and produced a card taller than the display with its contents running
off the bottom. Both were found by looking at it rather than by a test,
which is the argument for looking at it.

Fixes a real bug on the way past: luaChord and formatChord appended the
key unconditionally, so the window switcher's modifier-only release
bind became "SUPER + " with a dangling separator. That matched neither
the chord keybinds.lua binds nor the one an override is keyed by, so
that bind could never be rebound and had no category -- it was sitting
in a seventh group of its own, which is how it was noticed.
This commit is contained in:
Gabriel Brown
2026-08-21 23:53:50 -04:00
parent 6ae8265730
commit 9fbbdd902b
12 changed files with 469 additions and 5 deletions
+125
View File
@@ -0,0 +1,125 @@
#!/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'
+1 -1
View File
@@ -77,7 +77,7 @@ declare -a standalone=(
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
copy-password
copy-password keyboard-shortcuts
)
# Generated commands must match their source. A stale command dispatches to a