Shortcuts you invent, rules you write, gestures you own - all still just data

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-25 01:51:59 -04:00
parent f9e5d3f470
commit 06c53d6c21
48 changed files with 4749 additions and 140 deletions
@@ -106,13 +106,46 @@ SettingsPage {
readonly property bool filtering: root.filter.trim() !== ""
readonly property int overrideCount: Object.keys(Keybinds.overrides).length
// ── The shortcuts you invented ──────────────────────────────────────────
// Rendered from the stored `customBinds` array rather than from the
// compositor's report, for two reasons: the stored entry is the only place
// the ACTION is written down (Hyprland reports every Lua bind as "__lua"
// plus a bytecode offset), and a shortcut has to appear the moment it is
// added rather than after the reload settles. The row asks Keybinds whether
// the compositor is actually answering it, so nothing here claims a bind
// that did not take.
property bool adding: false
// The chord of the custom bind being re-recorded, empty when none is.
property string rebindingChord: ""
readonly property var customBinds: {
const needle = root.filter.trim().toLowerCase();
if (needle === "")
return Keybinds.customBinds;
return Keybinds.customBinds.filter(entry =>
String(entry?.label ?? "").toLowerCase().indexOf(needle) >= 0
|| "custom".indexOf(needle) >= 0);
}
function customMessage(): string {
return root.conflict === ""
? ""
: root.conflictChord + " is already " + root.conflict;
}
// Every group the compositor reports, in Keybinds' own order, narrowed by
// the filter. An empty filter narrows nothing: the page's job is to show
// the whole keymap, and searching is an extra rather than a gate.
//
// "Custom" is dropped here: those binds are drawn above from the stored
// entries, and showing them twice would read as two shortcuts on one chord.
readonly property var groups: {
const needle = root.filter.trim().toLowerCase();
const out = [];
for (const group of Keybinds.grouped()) {
if (group.name === "Custom")
continue;
const hits = needle === ""
? group.binds
: group.binds.filter(bind =>
@@ -264,7 +297,7 @@ SettingsPage {
SettingsCard {
title: "Shortcuts"
subtitle: "Click Change and press the new keys. A shortcut another action holds is refused, never stolen."
subtitle: "Click Change and press the new keys. A shortcut another action holds is refused, never stolen. Your own shortcuts live in the Custom group — none of them stores a command: each names an application, a shell action, or a window move, and Panama resolves the name when you press it."
Item {
width: parent.width
@@ -283,14 +316,107 @@ SettingsPage {
Text {
id: counts
anchors.right: parent.right
anchors.right: addButton.left
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: Keybinds.binds.length + " bound · " + root.overrideCount + " changed"
text: Keybinds.binds.length + " bound · " + root.overrideCount + " changed · "
+ Keybinds.customBinds.length + " custom"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
}
SettingsButton {
id: addButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
tone: "accent"
text: " Add shortcut"
enabled: !root.adding && !Keybinds.reloading
onClicked: {
root.capturingChord = "";
root.rebindingChord = "";
root.conflict = "";
root.conflictChord = "";
addEditor.reset();
root.adding = true;
}
}
}
CustomShortcutEditor {
id: addEditor
width: parent.width
visible: root.adding
onCommitted: (chord, kind, target, label) => {
if (Keybinds.addCustomBind(chord, kind, target, label))
root.adding = false;
}
onCanceled: root.adding = false
}
// The Custom group, pinned above everything the compositor reports.
Column {
width: parent.width
visible: root.customBinds.length > 0
spacing: 0
SectionLabel {
text: "Custom — yours"
count: root.filtering
? "· showing " + root.customBinds.length + " of " + Keybinds.customBinds.length
: "· " + Keybinds.customBinds.length
}
Repeater {
id: customRows
model: root.customBinds
CustomShortcutRow {
id: customRow
required property var modelData
required property int index
entry: customRow.modelData
capturing: root.rebindingChord === String(customRow.modelData.chord ?? "")
message: customRow.capturing ? root.customMessage() : ""
divider: customRow.index < customRows.count - 1
onRebindRequested: {
root.conflict = "";
root.conflictChord = "";
root.capturingChord = "";
root.rebindingChord = String(customRow.modelData.chord ?? "");
}
onRemoveRequested: Keybinds.removeCustomBind(String(customRow.modelData.chord ?? ""))
onCaptured: chord => {
const current = String(customRow.modelData.chord ?? "");
const taken = Keybinds.boundTo(chord, current);
if (taken !== "") {
root.conflict = taken;
root.conflictChord = chord;
return;
}
root.conflict = "";
Keybinds.rebindCustomBind(current, chord);
root.rebindingChord = "";
}
onCanceled: {
root.conflict = "";
root.rebindingChord = "";
}
}
}
}
// One Column of Repeaters rather than a Loader per row: at a hundred and
@@ -363,6 +489,7 @@ SettingsPage {
onChangeRequested: {
root.conflict = "";
root.conflictChord = "";
root.rebindingChord = "";
root.capturingChord = shortcutRow.modelData.luaChord;
}
@@ -395,7 +522,7 @@ SettingsPage {
Text {
width: parent.width
visible: root.groups.length === 0 && Keybinds.loaded
visible: root.groups.length === 0 && root.customBinds.length === 0 && Keybinds.loaded
text: root.filtering
? "Nothing matches — the filter searches shortcut names and group names."
: "The compositor reported no shortcuts."