Coherence: one component per idea, one spelling per word, one truth per claim

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-25 02:21:45 -04:00
parent 06c53d6c21
commit bd2fd12de2
87 changed files with 660 additions and 766 deletions
@@ -18,8 +18,10 @@
// knobs come back up with it: they are about gestures, not about the touchpad.
import QtQuick
import Quickshell
import qs.config
import qs.services
import qs.modules.clipboard
SettingsPage {
id: root
@@ -99,7 +101,7 @@ SettingsPage {
ToggleRow { setting: "leftHanded" }
ToggleRow {
setting: "middleClickPaste"
detail: "Paste the primary selection in GTK and native Wayland applications; individual apps may choose not to support it"
detail: "Paste the primary selection in GTK and native Wayland applications; individual applications may choose not to support it"
}
OptionPickerRow {
@@ -170,14 +172,19 @@ SettingsPage {
Keybinds.applyReload();
}
// Nothing, then the shell's own actions, then the compositor's window
// verbs -- the same lists the shortcut editor offers, read from Keybinds so
// this page never becomes a second opinion on what resolves.
// Nothing, then an application, then the shell's own actions, then the
// compositor's window verbs -- the same lists the shortcut editor offers,
// read from Keybinds so this page never becomes a second opinion on what
// resolves.
//
// Applications are deliberately absent: assigning one means searching
// several hundred desktop entries, which a picker of this shape cannot do.
// A gesture that already holds an application (written by the shortcut
// vocabulary elsewhere) still shows, so it can be read and cleared.
// Applications cannot be listed inline: a normal machine has several
// hundred desktop entries and a picker of this shape would be a page nobody
// can read. So the option is a door rather than a list -- picking it opens
// the same search the shortcut editor uses, below the row. A gesture that
// already holds an application shows it too, so it can be read and cleared
// without going back through the search.
readonly property string appPickerValue: "pick-application"
function gestureOptions(key: string): var {
const out = [{ value: "", label: "Nothing", detail: "The swipe passes through to whatever is under it" }];
const stored = DesktopPreferences.get(key);
@@ -187,6 +194,11 @@ SettingsPage {
label: String(stored.label),
detail: "Application · launch-or-focus"
});
out.push({
value: root.appPickerValue,
label: "Launch an application…",
detail: "Search everything installed, the way a custom shortcut does"
});
for (const action of Keybinds.shellActions)
out.push({ value: "shell:" + action.target, label: action.label, detail: "Shell action" });
for (const action of Keybinds.windowActions)
@@ -194,6 +206,49 @@ SettingsPage {
return out;
}
// Which gesture is currently choosing an application, "" while none is.
property string pickingAppFor: ""
// Capped at eight and only ever searched, never browsed -- the same rule
// and the same reason as CustomShortcutEditor's list.
readonly property var appMatches: {
const needle = gestureAppSearch.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;
}
// The one path that writes an application onto a gesture. It builds the
// same three-field record assignGesture() writes for the named actions --
// an entry id is never a command, and hypr/actions.lua resolves it the same
// way it resolves one written by a custom shortcut.
function assignGestureApp(key: string, entry: var): void {
const target = String(entry?.id ?? "");
if (Keybinds.describeAction({ kind: "app", target: target }) === "")
return;
DesktopPreferences.set(key, {
kind: "app",
target: target,
label: "Launch " + String(entry?.name ?? target)
});
root.closeAppPicker();
Keybinds.applyReload();
}
function closeAppPicker(): void {
root.pickingAppFor = "";
gestureAppSearch.text = "";
}
SettingsCard {
visible: InputDevices.hasTouchpad
title: "Gestures"
@@ -236,7 +291,67 @@ SettingsPage {
current: root.gestureValue(String(gestureRow.modelData.key))
enabled: !Keybinds.reloading
divider: gestureRow.index < root.gestureKeys.length - 1
onPicked: value => root.assignGesture(String(gestureRow.modelData.key), String(value))
onPicked: value => {
if (String(value) === root.appPickerValue) {
root.pickingAppFor = String(gestureRow.modelData.key);
gestureAppSearch.text = "";
return;
}
root.closeAppPicker();
root.assignGesture(String(gestureRow.modelData.key), String(value));
}
}
}
// The search, unfolded under the four rows rather than inside one of
// them: the picker has already collapsed by the time this appears, and
// a list of eight results inside a collapsed row has nowhere to go.
Column {
width: parent.width
visible: root.pickingAppFor !== ""
spacing: 0
Item {
width: parent.width
height: 40
SearchField {
id: gestureAppSearch
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
placeholder: "Search installed applications"
}
}
Repeater {
model: root.appMatches
SettingRow {
id: candidate
required property var modelData
required property int index
width: parent.width
label: String(candidate.modelData.name ?? "")
detail: String(candidate.modelData.id ?? "")
controlWidth: 96
divider: candidate.index < root.appMatches.length - 1
activatable: true
onActivated: root.assignGestureApp(root.pickingAppFor, candidate.modelData)
}
}
TextRow {
visible: root.appMatches.length === 0
label: gestureAppSearch.text.trim() === ""
? "Type to find an application"
: "Nothing installed matches that"
detail: "The gesture stores the application, never a command — the same three fields a custom shortcut stores"
divider: false
}
}