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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user