Say what a keybind is for, rather than guessing from its name

The Shortcuts page grouped shortcuts by matching substrings in their
descriptions, which put "Close window" and "Close the notification
list" in the same group and left anything phrased unusually in
whichever bucket matched first. The cheatsheet that comes next would
have inherited the same guesswork.

keybinds.lua says it outright now. Its sections already were the
categories, so a section sets one and the binds below inherit it: one
line per section instead of one per bind, and a new bind lands in the
category of the section somebody wrote it in without having to
remember anything.

Hyprland reports a Lua bind's dispatcher as __lua with a bytecode
offset, so nothing can be attached to a bind that survives into
`hyprctl binds`. The config writes a manifest at load instead, keyed
by the chord actually bound so the shell can join on what it sees.
Writing never raises: a read-only state directory costs the grouping,
never the keymap, and the shell keeps the old derivation as its
fallback so a machine that has not reloaded its compositor still works.

The one failure mode is a section that forgets to set a category and
silently inherits the one above. That is not hypothetical -- it
happened while writing this, because the dictation section sits in the
middle of the media binds and its category leaked onto the volume,
media and brightness keys below it. The contract walks the file for
sections with binds and no category, and spot-checks the boundaries
where inheritance is doing the work.
This commit is contained in:
Gabriel Brown
2026-08-21 23:30:36 -04:00
parent 317b7a0962
commit 6ae8265730
4 changed files with 263 additions and 3 deletions
+44 -2
View File
@@ -287,7 +287,41 @@ Singleton {
// Order matters: "Next window splits down" is about splitting rather than
// focus, and "Focus session" is a Panama feature rather than window focus,
// so both are settled before the general checks below them.
// What hypr/keybinds.lua says this bind is for, when it has said anything.
// Written at config load to a manifest keyed by the chord actually bound,
// because Hyprland reports a Lua bind's dispatcher as `__lua` with a
// bytecode offset and nothing can be attached to a bind that survives into
// `hyprctl binds`.
property var categoryManifest: ({})
FileView {
path: (Quickshell.env("XDG_STATE_HOME") || `${Quickshell.env("HOME")}/.local/state`)
+ "/panama/keybind-categories.json"
printErrors: false
watchChanges: true
onFileChanged: this.reload()
onLoaded: {
try {
const parsed = JSON.parse(this.text());
root.categoryManifest = (parsed && typeof parsed === "object") ? parsed : ({});
} catch (error) {
root.categoryManifest = ({});
}
}
// No manifest is the normal state on a machine whose compositor config
// has not been reloaded since this was added. The substring derivation
// below still produces groups, so the keymap page and the cheatsheet
// work; they are just grouped by guesswork until the next reload.
onLoadFailed: root.categoryManifest = ({})
}
function groupFor(description: string, bind: var): string {
// The authored category wins. Keyed by the raw chord, which is what
// the manifest records and what Hyprland reports.
const authored = root.categoryManifest[root.luaChord(bind)];
if (typeof authored === "string" && authored !== "")
return authored;
const text = description.toLowerCase();
if (bind.key && String(bind.key).indexOf("XF86") === 0)
return "Media & hardware keys";
@@ -329,8 +363,16 @@ Singleton {
// Section order for the page. Anything a future bind invents lands at the
// end rather than being dropped.
readonly property var groupOrder: ["Focus", "Move & split", "Size", "Window state",
"Workspaces", "Applications & shell", "Media & hardware keys"]
// The authored categories come first, in the order somebody learning this
// desktop would want them: what you do to a window, then to a workspace,
// then how you start things, then the shell's own surfaces. The names
// after them are the ones the substring derivation produces, kept so a
// machine whose compositor has not reloaded since the manifest was added
// still sorts into a sensible order rather than alphabetically.
readonly property var groupOrder: ["Windows", "Workspaces", "Applications", "Shell",
"Session", "Media & hardware", "Other",
"Focus", "Move & split", "Size", "Window state",
"Applications & shell", "Media & hardware keys"]
// The action already bound to a chord, or "" if it is free. Compared on the
// form keybinds.lua writes rather than the prettified display form, because