aboutsummaryrefslogtreecommitdiffstats
path: root/keybinds
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-25 19:08:15 +0200
committerDanilo M. <danix@danix.xyz>2026-09-25 19:08:15 +0200
commitea063fa984ef775950c840013462a49355576d0b (patch)
tree958d2618693ae83586f932e7b3a131ce68d823ee /keybinds
parent2fb72210ad5d8151718930b87fb7194467e600a4 (diff)
downloadquickshell-ea063fa984ef775950c840013462a49355576d0b.tar.gz
quickshell-ea063fa984ef775950c840013462a49355576d0b.zip
feat(keybinds): group hyprctl binds by description
Binds.js parses hyprctl binds -j output into display groups for the upcoming keybind reminder overlay. Group name and row label come from the "Group: Label" convention in each bind's description; a bind without one still surfaces under "Undescribed" rather than vanishing, so a gap in the Hyprland config stays visible. Pure functions with no QML dependency, so test_binds.js checks them directly with node, stripping the .pragma library line the QML runtime requires but node does not understand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'keybinds')
-rw-r--r--keybinds/Binds.js50
-rw-r--r--keybinds/test_binds.js45
2 files changed, 95 insertions, 0 deletions
diff --git a/keybinds/Binds.js b/keybinds/Binds.js
new file mode 100644
index 0000000..5c0ce2d
--- /dev/null
+++ b/keybinds/Binds.js
@@ -0,0 +1,50 @@
+.pragma library
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// hyprctl binds -j into display groups. Pure, so node can check it:
+// see test_binds.js.
+
+// Modmask bits, in the order they are written in keybindings.lua.
+const MODS = [[64, "SUPER"], [4, "CTRL"], [8, "ALT"], [1, "SHIFT"]];
+
+function mods(mask) {
+ return MODS.filter(m => mask & m[0]).map(m => m[1]);
+}
+
+function keys(bind) {
+ return mods(bind.modmask).concat([bind.key]).join(" + ");
+}
+
+// A description reads "Group: Label". A bind without one still shows, last,
+// so a gap in the config is visible rather than silently missing.
+function group(binds) {
+ const groups = [];
+ const byName = {};
+ const undescribed = [];
+ for (const b of binds) {
+ const d = b.description || "";
+ if (!d) {
+ undescribed.push({ keys: keys(b), label: "no description" });
+ continue;
+ }
+ const i = d.indexOf(": ");
+ const name = i < 0 ? "Other" : d.slice(0, i);
+ const label = i < 0 ? d : d.slice(i + 2);
+ if (!byName[name]) {
+ byName[name] = { name: name, rows: [] };
+ groups.push(byName[name]);
+ }
+ byName[name].rows.push({ keys: keys(b), label: label });
+ }
+ if (undescribed.length) groups.push({ name: "Undescribed", rows: undescribed });
+ return groups;
+}
diff --git a/keybinds/test_binds.js b/keybinds/test_binds.js
new file mode 100644
index 0000000..9be1dea
--- /dev/null
+++ b/keybinds/test_binds.js
@@ -0,0 +1,45 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// Run: node keybinds/test_binds.js
+// Binds.js is a QML library, so its .pragma line is stripped before node
+// evaluates it.
+
+const fs = require("fs");
+const assert = require("assert");
+
+const src = fs.readFileSync(__dirname + "/Binds.js", "utf8")
+ .replace(/^\.pragma library$/m, "");
+const B = new Function(src + "\nreturn { mods, keys, group };")();
+
+assert.deepStrictEqual(B.mods(0), []);
+assert.deepStrictEqual(B.mods(65), ["SUPER", "SHIFT"]);
+assert.deepStrictEqual(B.mods(13), ["CTRL", "ALT", "SHIFT"]);
+assert.strictEqual(B.keys({ modmask: 69, key: "F1" }), "SUPER + CTRL + SHIFT + F1");
+assert.strictEqual(B.keys({ modmask: 0, key: "XF86AudioMute" }), "XF86AudioMute");
+
+assert.deepStrictEqual(B.group([
+ { modmask: 64, key: "s", description: "" },
+ { modmask: 64, key: "w", description: "Apps: Browser" },
+ { modmask: 0, key: "XF86AudioMute", description: "Media: Mute" },
+ { modmask: 64, key: "e", description: "Apps: Files" },
+ { modmask: 8, key: "x", description: "No colon here" },
+]), [
+ { name: "Apps", rows: [
+ { keys: "SUPER + w", label: "Browser" },
+ { keys: "SUPER + e", label: "Files" },
+ ] },
+ { name: "Media", rows: [{ keys: "XF86AudioMute", label: "Mute" }] },
+ { name: "Other", rows: [{ keys: "ALT + x", label: "No colon here" }] },
+ { name: "Undescribed", rows: [{ keys: "SUPER + s", label: "no description" }] },
+]);
+
+console.log("ok");