// Copyright (C) 2026 Danilo M. // // 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");