1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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");
|