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
@@ -0,0 +1,276 @@
// The two-step flow behind " Add shortcut".
//
// Step one records the chord, because a shortcut whose keys are already taken
// is not worth choosing an action for -- the conflict is reported here, before
// anything is stored. Step two picks what it does, from three fixed
// vocabularies: an installed application, one of the shell's own actions, or
// one of the compositor's window verbs.
//
// Nothing typed here becomes a command. The editor reports an enum kind and a
// target that Keybinds.describeAction() already recognises; hypr/actions.lua
// resolves that to something runnable through whitelist tables of its own. That
// is the whole reason there is no "run this command" option: settings.json has
// to stay a file it is safe to hand somebody.
import QtQuick
import Quickshell
import qs.config
import qs.services
import qs.modules.clipboard
Column {
id: root
// { chord, kind, target, label }
signal committed(string chord, string kind, string target, string label)
signal canceled
property string chord: ""
property string kind: "app"
property string target: ""
property string targetLabel: ""
// The action already holding a chord somebody just pressed, and the chord
// itself, so the refusal can name both.
property string conflict: ""
property string conflictChord: ""
readonly property bool capturing: root.chord === ""
readonly property bool complete: root.chord !== "" && root.target !== "" && root.targetLabel !== ""
function reset(): void {
root.chord = "";
root.kind = "app";
root.target = "";
root.targetLabel = "";
root.conflict = "";
root.conflictChord = "";
appSearch.text = "";
}
width: parent ? parent.width : 620
spacing: 0
SectionLabel {
text: root.capturing ? "New shortcut · press the keys" : "New shortcut · pick what it does"
count: root.chord === "" ? "" : root.chord
}
// ── Step one: the chord ─────────────────────────────────────────────────
Item {
width: parent.width
height: root.capturing ? 56 : 0
visible: root.capturing
clip: true
ShortcutCapture {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 300
height: 32
focus: root.visible && root.capturing
message: root.conflict === ""
? ""
: root.conflictChord + " is already " + root.conflict
onCaptured: chord => {
const taken = Keybinds.boundTo(chord, "");
if (taken !== "") {
root.conflict = taken;
root.conflictChord = chord;
return;
}
if (Keybinds.isCustomChord(chord)) {
root.conflict = "one of your own shortcuts";
root.conflictChord = chord;
return;
}
root.conflict = "";
root.chord = chord;
}
onCanceled: root.canceled()
}
Text {
anchors.left: parent.left
anchors.leftMargin: 312
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: "A modifier is required. Esc cancels. A chord another action holds is reported, never taken."
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
}
}
// ── Step two: the action ────────────────────────────────────────────────
Column {
width: parent.width
visible: !root.capturing
spacing: 0
ChoiceGrid {
width: parent.width
label: "What it does"
detail: "Three kinds, and only three: Panama resolves the name you pick when you press the keys, so no shortcut ever stores a command."
current: root.kind
options: [
{ value: "app", label: "Launch an application" },
{ value: "shell", label: "Shell action" },
{ value: "window", label: "Window & workspace" }
]
onPicked: value => {
root.kind = String(value);
root.target = "";
root.targetLabel = "";
}
}
// Applications are searched rather than listed: a normal machine has
// several hundred desktop entries, and a flow of pills for all of them
// is a page nobody can read.
Column {
width: parent.width
visible: root.kind === "app"
spacing: 0
SearchField {
id: appSearch
width: parent.width
placeholder: "Search installed applications"
}
Repeater {
model: root.appMatches
SettingRow {
id: candidate
required property var modelData
required property int index
label: String(candidate.modelData.name ?? "")
detail: String(candidate.modelData.id ?? "")
value: candidate.modelData.id === root.target ? "Chosen" : ""
controlWidth: 96
divider: candidate.index < root.appMatches.length - 1
activatable: true
onActivated: {
root.target = String(candidate.modelData.id ?? "");
root.targetLabel = "Launch " + String(candidate.modelData.name ?? "");
}
}
}
Text {
width: parent.width
visible: root.appMatches.length === 0
text: appSearch.text.trim() === ""
? "Type to find an application."
: "Nothing installed matches that."
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
topPadding: 10
bottomPadding: 10
}
}
ChoiceGrid {
width: parent.width
visible: root.kind === "shell"
label: "Shell action"
detail: "The shell's own surfaces, each one something Panama already answers over IPC."
current: root.target
options: root.shellOptions
divider: false
onPicked: value => {
root.target = String(value);
root.targetLabel = Keybinds.shellActionLabel(root.target);
}
}
ChoiceGrid {
width: parent.width
visible: root.kind === "window"
label: "Window & workspace"
detail: "Compositor verbs: the first three act on the focused window, the rest go to a workspace."
current: root.target
options: root.windowOptions
divider: false
onPicked: value => {
root.target = String(value);
root.targetLabel = Keybinds.windowActionLabel(root.target);
}
}
Item {
width: parent.width
height: 50
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 8
SettingsButton {
tone: "accent"
text: "Add it"
enabled: root.complete && !Keybinds.reloading
onClicked: root.committed(root.chord, root.kind, root.target, root.targetLabel)
}
SettingsButton {
text: "Cancel"
onClicked: root.canceled()
}
}
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 200
horizontalAlignment: Text.AlignRight
text: root.complete
? root.targetLabel + " · saves, then the compositor reloads — same as rebinding"
: "Pick what the shortcut does."
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
}
}
// ── The vocabularies ────────────────────────────────────────────────────
// Shell and window targets come from Keybinds, which is the single QML
// authority on what hypr/actions.lua will resolve. Listing them here would
// be a second opinion.
readonly property var shellOptions:
Keybinds.shellActions.map(action => ({ value: action.target, label: action.label }))
readonly property var windowOptions:
Keybinds.windowActions.map(action => ({ value: action.target, label: action.label }))
readonly property var appMatches: {
const needle = appSearch.text.trim().toLowerCase();
if (needle === "")
return [];
const out = [];
for (const entry of DesktopEntries.applications.values) {
if (entry.noDisplay)
continue;
if (String(entry.name).toLowerCase().indexOf(needle) >= 0)
out.push(entry);
if (out.length >= 8)
break;
}
return out;
}
}