mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 23:34:01 -04:00
213 lines
12 KiB
JavaScript
213 lines
12 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import vm from "node:vm";
|
|
import { loadScript } from "./qml-script.mjs";
|
|
|
|
const OutputModel = loadScript(new URL("../Common/OutputModel.js", import.meta.url));
|
|
const BlurStrength = loadScript(new URL("../Common/BlurStrength.js", import.meta.url));
|
|
const SPEC = loadScript(new URL("../Common/settings/SettingsSpec.js", import.meta.url)).SPEC;
|
|
const source = readFileSync(new URL("../Services/HyprlandService.qml", import.meta.url), "utf8");
|
|
const extract = name => {
|
|
const found = source.match(new RegExp(`^ function ${name}\\([^)]*\\) \\{\\n[\\s\\S]*?^ \\}`, "m"));
|
|
if (!found)
|
|
throw new Error(`${name} not found in HyprlandService.qml`);
|
|
return found[0];
|
|
};
|
|
|
|
const live = {};
|
|
// Every option is reported as supported unless a test shrinks the set
|
|
const scope = vm.createContext({
|
|
OutputModel,
|
|
BlurStrength,
|
|
SettingsData: { displayNameMode: "system" },
|
|
liveMonitor: name => live[name] ?? null,
|
|
hyprOptionNames: new Proxy({}, { get: (names, key) => key in names ? names[key] : true })
|
|
});
|
|
for (const fn of ["getOutputIdentifier", "luaQuoted", "forceFlagValue", "buildOutputsLua", "luaString", "luaTable", "hyprSupports", "blurLines", "decorationLines", "groupbarLines", "hyprScrollMethod", "touchpadNames", "inputLines", "touchpadDeviceLines", "refreshFollowMouse"])
|
|
vm.runInContext(extract(fn), scope);
|
|
const withOptions = (names, run) => {
|
|
const previous = scope.hyprOptionNames;
|
|
scope.hyprOptionNames = names;
|
|
try {
|
|
return run();
|
|
} finally {
|
|
scope.hyprOptionNames = previous;
|
|
}
|
|
};
|
|
// Arrays from the vm realm fail deepStrictEqual against local ones
|
|
const decorationLines = (s, rounding) => [...scope.decorationLines(s, rounding)];
|
|
const groupbarLines = s => [...scope.groupbarLines(s)];
|
|
const inputLines = s => [...scope.inputLines(s)].map(line => line.trim());
|
|
const deviceLines = (s, names) => [...scope.touchpadDeviceLines(s, names)];
|
|
|
|
const settingsFor = prefix => {
|
|
const defaults = Object.fromEntries(Object.keys(SPEC).filter(key => prefix.test(key)).map(key => [key, SPEC[key].def]));
|
|
return overrides => Object.assign({}, defaults, overrides);
|
|
};
|
|
const layout = settingsFor(/^(hyprland|blurStrength)/);
|
|
const input = settingsFor(/^(mouse|touchpad|keyboard)[A-Z]/);
|
|
const table = (lines, name) => {
|
|
const start = lines.indexOf(`${name} = {`);
|
|
return start < 0 ? null : lines.slice(start + 1, lines.indexOf("},", start)).map(line => line.trim());
|
|
};
|
|
|
|
const fallback = 'hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto" })';
|
|
const saved = OutputModel.parseHyprlandOutputs([
|
|
fallback,
|
|
'hl.monitor({ output = "eDP-1", mode = "[email protected]", position = "0x0", scale = 1.25, vrr = 0 })',
|
|
'hl.monitor({ output = "DP-1", mode = "preferred", position = "auto", scale = "auto" })',
|
|
'hl.monitor({ output = "DP-2", mode = "preferred", position = "auto-right", scale = "auto" })'
|
|
].join("\n"));
|
|
const connected = (name, x, y, scale) => ({ name, logical: { x, y, scale, transform: "Normal" }, modes: [], current_mode: -1 });
|
|
const monitorLine = (content, name) => content.split("\n").find(line => line.includes(`output = "${name}"`));
|
|
|
|
test("outputs: the catch-all line is not a monitor and is rewritten verbatim, once, last", () => {
|
|
assert.deepEqual(Object.keys(saved), ["eDP-1", "DP-1", "DP-2"]);
|
|
const content = scope.buildOutputsLua({ "": { name: "", logical: { x: 0, y: 0, scale: 1 } }, "eDP-1": connected("eDP-1", 0, 0, 1.25) }, {}, saved);
|
|
assert.deepEqual(content.split("\n").filter(line => line.includes('output = ""')), [fallback]);
|
|
assert.equal(content.trimEnd().split("\n").at(-1), fallback);
|
|
});
|
|
|
|
test("outputs: auto position and scale survive a rewrite until the written value differs from what Hyprland resolved", () => {
|
|
live["eDP-1"] = { x: 0, y: 0, scale: 1.25 };
|
|
live["DP-1"] = { x: 1920, y: 0, scale: 1.5 };
|
|
const cases = [
|
|
[connected("DP-1", 1920, 0, 1.5 + 1 / 512), 'position = "auto", scale = "auto"'],
|
|
[connected("DP-1", 2000, 0, 1.5), 'position = "2000x0", scale = "auto"'],
|
|
[connected("DP-1", 1920, 0, 2), 'position = "auto", scale = 2']
|
|
];
|
|
for (const [output, want] of cases) {
|
|
const line = monitorLine(scope.buildOutputsLua({ "eDP-1": connected("eDP-1", 0, 0, 1.25), "DP-1": output }, {}, saved), "DP-1");
|
|
assert.ok(line.includes(want), line);
|
|
}
|
|
const disconnected = monitorLine(scope.buildOutputsLua({ "eDP-1": connected("eDP-1", 0, 0, 1.25), "DP-2": saved["DP-2"] }, {}, saved), "DP-2");
|
|
assert.ok(disconnected.includes('position = "auto-right", scale = "auto"'), disconnected);
|
|
});
|
|
|
|
test("outputs: vrr is written only for a chosen mode, so untouched monitors inherit misc:vrr", () => {
|
|
const vrrFile = OutputModel.parseHyprlandOutputs([
|
|
'hl.monitor({ output = "eDP-1", mode = "[email protected]", position = "0x0", scale = 1.25, vrr = 0 })',
|
|
'hl.monitor({ output = "DP-1", mode = "preferred", position = "2400x0", scale = 1, vrr = 2 })'
|
|
].join("\n"));
|
|
const supported = name => Object.assign(connected(name, 0, 0, 1), { vrr_supported: true, vrr_enabled: false });
|
|
const vrrOf = (name, settings) => monitorLine(scope.buildOutputsLua({ [name]: supported(name) }, settings, vrrFile), name).match(/vrr = (-?\d+)/)?.[1];
|
|
const cases = [
|
|
["eDP-1", {}, undefined],
|
|
["DP-1", {}, "2"],
|
|
["DP-1", { "DP-1": { vrr: -1 } }, undefined],
|
|
["eDP-1", { "eDP-1": { vrr: 0 } }, "0"],
|
|
["eDP-1", { "eDP-1": { vrrFullscreenOnly: true } }, "2"]
|
|
];
|
|
assert.deepEqual(cases.map(([name, settings]) => vrrOf(name, settings)), cases.map(c => c[2]));
|
|
});
|
|
|
|
test("outputs: written scale snaps to Hyprland's n/120 grid so live float32 scales round-trip unchanged", () => {
|
|
live["eDP-1"] = { x: 0, y: 0, scale: 1.25 };
|
|
live["DP-1"] = { x: 1920, y: 0, scale: 1.3333333730697632 };
|
|
const scaleOf = (name, scale) => monitorLine(scope.buildOutputsLua({ [name]: connected(name, name === "DP-1" ? 1920 : 0, 0, scale) }, {}, saved), name).match(/scale = ([^,}\s]+)/)[1];
|
|
assert.deepEqual([scaleOf("eDP-1", 1.3333333730697632), scaleOf("eDP-1", 1.33), scaleOf("DP-1", 1.3333333730697632)], ["1.333333", "1.333333", '"auto"']);
|
|
});
|
|
|
|
test("layout: a blur variant forces blur on and writes only its own tuning table", () => {
|
|
assert.equal(table(decorationLines(layout({ hyprlandBlurVariant: "kawase", blurStrength: 16 }), 0), "blur"), null);
|
|
const frost = table(decorationLines(layout({ hyprlandBlurVariant: "frost", blurStrength: 16 }), 0), "blur");
|
|
assert.deepEqual(frost, ["enabled = true,", 'variant = "frost",']);
|
|
const aurora = table(decorationLines(layout({ hyprlandBlurVariant: "aurora", blurStrength: 16 }), 0), "blur");
|
|
assert.deepEqual(aurora.slice(0, 2), ["enabled = true,", 'variant = "aurora",']);
|
|
assert.deepEqual(table(aurora, "aurora"), ["intensity = 0.35,", "speed = 1,"]);
|
|
assert.equal(table(aurora, "haze"), null);
|
|
});
|
|
|
|
test("layout: blur strength writes Hyprland's size and passes, default reach gives two passes and skips the stock size", () => {
|
|
assert.deepEqual(table(decorationLines(layout(), 0), "blur"), ["passes = 2,"]);
|
|
assert.equal(table(decorationLines(layout({ blurStrength: 16 }), 0), "blur"), null);
|
|
assert.deepEqual(table(decorationLines(layout({ blurStrength: 112, hyprlandBlurVariant: "frost" }), 0), "blur"), ["passes = 3,", "enabled = true,", 'variant = "frost",']);
|
|
});
|
|
|
|
test("layout: effects the running Hyprland does not know are not written even when enabled", () => {
|
|
const enabled = layout({
|
|
blurStrength: 16,
|
|
hyprlandBlurVariant: "aurora",
|
|
hyprlandGlowEnabled: true,
|
|
hyprlandWobbleEnabled: true,
|
|
hyprlandMotionBlurEnabled: true,
|
|
hyprlandGroupbarBlur: true
|
|
});
|
|
withOptions({ "decoration:glow:enabled": true }, () => {
|
|
assert.deepEqual(decorationLines(enabled, 12).filter(line => line.endsWith("= {")), ["glow = {"]);
|
|
assert.deepEqual(groupbarLines(enabled), []);
|
|
});
|
|
});
|
|
|
|
test("input: turning natural scroll off is written so the template's true cannot shadow it", () => {
|
|
assert.ok(table(inputLines(input({ touchpadNaturalScroll: false })), "touchpad").includes("natural_scroll = false,"));
|
|
});
|
|
|
|
test("input: focus follows mouse writes both on and off, leaves unmanaged modes alone, and skips unsupported options", () => {
|
|
const modeLine = mode => inputLines(input({ focusFollowsMouse: mode })).filter(line => line.startsWith("follow_mouse ="));
|
|
assert.deepEqual(modeLine(1), ["follow_mouse = 1,"]);
|
|
assert.deepEqual(modeLine(0), ["follow_mouse = 0,"]);
|
|
assert.deepEqual(modeLine(-1), []);
|
|
withOptions({}, () => {
|
|
assert.deepEqual(modeLine(1), []);
|
|
});
|
|
});
|
|
|
|
test("input: the live focus mode accepts all Hyprland modes and keeps the last value on failed reads", () => {
|
|
scope.CompositorService = { isHyprland: true };
|
|
scope.log = { warn() {} };
|
|
let callback;
|
|
scope.Proc = { runCommand(id, args, cb) { callback = cb; } };
|
|
for (const mode of [0, 1, 2, 3]) {
|
|
scope.refreshFollowMouse();
|
|
callback(JSON.stringify({ int: mode }), 0);
|
|
assert.equal(scope.followMouseMode, mode);
|
|
}
|
|
for (const [output, status] of [["", 1], ["invalid", 0], ['{"int":4}', 0], ['{"int":"1"}', 0]]) {
|
|
scope.refreshFollowMouse();
|
|
callback(output, status);
|
|
assert.equal(scope.followMouseMode, 3);
|
|
}
|
|
scope.CompositorService.isHyprland = false;
|
|
callback('{"int":1}', 0);
|
|
assert.equal(scope.followMouseMode, 3);
|
|
});
|
|
|
|
test("input: keyboard keys stay out until one is configured, then numlock is explicit and kb_file replaces the xkb names", () => {
|
|
const untouched = inputLines(input({ keyboardTrackLayout: "window" }));
|
|
assert.equal(untouched.some(line => /^(kb_|repeat_|numlock)/.test(line)), false);
|
|
const keyboard = inputLines(input({ keyboardLayouts: "us,de", keyboardOptions: "grp:alt_shift_toggle", keyboardRepeatRate: 40 }));
|
|
assert.deepEqual(keyboard.slice(keyboard.lastIndexOf("},") + 1), [
|
|
'kb_layout = "us,de",',
|
|
'kb_options = "grp:alt_shift_toggle",',
|
|
"repeat_rate = 40,",
|
|
"numlock_by_default = false,"
|
|
]);
|
|
const file = inputLines(input({ keyboardKeymapFile: "/k.xkb", keyboardLayouts: "us" }));
|
|
assert.ok(file.includes('kb_file = "/k.xkb",'));
|
|
assert.equal(file.some(line => line.startsWith("kb_layout")), false);
|
|
});
|
|
|
|
test("input: click method maps to clickfinger_behavior and default leaves it out", () => {
|
|
const clickfinger = method => table(inputLines(input({ touchpadClickMethod: method })), "touchpad").find(line => line.startsWith("clickfinger_behavior"));
|
|
assert.equal(clickfinger("clickfinger"), "clickfinger_behavior = true,");
|
|
assert.equal(clickfinger("button-areas"), "clickfinger_behavior = false,");
|
|
assert.equal(clickfinger("default"), undefined);
|
|
});
|
|
|
|
test("input: an option the running Hyprland does not know is never written", () => {
|
|
withOptions({ "input:sensitivity": false, "input:touchpad:natural_scroll": true, "input:touchpad:tap-to-click": false }, () => {
|
|
assert.deepEqual(inputLines(input({ mouseAccelSpeed: 0.5, touchpadDragLock: true })), ["touchpad = {", "natural_scroll = true,", "},"]);
|
|
assert.deepEqual(deviceLines(input({ touchpadAccelSpeed: 0.3 }), ["elan-touchpad"]), []);
|
|
});
|
|
});
|
|
|
|
test("input: touchpad speed goes per device and pins its own values against the global mouse ones", () => {
|
|
assert.deepEqual(deviceLines(input({ touchpadAccelSpeed: 0.3 }), ["elan-touchpad"]), ['hl.device({ name = "elan-touchpad", sensitivity = 0.30 })']);
|
|
assert.deepEqual(deviceLines(input({ mouseAccelSpeed: -0.5, mouseAccelProfile: "flat", mouseLeftHanded: true }), ["a-touchpad"]), [
|
|
'hl.device({ name = "a-touchpad", sensitivity = 0.00, accel_profile = "", left_handed = false })'
|
|
]);
|
|
assert.deepEqual([...scope.touchpadNames({ mice: [{ name: "logitech-mx-master-3" }, { name: "elan0670:00-04f3:3150-touchpad" }, { name: "apple-inc.-magic-trackpad" }] })], ["elan0670:00-04f3:3150-touchpad", "apple-inc.-magic-trackpad"]);
|
|
});
|