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
Showing 4 changed files+835-1
▼bin/omarchy
+1-0
| @@ -56,6 +56,7 @@ GROUP_DESCRIPTIONS[hw]="Hardware detection and controls" | ||
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
| @@ -0,0 +1,85 @@ | ||
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,7 +44,7 @@ | ||
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,12 +58,14 @@ Use SUPER+SHIFT+M (Omarchy menu) instead. | ||
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
| @@ -0,0 +1,746 @@ | ||
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 | |