summaryrefslogtreecommitdiffstats
path: root/desktop/modules/hyprland/test_hypr.js
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/hyprland/test_hypr.js')
-rw-r--r--desktop/modules/hyprland/test_hypr.js103
1 files changed, 103 insertions, 0 deletions
diff --git a/desktop/modules/hyprland/test_hypr.js b/desktop/modules/hyprland/test_hypr.js
new file mode 100644
index 0000000..8398aa7
--- /dev/null
+++ b/desktop/modules/hyprland/test_hypr.js
@@ -0,0 +1,103 @@
+// 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 desktop/modules/hyprland/test_hypr.js
+// Hypr.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 + "/Hypr.js", "utf8").replace(/^\.pragma library$/m, "");
+const H = new Function(src + "\nreturn { parseCombo, formatCombo, sources, rows, clashes, rebind, "
+ + "parseBatch, show, changed, colors, lua, line, overridden, override, drop };")();
+
+const file = [
+ 'local mainMod = "SUPER"',
+ '-- hl.bind("SUPER + x", hl.dsp.exec_cmd("off"), { description = "Off: Commented" })',
+ 'hl.bind(mainMod .. " + w", hl.dsp.exec_cmd(P.browser), { description = "Apps: Browser" })',
+ 'hl.bind("ALT + l", hl.dsp.exec_cmd([[sh -c \'a, b) hl.bind(\']]), { description = "Apps: QR" })',
+ 'for _, g in ipairs(groups) do',
+ ' hl.bind(g.mod .. " + " .. key,',
+ ' hl.dsp.focus({ workspace = g.wss[i] }),',
+ ' { description = "Workspaces: Go to " .. g.wss[i] })',
+ 'end',
+ 'hl.bind("CTRL + SUPER + z", zoom, { description = "Zoom: Toggle" })',
+].join("\n");
+
+const s = H.sources([{ path: "k.lua", text: file }]);
+assert.strictEqual(s.length, 4, "the commented bind and the one in a long string are skipped");
+assert.deepStrictEqual([s[0].line, s[0].key, s[0].action, s[0].desc, s[0].exact],
+ [3, "SUPER + w", "exec_cmd(P.browser)", "Apps: Browser", true]);
+assert.strictEqual(s[1].key, "ALT + l");
+assert.deepStrictEqual([s[2].line, s[2].key, s[2].desc, s[2].exact], [6, null, "Workspaces: Go to ", false]);
+assert.strictEqual(s[3].action, "zoom");
+
+const bind = (modmask, key, description) => ({ modmask, key, keycode: 0, description, submap: "", release: false, mouse: false });
+const r = H.rows([bind(64, "w", "Apps: Browser"), bind(4, "F3", "Workspaces: Go to 3"),
+ bind(68, "z", ""), bind(64, "W", "Other"), bind(1, "x", "")], s);
+assert.deepStrictEqual(r[0].keys, ["SUPER", "w"]);
+assert.strictEqual(r[0].src.line, 3);
+assert.strictEqual(r[1].src.line, 6, "a loop bind finds its call by description head");
+assert.strictEqual(r[2].src.line, 10, "an undescribed bind finds its call by combo");
+assert.strictEqual(r[4].src, null);
+assert.deepStrictEqual(H.clashes(r).map(g => g.map(x => x.desc)), [["Apps: Browser", "Other"]]);
+
+assert.deepStrictEqual(H.parseCombo("super + Shift+q"), { mask: 65, key: "q" });
+assert.strictEqual(H.parseCombo("SUPRE + q"), null);
+assert.strictEqual(H.parseCombo("SUPER +"), null);
+assert.strictEqual(H.parseCombo("SUPER + SHIFT"), null);
+assert.strictEqual(H.formatCombo("super+shift +q"), "SUPER + SHIFT + q");
+
+const re = H.rebind(file, s[0], "SUPER + SHIFT + b");
+assert.ok(re.includes('hl.bind("SUPER + SHIFT + b", hl.dsp.exec_cmd(P.browser)'));
+assert.strictEqual(re.split("\n").length, file.split("\n").length);
+assert.strictEqual(H.rebind("x" + file, s[0], "SUPER + b"), null, "a moved call is refused");
+
+const batch = '{"option": "general:border_size", "int": 2, "set": true }\n\n\ninvalid type (internal error)\n\n\n'
+ + '{"option": "general:col.active_border", "gradient": "ffc3e88d ff89ddff 45deg", "set": true }\n\n\n'
+ + '{"option": "general:gaps_in", "css": "8 8 8 8", "set": true }\n\n\n'
+ + '{"option": "general:resize_corner", "int": 2, "set": false }\n\n\n'
+ + '{"option": "decoration:rounding_power", "float": 4.000000, "set": true }\n';
+const o = H.parseBatch(batch);
+assert.deepStrictEqual(Object.keys(o).length, 5);
+const corner = { default: 0, map: [{ top_right: 2 }, { disable: 0 }] };
+assert.strictEqual(H.show(corner, o["general:resize_corner"]), "2 · top_right");
+assert.strictEqual(H.show({}, o["decoration:rounding_power"]), "4");
+assert.strictEqual(H.changed({ default: 4 }, o["decoration:rounding_power"]), false);
+assert.strictEqual(H.changed({ default: "5 5 5 5" }, o["general:gaps_in"]), true);
+assert.deepStrictEqual(H.colors(o["general:col.active_border"]), ["#ffc3e88d", "#ff89ddff"]);
+assert.strictEqual(H.lua(o["general:col.active_border"]), '{ colors = { "rgba(c3e88dff)", "rgba(89ddffff)" }, angle = 45 }');
+assert.strictEqual(H.lua(o["general:gaps_in"]), "{ top = 8, right = 8, bottom = 8, left = 8 }");
+assert.strictEqual(H.lua({ str: 'a"b' }), '"a\\"b"');
+
+assert.strictEqual(H.line("general:col.active_border", "x"),
+ "hl.config({ general = { col = { active_border = x } } }) -- keystone: general:col.active_border");
+let t = "-- header\n";
+t = H.override(t, "general:border_size", "4");
+t = H.override(t, "general:gaps_in", "2");
+t = H.override(t, "general:border_size", "3");
+assert.deepStrictEqual(H.overridden(t), ["general:border_size", "general:gaps_in"]);
+assert.ok(t.includes("border_size = 3 }"));
+assert.deepStrictEqual(H.overridden(H.drop(t, "general:border_size")), ["general:gaps_in"]);
+
+// The live config, when it is there: every bind Hyprland reports should
+// find its source.
+const dir = process.env.HOME + "/.config/hypr";
+if (fs.existsSync(dir + "/hyprland.lua")) {
+ const files = ["hyprland.lua"].concat(fs.readdirSync(dir + "/sections").filter(f => f.endsWith(".lua")).map(f => "sections/" + f))
+ .map(p => ({ path: p, text: fs.readFileSync(dir + "/" + p, "utf8") }));
+ const live = JSON.parse(require("child_process").execFileSync("hyprctl", ["binds", "-j"], { encoding: "utf8" }));
+ const lost = H.rows(live, H.sources(files)).filter(x => !x.src);
+ assert.deepStrictEqual(lost.map(x => x.keys.join("+")), [], "live binds without a source");
+}
+
+console.log("ok");