Add standalone keybindings cheat-sheet window (SUPER+K), register keybindings group

swadhinbiswas committed 2 months ago
5255918
1 parent eb36c6c
commit 5255918
4 files changed
+835 -1
▼bin/omarchy
+1-0
56
56
GROUP_DESCRIPTIONS[hyprland]="Hyprland window, monitor, and toggle controls"
57
57
GROUP_DESCRIPTIONS[install]="Optional software installers"
58
58
GROUP_DESCRIPTIONS[installed]="Installed optional service checks"
59
+GROUP_DESCRIPTIONS[keybindings]="Keybinding cheat sheet and reference"
59
60
GROUP_DESCRIPTIONS[launch]="Application launchers"
60
61
GROUP_DESCRIPTIONS[menu]="Omarchy menu commands"
61
62
GROUP_DESCRIPTIONS[migrate]="Migration runner"
62
63
▼bin/omarchy-keybindings
+85-0
1
+#!/bin/bash
2
+
3
+# omarchy:summary=Open or toggle the graphical Keybindings page
4
+# omarchy:group=keybindings
5
+# omarchy:args=[--print]
6
+# omarchy:examples=omarchy keybindings | omarchy keybindings --print
7
+# omarchy:alias=omarchy key cheat sheet
8
+
9
+# --print keeps the menu pipeline usable from scripts and TUIs.
10
+if [[ $1 == "--print" || $1 == "-p" ]]; then
11
+ exec "${OMARCHY_PATH:-/usr/share/de-omarchy}/bin/omarchy-menu-keybindings" --print
12
+fi
13
+
14
+LOCKDIR="/tmp/omarchy-keybindings.lock"
15
+if ! mkdir "$LOCKDIR" 2>/dev/null; then
16
+ lock_age=$(( $(date +%s) - $(stat -c %Y "$LOCKDIR" 2>/dev/null || echo 0) ))
17
+ if (( lock_age > 3 )); then
18
+ rm -rf "$LOCKDIR"
19
+ mkdir "$LOCKDIR" 2>/dev/null || exit 0
20
+ else
21
+ exit 0
22
+ fi
23
+fi
24
+trap 'rm -rf "$LOCKDIR"' EXIT
25
+
26
+# The installed runtime is authoritative — never reference the dev checkout
27
+# here; an installed desktop must not depend on (or prefer) a home git clone.
28
+PATH_CANDIDATES=(
29
+ "${OMARCHY_PATH:-/usr/share/de-omarchy}/keybindings"
30
+ "/usr/share/de-omarchy/keybindings"
31
+)
32
+
33
+APP_PATH=""
34
+for p in "${PATH_CANDIDATES[@]}"; do
35
+ if [[ -f "$p/shell.qml" ]]; then
36
+ APP_PATH="$p"
37
+ break
38
+ fi
39
+done
40
+
41
+if [[ -z $APP_PATH ]]; then
42
+ echo "Keybindings QML path not found!" >&2
43
+ exit 1
44
+fi
45
+
46
+# Toggle: close when focused, focus/move otherwise, launch when absent.
47
+existing_window=$(hyprctl clients -j 2>/dev/null | jq -r '.[] | select(.title == "Omarchy Keybindings") | .address' | head -n 1)
48
+active_window=$(hyprctl activewindow -j 2>/dev/null | jq -r '.address // empty')
49
+current_ws=$(hyprctl activeworkspace -j 2>/dev/null | jq -r '.id // "1"')
50
+
51
+if [[ -n "$existing_window" ]]; then
52
+ if [[ "$existing_window" == "$active_window" ]]; then
53
+ hyprctl dispatch "hl.dsp.window.close({ window = \"address:$existing_window\" })" >/dev/null 2>&1 || true
54
+ pkill -f "quickshell.*keybindings" 2>/dev/null || true
55
+ else
56
+ hyprctl dispatch "hl.dsp.window.move({ workspace = \"$current_ws\", window = \"address:$existing_window\" })" >/dev/null 2>&1 || true
57
+ hyprctl dispatch "hl.dsp.focus({ window = \"address:$existing_window\" })" >/dev/null 2>&1 || true
58
+ fi
59
+ exit 0
60
+fi
61
+
62
+# Clean up any stale instances before a fresh launch.
63
+pkill -f "quickshell.*keybindings" 2>/dev/null || true
64
+sleep 0.05
65
+
66
+if command -v uwsm-app >/dev/null 2>&1; then
67
+ uwsm-app -- quickshell -p "$APP_PATH" >/dev/null 2>&1 &
68
+else
69
+ setsid quickshell -p "$APP_PATH" >/dev/null 2>&1 &
70
+fi
71
+
72
+# Bring the new window to the current workspace and focus it.
73
+(
74
+ for (( i = 0; i < 30; i++ )); do
75
+ win_addr=$(hyprctl clients -j 2>/dev/null | jq -r '.[] | select(.title == "Omarchy Keybindings") | .address' | head -n 1)
76
+ if [[ -n "$win_addr" ]]; then
77
+ hyprctl dispatch "hl.dsp.window.move({ workspace = \"$current_ws\", window = \"address:$win_addr\" })" >/dev/null 2>&1 || true
78
+ hyprctl dispatch "hl.dsp.focus({ window = \"address:$win_addr\" })" >/dev/null 2>&1 || true
79
+ break
80
+ fi
81
+ sleep 0.05
82
+ done
83
+) &
84
+
85
+exit 0
0
86
▼keybindings/KEYMAP.md
+3-1
44
44
| F6, SUPER+CTRL+S | Fullshot, region shot |
45
45
| XF86 audio/brightness/kbd | Your scripts |
46
46
| SUPER+SHIFT+P/N/COMMA | Playerctl |
47
-| CTRL+SUPER+SHIFT/ALT+R | Kill/restart shell |
47
+| CTRL+SUPER+ALT+S | Restart Omarchy shell |
48
48
49
49
\* launcher keys call the moonshell IPC target which the bridge stops at
50
50
startup; they become no-ops until you uninstall or re-enable moonshell.
58
58
| SUPER+ALT+SPACE | Apps menu |
59
59
| SUPER+K | Keybindings cheat sheet |
60
60
| SUPER+CTRL+E | Emoji picker |
61
+| SUPER+CTRL+Y | Cycle bar style (menu picker: omarchy bar style menu) |
61
62
| SUPER+CTRL+Q | Calculator (omacalc) |
62
63
| SUPER+CTRL+C | Capture menu |
63
64
| SUPER+CTRL+O | Toggles menu |
64
65
| SUPER+CTRL+H | Hardware menu |
65
66
| SUPER+CTRL+V | Shell clipboard panel |
66
67
| SUPER+comma | Dismiss last notification |
68
+| SUPER+ALT+N | Notification center (bell widget on the bar) |
67
69
| SUPER+ALT+comma | Invoke last notification |
68
70
| SUPER+SHIFT+ALT+comma | Notification history |
69
71
| SUPER+CTRL+comma | Silence notifications toggle |
70
72
▼keybindings/shell.qml
+746-0
1
+import QtQuick
2
+import QtQuick.Controls
3
+import QtQuick.Layouts
4
+import Quickshell
5
+import Quickshell.Io
6
+
7
+ShellRoot {
8
+ id: root
9
+
10
+ // ---------------------------------------------------------------- Visual Design System
11
+ // Matches the Plugin Manager window so the two feel like one suite.
12
+ readonly property color bgBase: "#11121d"
13
+ readonly property color bgSurface: "#161726"
14
+ readonly property color bgCard: "#1d1f33"
15
+ readonly property color bgCardHover: "#252842"
16
+ readonly property color bgElevated: "#2a2e4a"
17
+ readonly property color borderSubtle: "#282b45"
18
+ readonly property color borderActive: "#3d426b"
19
+
20
+ readonly property color fgMain: "#cdd6f4"
21
+ readonly property color fgMuted: "#a6adc8"
22
+ readonly property color fgDim: "#585b70"
23
+
24
+ readonly property color accent: "#89b4fa"
25
+ readonly property color accentGlow: Qt.rgba(137/255, 180/255, 250/255, 0.15)
26
+
27
+ // ---------------------------------------------------------------- Iconography
28
+ //
29
+ // Nerd Font glyph codepoints (the family the bar and terminal render),
30
+ // stored as ints because most live beyond the BMP.
31
+ readonly property string nfFontFamily: "JetBrainsMono Nerd Font"
32
+ function nf(code) { return String.fromCodePoint(code) }
33
+
34
+ readonly property var ic: ({
35
+ keyboard: 0xF097B, // md-keyboard_outline brand / hardware keys
36
+ magnify: 0xF0349, // md-magnify search
37
+ close: 0xF0156, // md-close clear search
38
+ refresh: 0xF0450, // md-refresh reload data
39
+ chevronRight: 0xF0142, // md-chevron_right row affordance
40
+ dotsHorizontal: 0xF01D8, // md-dots_horizontal general category
41
+ rocket: 0xF0463, // md-rocket menus & launchers
42
+ windowOpen: 0xF05B1, // md-window_open windows & tiling
43
+ viewGrid: 0xF0570, // md-view_grid workspaces & monitors
44
+ power: 0xF0425, // md-power system & power
45
+ cameraOutline: 0xF0D5D, // md-camera_outline capture & recording
46
+ volumeHigh: 0xF057E, // md-volume_high audio & media
47
+ brightness: 0xF00DF, // md-brightness_6 display & brightness
48
+ wifi: 0xF05A9, // md-wifi network & devices
49
+ bell: 0xF009A, // md-bell clipboard & notifications
50
+ dockWindow: 0xF10AC, // md-dock_window shell & panels
51
+ alertCircle: 0xF05D6 // md-alert_circle_outline load failure
52
+ })
53
+
54
+ // ---------------------------------------------------------------- Categories
55
+ //
56
+ // Ordered; first pattern hit wins. Action text drives placement so the
57
+ // grouping survives wording changes in the bind sources.
58
+ readonly property var categories: [
59
+ { id: "launchers", label: "Menus & Launchers", icon: root.ic.rocket,
60
+ re: /(menu|launcher|apps? list|picker|actions menu|app launcher)/ },
61
+ { id: "capture", label: "Capture & Recording", icon: root.ic.cameraOutline,
62
+ re: /(screenshot|screenrecording|capture|record|webcam|color picker|ocr|share)/ },
63
+ { id: "audio", label: "Audio & Media", icon: root.ic.volumeHigh,
64
+ re: /(audio|volume|mic mute|playerctl|media|music|cmus|mute mic|play\/pause|track)/ },
65
+ { id: "display", label: "Display & Brightness", icon: root.ic.brightness,
66
+ re: /(brightness|laptop display|display mirroring|^display$|toggle display)/ },
67
+ { id: "network", label: "Network & Devices", icon: root.ic.wifi,
68
+ re: /(wifi|bluetooth|network|tailscale|airplane)/ },
69
+ { id: "clipboard", label: "Clipboard & Notifications", icon: root.ic.bell,
70
+ re: /(clipboard|notification|emoji|reminder|silenc)/ },
71
+ { id: "windows", label: "Windows & Tiling", icon: root.ic.windowOpen,
72
+ re: /(window|tiling|tile|float|fullscreen|maximi|split|swap|pseudo|pop out|pin out|group|resize|expand|shrink|aspect|window width|drag|focus on|cycle)/ },
73
+ { id: "workspaces", label: "Workspaces & Monitors", icon: root.ic.viewGrid,
74
+ re: /(workspace|scratchpad|monitor move|to (left|right|up|down) monitor|next monitor|scroll active)/ },
75
+ { id: "system", label: "System & Power", icon: root.ic.power,
76
+ re: /(lock|logout|reboot|shutdown|power menu|suspend|session|reload|restart|exit|zoom|show time|battery remaining|weather toggle|lid)/ },
77
+ { id: "shell", label: "Shell & Panels", icon: root.ic.dockWindow,
78
+ re: /(panel|top bar|bar$|dashboard|sidebar|cheat|keybind|plugin|theme|wallpaper|background switcher|calculator|activity|agent|utilities|nexus|tmux|herdr)/ }
79
+ ]
80
+
81
+ function categorize(action, keys) {
82
+ var a = String(action || "").toLowerCase()
83
+ for (var i = 0; i < root.categories.length; i++) {
84
+ if (root.categories[i].re.test(a)) return root.categories[i].id
85
+ }
86
+ var k = String(keys || "")
87
+ if (k.indexOf("XF86") === 0 || k.indexOf("switch:") === 0 || k.indexOf("mouse") === 0)
88
+ return "hardware"
89
+ return "general"
90
+ }
91
+
92
+ function categoryMeta(id) {
93
+ if (id === "hardware")
94
+ return { id: id, label: "Hardware Keys", icon: root.ic.keyboard }
95
+ if (id === "general")
96
+ return { id: id, label: "General", icon: root.ic.dotsHorizontal }
97
+ for (var i = 0; i < root.categories.length; i++)
98
+ if (root.categories[i].id === id) return root.categories[i]
99
+ return { id: "general", label: "General", icon: root.ic.dotsHorizontal }
100
+ }
101
+
102
+ // ---------------------------------------------------------------- State
103
+ property var allBindings: []
104
+ property string searchText: ""
105
+ property string activeCategory: "all"
106
+ property bool loading: true
107
+ property bool loadFailed: false
108
+
109
+ // Pretty names for keys the engine leaves symbolic.
110
+ function prettyKey(token) {
111
+ var map = {
112
+ MINUS: "-", EQUAL: "=", COMMA: ",", PERIOD: ".", SLASH: "/",
113
+ GRAVE: "`", PRINT: "PrtSc", RETURN: "Enter", ESCAPE: "Esc",
114
+ BACKSPACE: "Bksp", DELETE: "Del", mouse_up: "Wheel Up",
115
+ mouse_down: "Wheel Down", SUPER_L: "SUPER"
116
+ }
117
+ var t = String(token || "")
118
+ if (map[t] !== undefined) return map[t]
119
+ if (t.indexOf("mouse:") === 0) {
120
+ var b = { "272": "Click L", "273": "Click R", "274": "Click M" }
121
+ return b[t.substring(6)] || t
122
+ }
123
+ if (t.indexOf("XF86") === 0) return t.substring(5).replace(/_/g, " ")
124
+ if (t === "LEFT") return "\u2190"
125
+ if (t === "RIGHT") return "\u2192"
126
+ if (t === "UP") return "\u2191"
127
+ if (t === "DOWN") return "\u2193"
128
+ return t
129
+ }
130
+
131
+ function isModifier(token) {
132
+ return token === "SUPER" || token === "SHIFT" || token === "ALT" || token === "CTRL"
133
+ }
134
+
135
+ // "SUPER SHIFT CTRL + MINUS" → modifier chips + one key chip.
136
+ function keyParts(combo) {
137
+ var groups = String(combo || "").split(" + ")
138
+ var mods = []
139
+ for (var i = 0; i < groups.length - 1; i++) {
140
+ var toks = groups[i].split(" ").filter(function(t) { return t !== "" })
141
+ mods = mods.concat(toks)
142
+ }
143
+ return { mods: mods, key: groups[groups.length - 1] || "" }
144
+ }
145
+
146
+ function applyFilter() {
147
+ var q = root.searchText.toLowerCase().trim()
148
+ var res = []
149
+ for (var i = 0; i < root.allBindings.length; i++) {
150
+ var b = root.allBindings[i]
151
+ if (root.activeCategory !== "all" && b.category !== root.activeCategory) continue
152
+ if (q !== "" && b.action.toLowerCase().indexOf(q) === -1 && b.keys.toLowerCase().indexOf(q) === -1) continue
153
+ res.push(b)
154
+ }
155
+ root.filteredBindings = res
156
+ }
157
+
158
+ property var filteredBindings: []
159
+
160
+ onSearchTextChanged: root.applyFilter()
161
+ onActiveCategoryChanged: root.applyFilter()
162
+
163
+ function categoryCount(id) {
164
+ var n = 0
165
+ for (var i = 0; i < root.allBindings.length; i++)
166
+ if (root.allBindings[i].category === id) n++
167
+ return n
168
+ }
169
+
170
+ Component.onCompleted: root.loadBindings()
171
+
172
+ function loadBindings() {
173
+ root.loading = true
174
+ root.loadFailed = false
175
+ var om = Quickshell.env("OMARCHY_PATH") || "/usr/share/de-omarchy"
176
+ bindsProc.command = ["bash", "-c",
177
+ "omarchy-menu-keybindings --print 2>/dev/null || " + om + "/bin/omarchy-menu-keybindings --print 2>/dev/null"]
178
+ bindsProc.running = true
179
+ }
180
+
181
+ Process {
182
+ id: bindsProc
183
+ running: false
184
+ stdout: StdioCollector {
185
+ waitForEnd: true
186
+ onStreamFinished: {
187
+ var list = []
188
+ var lines = String(text).split("\n")
189
+ for (var i = 0; i < lines.length; i++) {
190
+ var line = lines[i]
191
+ if (!line.trim()) continue
192
+ var tabAt = line.indexOf("\t")
193
+ var head = tabAt === -1 ? line : line.substring(0, tabAt)
194
+ var rest = tabAt === -1 ? "" : line.substring(tabAt + 1)
195
+ var arrowAt = head.indexOf(" \u2192 ")
196
+ if (arrowAt === -1) arrowAt = head.indexOf(" -> ")
197
+ if (arrowAt === -1) continue
198
+ var sepLen = head.charAt(arrowAt + 1) === "\u2192" ? 3 : 4
199
+ var keys = head.substring(0, arrowAt).replace(/\s+/g, " ").trim()
200
+ var action = head.substring(arrowAt + sepLen).trim()
201
+ if (!keys || !action) continue
202
+ var restParts = rest.split("\t")
203
+ list.push({
204
+ keys: keys,
205
+ action: action,
206
+ dispatcher: restParts[0] || "",
207
+ arg: restParts.slice(1).join("\t").trim(),
208
+ category: root.categorize(action, keys.split(" + ").pop())
209
+ })
210
+ }
211
+ root.allBindings = list
212
+ root.loading = false
213
+ root.loadFailed = list.length === 0
214
+ root.applyFilter()
215
+ }
216
+ }
217
+ }
218
+
219
+ // ---------------------------------------------------------------- Main Window
220
+ Window {
221
+ id: win
222
+ title: "Omarchy Keybindings"
223
+ visible: true
224
+ width: 1060
225
+ height: 720
226
+ minimumWidth: 900
227
+ minimumHeight: 600
228
+ color: root.bgBase
229
+
230
+ Keys.onPressed: function(event) {
231
+ if (event.key === Qt.Key_Escape) Qt.quit()
232
+ }
233
+
234
+ Rectangle { anchors.fill: parent; color: root.bgBase }
235
+
236
+ ColumnLayout {
237
+ anchors.fill: parent
238
+ anchors.margins: 20
239
+ spacing: 14
240
+
241
+ // ======================================================= Header
242
+ RowLayout {
243
+ Layout.fillWidth: true
244
+ spacing: 16
245
+
246
+ RowLayout {
247
+ spacing: 12
248
+ Rectangle {
249
+ Layout.preferredWidth: 42
250
+ Layout.preferredHeight: 42
251
+ radius: 12
252
+ color: root.accentGlow
253
+ border.color: Qt.rgba(root.accent.r, root.accent.g, root.accent.b, 0.4)
254
+ border.width: 1
255
+ Text {
256
+ anchors.centerIn: parent
257
+ text: root.nf(root.ic.keyboard)
258
+ color: root.accent
259
+ font.family: root.nfFontFamily
260
+ font.pixelSize: 22
261
+ }
262
+ }
263
+ ColumnLayout {
264
+ spacing: 2
265
+ Text {
266
+ text: "Keybindings"
267
+ color: root.fgMain
268
+ font.pixelSize: 18
269
+ font.bold: true
270
+ }
271
+ Text {
272
+ text: "Every active Hyprland binding on this system"
273
+ color: root.fgMuted
274
+ font.pixelSize: 11
275
+ }
276
+ }
277
+ }
278
+
279
+ Item { Layout.fillWidth: true }
280
+
281
+ // Search box
282
+ Rectangle {
283
+ Layout.preferredWidth: 340
284
+ Layout.preferredHeight: 38
285
+ radius: 19
286
+ color: root.bgSurface
287
+ border.color: searchInput.activeFocus ? root.accent : root.borderSubtle
288
+ border.width: 1
289
+
290
+ RowLayout {
291
+ anchors.fill: parent
292
+ anchors.leftMargin: 14
293
+ anchors.rightMargin: 12
294
+ spacing: 8
295
+
296
+ Text {
297
+ text: root.nf(root.ic.magnify)
298
+ color: root.fgDim
299
+ font.family: root.nfFontFamily
300
+ font.pixelSize: 14
301
+ }
302
+
303
+ TextInput {
304
+ id: searchInput
305
+ Layout.fillWidth: true
306
+ text: root.searchText
307
+ color: root.fgMain
308
+ font.pixelSize: 13
309
+ selectByMouse: true
310
+ focus: true
311
+ onTextChanged: root.searchText = text
312
+ Keys.onPressed: function(event) {
313
+ if (event.key === Qt.Key_Escape) {
314
+ if (root.searchText !== "") root.searchText = ""
315
+ else Qt.quit()
316
+ event.accepted = true
317
+ }
318
+ }
319
+
320
+ Text {
321
+ text: "Search actions or keys... / to focus"
322
+ color: root.fgDim
323
+ font.pixelSize: 13
324
+ visible: !searchInput.text
325
+ }
326
+ }
327
+
328
+ Text {
329
+ visible: searchInput.text.length > 0
330
+ text: root.nf(root.ic.close)
331
+ color: root.fgDim
332
+ font.family: root.nfFontFamily
333
+ font.pixelSize: 13
334
+
335
+ MouseArea {
336
+ anchors.fill: parent
337
+ anchors.margins: -6
338
+ cursorShape: Qt.PointingHandCursor
339
+ onClicked: root.searchText = ""
340
+ }
341
+ }
342
+ }
343
+ }
344
+
345
+ // Refresh
346
+ Rectangle {
347
+ Layout.preferredWidth: 38
348
+ Layout.preferredHeight: 38
349
+ radius: 10
350
+ color: refreshArea.pressed ? root.bgElevated : root.bgSurface
351
+ border.color: root.borderSubtle
352
+ border.width: 1
353
+
354
+ Text {
355
+ anchors.centerIn: parent
356
+ text: root.nf(root.ic.refresh)
357
+ color: root.fgMuted
358
+ font.family: root.nfFontFamily
359
+ font.pixelSize: 15
360
+ }
361
+
362
+ MouseArea {
363
+ id: refreshArea
364
+ anchors.fill: parent
365
+ cursorShape: Qt.PointingHandCursor
366
+ onClicked: root.loadBindings()
367
+ }
368
+ }
369
+ }
370
+
371
+ // ======================================================= Body
372
+ RowLayout {
373
+ Layout.fillWidth: true
374
+ Layout.fillHeight: true
375
+ spacing: 14
376
+
377
+ // -------- Sidebar
378
+ Rectangle {
379
+ Layout.preferredWidth: 232
380
+ Layout.fillHeight: true
381
+ radius: 14
382
+ color: root.bgSurface
383
+ border.color: root.borderSubtle
384
+ border.width: 1
385
+
386
+ ColumnLayout {
387
+ anchors.fill: parent
388
+ anchors.margins: 8
389
+ spacing: 2
390
+
391
+ // All
392
+ Rectangle {
393
+ id: allChip
394
+ Layout.fillWidth: true
395
+ Layout.preferredHeight: 34
396
+ radius: 9
397
+ color: root.activeCategory === "all" ? root.accentGlow : "transparent"
398
+ border.color: root.activeCategory === "all" ? root.accent : "transparent"
399
+ border.width: 1
400
+
401
+ RowLayout {
402
+ anchors.fill: parent
403
+ anchors.leftMargin: 12
404
+ anchors.rightMargin: 12
405
+ spacing: 10
406
+
407
+ Text {
408
+ text: root.nf(root.ic.keyboard)
409
+ color: root.activeCategory === "all" ? root.accent : root.fgDim
410
+ font.family: root.nfFontFamily
411
+ font.pixelSize: 14
412
+ }
413
+ Text {
414
+ Layout.fillWidth: true
415
+ text: "All bindings"
416
+ color: root.activeCategory === "all" ? root.fgMain : root.fgMuted
417
+ font.pixelSize: 12
418
+ font.bold: root.activeCategory === "all"
419
+ }
420
+ Rectangle {
421
+ Layout.preferredHeight: 18
422
+ Layout.preferredWidth: allCount.implicitWidth + 10
423
+ radius: 9
424
+ color: root.activeCategory === "all" ? root.accent : root.bgElevated
425
+ Text {
426
+ id: allCount
427
+ anchors.centerIn: parent
428
+ text: root.allBindings.length
429
+ color: root.activeCategory === "all" ? "#11121d" : root.fgMuted
430
+ font.pixelSize: 10
431
+ font.bold: true
432
+ }
433
+ }
434
+ }
435
+
436
+ MouseArea {
437
+ anchors.fill: parent
438
+ cursorShape: Qt.PointingHandCursor
439
+ onClicked: root.activeCategory = "all"
440
+ }
441
+ }
442
+
443
+ Rectangle {
444
+ Layout.fillWidth: true
445
+ Layout.preferredHeight: 1
446
+ Layout.topMargin: 4
447
+ Layout.bottomMargin: 4
448
+ color: root.borderSubtle
449
+ }
450
+
451
+ // Category list
452
+ ListView {
453
+ id: catList
454
+ Layout.fillWidth: true
455
+ Layout.fillHeight: true
456
+ clip: true
457
+ spacing: 2
458
+ model: {
459
+ var ids = []
460
+ for (var i = 0; i < root.categories.length; i++) ids.push(root.categories[i].id)
461
+ ids.push("hardware")
462
+ ids.push("general")
463
+ var out = []
464
+ for (var j = 0; j < ids.length; j++) {
465
+ if (root.categoryCount(ids[j]) > 0) out.push(ids[j])
466
+ }
467
+ return out
468
+ }
469
+ delegate: Rectangle {
470
+ required property var modelData
471
+ readonly property var meta: root.categoryMeta(modelData)
472
+ readonly property bool selected: root.activeCategory === modelData
473
+ width: catList.width
474
+ height: 32
475
+ radius: 9
476
+ color: selected ? root.accentGlow : (catHover.hovered ? root.bgCard : "transparent")
477
+ border.color: selected ? root.accent : "transparent"
478
+ border.width: 1
479
+
480
+ RowLayout {
481
+ anchors.fill: parent
482
+ anchors.leftMargin: 12
483
+ anchors.rightMargin: 12
484
+ spacing: 10
485
+
486
+ Text {
487
+ text: root.nf(meta.icon)
488
+ color: selected ? root.accent : root.fgDim
489
+ font.family: root.nfFontFamily
490
+ font.pixelSize: 13
491
+ }
492
+ Text {
493
+ Layout.fillWidth: true
494
+ text: meta.label
495
+ elide: Text.ElideRight
496
+ color: selected ? root.fgMain : root.fgMuted
497
+ font.pixelSize: 12
498
+ font.bold: selected
499
+ }
500
+ Text {
501
+ text: root.categoryCount(modelData)
502
+ color: selected ? root.accent : root.fgDim
503
+ font.pixelSize: 10
504
+ font.bold: selected
505
+ }
506
+ }
507
+
508
+ HoverHandler { id: catHover }
509
+
510
+ MouseArea {
511
+ anchors.fill: parent
512
+ cursorShape: Qt.PointingHandCursor
513
+ onClicked: root.activeCategory = modelData
514
+ }
515
+ }
516
+ }
517
+ }
518
+ }
519
+
520
+ // -------- Binding list
521
+ ColumnLayout {
522
+ Layout.fillWidth: true
523
+ Layout.fillHeight: true
524
+ spacing: 8
525
+
526
+ Rectangle {
527
+ Layout.fillWidth: true
528
+ Layout.preferredHeight: 30
529
+ color: "transparent"
530
+
531
+ RowLayout {
532
+ anchors.fill: parent
533
+ anchors.leftMargin: 6
534
+ anchors.rightMargin: 6
535
+
536
+ Text {
537
+ text: root.activeCategory === "all"
538
+ ? "All bindings"
539
+ : root.categoryMeta(root.activeCategory).label
540
+ color: root.fgMain
541
+ font.pixelSize: 13
542
+ font.bold: true
543
+ }
544
+ Item { Layout.fillWidth: true }
545
+ Text {
546
+ text: root.filteredBindings.length + " shown"
547
+ color: root.fgDim
548
+ font.pixelSize: 11
549
+ }
550
+ }
551
+ }
552
+
553
+ // Loading / empty states
554
+ Item {
555
+ Layout.fillWidth: true
556
+ Layout.fillHeight: true
557
+ visible: root.loading || root.loadFailed || root.filteredBindings.length === 0
558
+
559
+ ColumnLayout {
560
+ anchors.centerIn: parent
561
+ spacing: 12
562
+
563
+ Text {
564
+ Layout.alignment: Qt.AlignHCenter
565
+ text: root.loading
566
+ ? root.nf(root.ic.refresh)
567
+ : root.nf(root.alertCircle)
568
+ color: root.fgDim
569
+ font.family: root.nfFontFamily
570
+ font.pixelSize: 30
571
+ }
572
+ Text {
573
+ Layout.alignment: Qt.AlignHCenter
574
+ text: root.loading
575
+ ? "Reading bindings from Hyprland..."
576
+ : (root.loadFailed
577
+ ? "Could not read bindings.\nIs hyprctl available and is the shell inside a Hyprland session?"
578
+ : "No bindings match your search.")
579
+ color: root.fgMuted
580
+ font.pixelSize: 13
581
+ horizontalAlignment: Text.AlignHCenter
582
+ }
583
+ }
584
+ }
585
+
586
+ ListView {
587
+ id: bindList
588
+ Layout.fillWidth: true
589
+ Layout.fillHeight: true
590
+ clip: true
591
+ spacing: 6
592
+ visible: !root.loading && !root.loadFailed
593
+ model: root.filteredBindings
594
+
595
+ ScrollBar.vertical: ScrollBar {
596
+ policy: ScrollBar.AsNeeded
597
+ }
598
+
599
+ delegate: Rectangle {
600
+ required property var modelData
601
+ readonly property var parts: root.keyParts(modelData.keys)
602
+
603
+ width: bindList.width
604
+ implicitHeight: rowCol.implicitHeight + 24
605
+ height: implicitHeight
606
+ radius: 12
607
+ color: rowHover.hovered ? root.bgCardHover : root.bgCard
608
+ border.color: rowHover.hovered ? root.borderActive : root.borderSubtle
609
+ border.width: 1
610
+
611
+ RowLayout {
612
+ id: rowCol
613
+ anchors.left: parent.left
614
+ anchors.right: parent.right
615
+ anchors.top: parent.top
616
+ anchors.margins: 12
617
+ spacing: 14
618
+
619
+ // Keycaps — capped so long modifier chains never squeeze the
620
+ // action column.
621
+ RowLayout {
622
+ Layout.preferredWidth: 286
623
+ Layout.maximumWidth: 286
624
+ Layout.alignment: Qt.AlignVCenter
625
+ spacing: 4
626
+
627
+ Repeater {
628
+ model: parts.mods
629
+ delegate: RowLayout {
630
+ required property var modelData
631
+ spacing: 4
632
+ Rectangle {
633
+ Layout.preferredHeight: 24
634
+ Layout.preferredWidth: modCap.implicitWidth + 14
635
+ radius: 6
636
+ color: root.accentGlow
637
+ border.color: Qt.rgba(root.accent.r, root.accent.g, root.accent.b, 0.45)
638
+ border.width: 1
639
+ Text {
640
+ id: modCap
641
+ anchors.centerIn: parent
642
+ text: modelData
643
+ color: root.accent
644
+ font.family: root.nfFontFamily
645
+ font.pixelSize: 10
646
+ font.bold: true
647
+ }
648
+ }
649
+ Text {
650
+ visible: index === parts.mods.length - 1
651
+ text: "+"
652
+ color: root.fgDim
653
+ font.pixelSize: 11
654
+ }
655
+ }
656
+ }
657
+
658
+ Rectangle {
659
+ Layout.preferredHeight: 26
660
+ Layout.preferredWidth: keyCap.implicitWidth + 16
661
+ radius: 7
662
+ color: root.bgElevated
663
+ border.color: root.borderActive
664
+ border.width: 1
665
+ Text {
666
+ id: keyCap
667
+ anchors.centerIn: parent
668
+ text: root.prettyKey(parts.key)
669
+ color: root.fgMain
670
+ font.family: root.nfFontFamily
671
+ font.pixelSize: 11
672
+ font.bold: true
673
+ }
674
+ }
675
+
676
+ Item { Layout.fillWidth: true }
677
+ }
678
+
679
+ // Action
680
+ ColumnLayout {
681
+ Layout.fillWidth: true
682
+ Layout.minimumWidth: 180
683
+ Layout.alignment: Qt.AlignVCenter
684
+ spacing: 2
685
+
686
+ Text {
687
+ Layout.fillWidth: true
688
+ text: modelData.action
689
+ color: root.fgMain
690
+ font.pixelSize: 13
691
+ elide: Text.ElideRight
692
+ maximumLineCount: 1
693
+ }
694
+ Text {
695
+ Layout.fillWidth: true
696
+ visible: modelData.dispatcher === "exec" && modelData.arg !== ""
697
+ text: modelData.arg
698
+ color: root.fgDim
699
+ font.family: root.nfFontFamily
700
+ font.pixelSize: 10
701
+ elide: Text.ElideMiddle
702
+ }
703
+ }
704
+
705
+ Text {
706
+ text: root.nf(root.ic.chevronRight)
707
+ color: root.fgDim
708
+ font.family: root.nfFontFamily
709
+ font.pixelSize: 13
710
+ opacity: rowHover.hovered ? 1 : 0.35
711
+ }
712
+ }
713
+
714
+ HoverHandler { id: rowHover }
715
+ }
716
+ }
717
+ }
718
+ }
719
+
720
+ // ======================================================= Footer
721
+ Rectangle {
722
+ Layout.fillWidth: true
723
+ Layout.preferredHeight: 28
724
+ color: "transparent"
725
+
726
+ RowLayout {
727
+ anchors.fill: parent
728
+ anchors.leftMargin: 6
729
+ anchors.rightMargin: 6
730
+
731
+ Text {
732
+ text: root.allBindings.length + " bindings \u00B7 grouped by action"
733
+ color: root.fgDim
734
+ font.pixelSize: 11
735
+ }
736
+ Item { Layout.fillWidth: true }
737
+ Text {
738
+ text: "/ search \u00B7 Esc close"
739
+ color: root.fgDim
740
+ font.pixelSize: 11
741
+ }
742
+ }
743
+ }
744
+ }
745
+ }
746
+}
0
747