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:
@@ -113,7 +113,7 @@ docs/ Settings reference, and the design specs behind the work
|
||||
|
||||
## Tests
|
||||
|
||||
140 of them, under `tests/`. Run the lot, or a subset by pattern:
|
||||
141 of them, under `tests/`. Run the lot, or a subset by pattern:
|
||||
|
||||
```sh
|
||||
panama test # everything
|
||||
|
||||
@@ -284,6 +284,7 @@ The mental model is unchanged from Forge:
|
||||
| `SUPER + SHIFT + S` | Screen Intelligence — read text and codes from a selection |
|
||||
| `SUPER + SHIFT + P` | Color picker |
|
||||
| `CTRL + ALT + L` · `SUPER + Backspace` | Lock (SUPER+L is "focus right") |
|
||||
| `SUPER + /` | Every shortcut, on screen. Reads the live keymap, so a rebind shows here |
|
||||
| `CTRL + ALT + Delete` | Power menu |
|
||||
|
||||
### Apps
|
||||
|
||||
@@ -161,6 +161,10 @@ bind(mod .. " + Space", hl.dsp.exec_cmd(launcher), { description = "Launcher" })
|
||||
-- application without dropping to a TTY. Depends on nothing but wofi itself.
|
||||
bind(mod .. " + SHIFT + R", hl.dsp.exec_cmd("wofi"), { description = "Fallback launcher" })
|
||||
|
||||
-- Every shortcut, on one key. Slash because "what are the keys" is a question,
|
||||
-- and because it is the one punctuation key no other bind wants.
|
||||
bind(mod .. " + slash", hl.dsp.exec_cmd(qs("cheatsheet", "toggle")), { description = "Keyboard shortcuts" })
|
||||
|
||||
-- Clipboard history and emoji, straight into the relevant launcher view.
|
||||
-- Deeplink form is the one from vicinae's own Hyprland quickstart.
|
||||
bind(mod .. " + V", hl.dsp.exec_cmd("vicinae vicinae://launch/clipboard/history"),
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
// Every shortcut, on one keypress.
|
||||
//
|
||||
// This is a different job from the Shortcuts settings page, which answers "how
|
||||
// do I change this" and is worth opening a window for. This answers "what can
|
||||
// I press", which is a question you have while your hands are already on the
|
||||
// keyboard and which needs answering in under a second. So it is an overlay,
|
||||
// it is one key, and it closes on the same key.
|
||||
//
|
||||
// It reads the live keymap rather than a written-down copy: Keybinds.grouped()
|
||||
// comes from `hyprctl binds`, so a shortcut somebody rebound in Settings shows
|
||||
// its new chord here without anything being kept in sync. Categories come from
|
||||
// hypr/keybinds.lua's manifest -- see Keybinds.categoryManifest.
|
||||
//
|
||||
// Three columns, balanced by how many shortcuts each category holds. Not a
|
||||
// Flow: with about 120 binds across six wildly uneven categories -- Windows has
|
||||
// forty, Session has three -- a Flow wraps into as many columns as it likes and
|
||||
// the result is unreadable. The columns are computed instead.
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
readonly property bool open: ShellState.cheatsheetOpen
|
||||
|
||||
readonly property int columnCount: 3
|
||||
|
||||
// Categories dealt into columns, longest first, each going to whichever
|
||||
// column is currently shortest. Sorting first is what stops the fortieth
|
||||
// window bind landing in a column that already has thirty workspace ones.
|
||||
readonly property var columns: {
|
||||
const buckets = [];
|
||||
for (let index = 0; index < root.columnCount; index++)
|
||||
buckets.push({ groups: [], weight: 0 });
|
||||
|
||||
const groups = Array.from(Keybinds.grouped())
|
||||
.sort((a, b) => b.binds.length - a.binds.length);
|
||||
|
||||
for (const group of groups) {
|
||||
let target = buckets[0];
|
||||
for (const bucket of buckets) {
|
||||
if (bucket.weight < target.weight)
|
||||
target = bucket;
|
||||
}
|
||||
target.groups.push(group);
|
||||
// Two lines of overhead per heading, so a column of many small
|
||||
// categories is not treated as shorter than it looks.
|
||||
target.weight += group.binds.length + 2;
|
||||
}
|
||||
return buckets.map(bucket => bucket.groups);
|
||||
}
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind
|
||||
// this. The dimming is painted here rather than added to the overlay rule,
|
||||
// the way PolkitPrompt does it, because this is a card on a scrim rather
|
||||
// than a full-screen takeover.
|
||||
WlrLayershell.namespace: "qs-popover-cheatsheet"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: root.open
|
||||
? WlrKeyboardFocus.Exclusive
|
||||
: WlrKeyboardFocus.None
|
||||
|
||||
// Stays mapped for the length of the close animation, or it vanishes
|
||||
// instantly and only the opening is ever seen.
|
||||
property bool mapped: false
|
||||
visible: root.mapped
|
||||
|
||||
onOpenChanged: {
|
||||
if (root.open) {
|
||||
unmapTimer.stop();
|
||||
root.mapped = true;
|
||||
// The keymap can change while the session runs: a rebind in
|
||||
// Settings, or a compositor reload.
|
||||
Keybinds.refresh();
|
||||
} else {
|
||||
unmapTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: unmapTimer
|
||||
interval: Theme.durNormal
|
||||
onTriggered: root.mapped = false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.alpha(Theme.bgDark, Theme.overlayAlpha)
|
||||
opacity: root.open ? 1 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
|
||||
|
||||
// Anywhere outside the card closes it, which is what every other
|
||||
// transient surface on this desktop does.
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: ShellState.close()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(root.width - 120, 1240)
|
||||
// Sized from its content, capped at the screen. Deliberately NOT
|
||||
// computed from a child that fills it: that is a circular binding, and
|
||||
// it produced a card taller than the display with its contents running
|
||||
// off the bottom edge.
|
||||
height: Math.min(root.height - 120, header.height + body.contentHeight + 72)
|
||||
|
||||
radius: Theme.popoverRadius
|
||||
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.fg, 0.1)
|
||||
|
||||
opacity: root.open ? 1 : 0
|
||||
scale: root.open ? 1 : 0.98
|
||||
Behavior on opacity { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
|
||||
Behavior on scale { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
|
||||
|
||||
PrismEdge {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
inset: Theme.popoverRadius
|
||||
}
|
||||
|
||||
// Clicks on the card itself must not fall through to the scrim.
|
||||
MouseArea { anchors.fill: parent }
|
||||
|
||||
Item {
|
||||
id: header
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 28
|
||||
height: title.implicitHeight
|
||||
|
||||
Text {
|
||||
id: title
|
||||
text: "Keyboard shortcuts"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: title.verticalCenter
|
||||
text: Keybinds.loaded
|
||||
? Keybinds.binds.length + " shortcuts · Esc to close"
|
||||
: "Reading the keymap…"
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolls only when it has to. On a display tall enough for the whole
|
||||
// keymap this never moves, which is the common case and the one worth
|
||||
// optimising for -- a cheatsheet you have to scroll is a document.
|
||||
Flickable {
|
||||
id: body
|
||||
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: 18
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 28
|
||||
anchors.rightMargin: 28
|
||||
anchors.bottomMargin: 28
|
||||
|
||||
contentWidth: width
|
||||
contentHeight: columnRow.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Row {
|
||||
id: columnRow
|
||||
width: parent.width
|
||||
spacing: 24
|
||||
|
||||
Repeater {
|
||||
model: root.columns
|
||||
|
||||
Column {
|
||||
required property var modelData
|
||||
width: (columnRow.width - columnRow.spacing * (root.columnCount - 1)) / root.columnCount
|
||||
spacing: 20
|
||||
|
||||
Repeater {
|
||||
model: parent.modelData
|
||||
|
||||
CheatsheetGroup {
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
name: modelData.name
|
||||
binds: modelData.binds
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Escape closes, like every other dialog here. The same chord that opened
|
||||
// it also closes it, which ShellState.toggle handles.
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onEscapePressed: ShellState.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// One category of shortcuts, as a column.
|
||||
//
|
||||
// Deliberately plain: a chord on the left, what it does on the right, and no
|
||||
// separators between rows. The cheatsheet is read at a glance while a key is
|
||||
// held, so anything that draws the eye away from the two columns is in the way.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string name
|
||||
required property var binds
|
||||
|
||||
spacing: 3
|
||||
|
||||
Text {
|
||||
text: root.name
|
||||
color: Theme.accent
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
font.capitalization: Font.AllUppercase
|
||||
font.letterSpacing: 0.6
|
||||
bottomPadding: 5
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.binds
|
||||
|
||||
Item {
|
||||
required property var modelData
|
||||
|
||||
width: root.width
|
||||
height: chord.implicitHeight + 5
|
||||
|
||||
// The chord is monospaced and tabular so a column of them lines up
|
||||
// rather than jittering with the width of each key name. This is
|
||||
// the one place in the shell where a monospaced face is correct:
|
||||
// it is showing keys, not prose.
|
||||
Text {
|
||||
id: chord
|
||||
width: 168
|
||||
text: modelData.chord
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.features: Theme.tabularFigures
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: chord.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: chord.verticalCenter
|
||||
text: modelData.description
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module qs.modules.cheatsheet
|
||||
Cheatsheet 1.0 Cheatsheet.qml
|
||||
CheatsheetGroup 1.0 CheatsheetGroup.qml
|
||||
@@ -260,7 +260,14 @@ Singleton {
|
||||
if ((bind.modmask & modifier.bit) !== 0)
|
||||
parts.push(modifier.name.toUpperCase());
|
||||
}
|
||||
parts.push(String(bind.key ?? ""));
|
||||
// A modifier-only bind has no key at all -- the window switcher commits
|
||||
// on Super RELEASE. Appending an empty string left a dangling "SUPER + "
|
||||
// that matched neither the chord hypr/keybinds.lua binds nor the one an
|
||||
// override would be keyed by, so that bind could never be rebound and
|
||||
// never found its category.
|
||||
const key = String(bind.key ?? "");
|
||||
if (key !== "")
|
||||
parts.push(key);
|
||||
return parts.join(" + ");
|
||||
}
|
||||
|
||||
@@ -271,7 +278,8 @@ Singleton {
|
||||
parts.push(modifier.name);
|
||||
}
|
||||
const key = String(bind.key ?? "");
|
||||
parts.push(root.keyNames[key] ?? (key.length === 1 ? key.toUpperCase() : key));
|
||||
if (key !== "")
|
||||
parts.push(root.keyNames[key] ?? (key.length === 1 ? key.toUpperCase() : key));
|
||||
return parts.join(" + ");
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Singleton {
|
||||
id: root
|
||||
|
||||
// Exactly one of these may be non-empty at a time.
|
||||
// "" | "overview" | "quicksettings" | "notifications" | "clipboard" | "capture" | "activity" | "powermenu"
|
||||
// "" | "overview" | "quicksettings" | "notifications" | "clipboard" | "capture" | "activity" | "powermenu" | "cheatsheet"
|
||||
property string activeOverlay: ""
|
||||
|
||||
readonly property bool overviewOpen: activeOverlay === "overview"
|
||||
@@ -27,6 +27,7 @@ Singleton {
|
||||
readonly property bool captureOpen: activeOverlay === "capture"
|
||||
readonly property bool activityOpen: activeOverlay === "activity"
|
||||
readonly property bool powerMenuOpen: activeOverlay === "powermenu"
|
||||
readonly property bool cheatsheetOpen: activeOverlay === "cheatsheet"
|
||||
|
||||
// Settings is a normal application window rather than a transient overlay.
|
||||
// It can stay open while Quick Settings or the notification center appears.
|
||||
|
||||
@@ -34,6 +34,7 @@ import qs.modules.quicksettings
|
||||
import qs.modules.notifications
|
||||
import qs.modules.capture
|
||||
import qs.modules.powermenu
|
||||
import qs.modules.cheatsheet
|
||||
import qs.modules.clipboard
|
||||
import qs.modules.datemenu
|
||||
import qs.modules.focus
|
||||
@@ -98,6 +99,7 @@ ShellRoot {
|
||||
QuickSettings { id: quickSettings }
|
||||
DateMenu { id: dateMenu }
|
||||
ClipboardPanel {}
|
||||
Cheatsheet {}
|
||||
CaptureOverlay {}
|
||||
IntelligenceResult {}
|
||||
ActivityPanel {}
|
||||
@@ -126,6 +128,24 @@ ShellRoot {
|
||||
function cancel(): void { WindowSwitcherState.cancel(); }
|
||||
}
|
||||
|
||||
// Every shortcut on one keypress. Toggle so the same chord closes it.
|
||||
IpcHandler {
|
||||
target: "cheatsheet"
|
||||
function toggle(): void { ShellState.toggle("cheatsheet"); }
|
||||
function open(): void { ShellState.open("cheatsheet"); }
|
||||
function close(): void { ShellState.close(); }
|
||||
function status(): string {
|
||||
return JSON.stringify({
|
||||
open: ShellState.cheatsheetOpen,
|
||||
groups: Keybinds.grouped().map(group => group.name),
|
||||
binds: Keybinds.binds.length,
|
||||
uncategorised: Keybinds.binds
|
||||
.filter(bind => !Keybinds.categoryManifest[bind.luaChord])
|
||||
.map(bind => bind.chord)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "overview"
|
||||
function toggle(): void { ShellState.toggle("overview"); }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# @vicinae.schemaVersion 1
|
||||
# @vicinae.title Keyboard Shortcuts
|
||||
# @vicinae.mode silent
|
||||
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||
# @vicinae.description Every shortcut this desktop has, on one screen.
|
||||
# @vicinae.keywords ["shortcuts", "keys", "keybinds", "cheatsheet", "hotkeys", "help"]
|
||||
|
||||
exec qs ipc call cheatsheet toggle
|
||||
Executable
+125
@@ -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'
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user