aboutsummaryrefslogtreecommitdiffstats
path: root/shared
diff options
context:
space:
mode:
Diffstat (limited to 'shared')
-rw-r--r--shared/NotifyConfig.js63
-rw-r--r--shared/test_notify_config.js51
2 files changed, 114 insertions, 0 deletions
diff --git a/shared/NotifyConfig.js b/shared/NotifyConfig.js
new file mode 100644
index 0000000..ff522c4
--- /dev/null
+++ b/shared/NotifyConfig.js
@@ -0,0 +1,63 @@
+// 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.
+
+.pragma library
+
+// ~/.config/notifyd/config.json, resolved over its defaults. notifyd reads
+// the same file for timeouts and caps (its config.go clamps the same way);
+// the balloon keys are this side's alone. Tested by test_notify_config.js.
+
+const corners = ["top-left", "top-right", "bottom-left", "bottom-right"];
+
+function defaults() {
+ return {
+ timeouts: { low: 10, normal: 10, critical: 0 },
+ liveCap: 20,
+ historyCap: 20,
+ balloons: { screen: "DP-1", corner: "bottom-right" },
+ dndPassCritical: true,
+ muted: [],
+ };
+}
+
+// Out of range (negative, or above a day) is the default, as in the daemon.
+function seconds(v, d) { return Number.isInteger(v) && v >= 0 && v <= 86400 ? v : d; }
+function cap(v, d) { return Number.isInteger(v) ? Math.min(Math.max(v, 1), 100) : d; }
+
+// Never throws: anything that is not the right shape reads as its default.
+function resolve(raw) {
+ const d = defaults();
+ if (!raw || typeof raw !== "object") return d;
+ const t = raw.timeouts || {};
+ const b = raw.balloons || {};
+ return {
+ timeouts: {
+ low: seconds(t.low, d.timeouts.low),
+ normal: seconds(t.normal, d.timeouts.normal),
+ critical: seconds(t.critical, d.timeouts.critical),
+ },
+ liveCap: cap(raw.liveCap, d.liveCap),
+ historyCap: cap(raw.historyCap, d.historyCap),
+ balloons: {
+ screen: typeof b.screen === "string" && b.screen !== "" ? b.screen : d.balloons.screen,
+ corner: corners.indexOf(b.corner) >= 0 ? b.corner : d.balloons.corner,
+ },
+ dndPassCritical: typeof raw.dndPassCritical === "boolean" ? raw.dndPassCritical : d.dndPassCritical,
+ muted: Array.isArray(raw.muted) ? raw.muted.filter(a => typeof a === "string") : d.muted,
+ };
+}
+
+// Whether a balloon is held back by mute or DND. Drawer and snooze are the
+// balloon's own checks; this is only what the config decides.
+function withheld(cfg, n, dnd) {
+ if (cfg.muted.indexOf(n.app) >= 0) return true;
+ return dnd && !(n.urgency === "critical" && cfg.dndPassCritical);
+}
diff --git a/shared/test_notify_config.js b/shared/test_notify_config.js
new file mode 100644
index 0000000..b58fbde
--- /dev/null
+++ b/shared/test_notify_config.js
@@ -0,0 +1,51 @@
+// 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 shared/test_notify_config.js
+// NotifyConfig.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 + "/NotifyConfig.js", "utf8")
+ .replace(/^\.pragma library$/m, "");
+const C = new Function(src + "\nreturn { defaults, resolve, withheld };")();
+
+// Nothing, or garbage, is the defaults.
+assert.deepStrictEqual(C.resolve(undefined), C.defaults());
+assert.deepStrictEqual(C.resolve("x"), C.defaults());
+
+// Partial merges; wrong types and bad values keep their defaults.
+const r = C.resolve({ timeouts: { low: 3, normal: "x" }, liveCap: 500, historyCap: 0,
+ balloons: { corner: "middle", screen: "HDMI-A-1" },
+ dndPassCritical: false, muted: ["a", 3, "b"] });
+assert.deepStrictEqual(r.timeouts, { low: 3, normal: 10, critical: 0 });
+assert.strictEqual(r.liveCap, 100);
+assert.strictEqual(r.historyCap, 1);
+assert.deepStrictEqual(r.balloons, { screen: "HDMI-A-1", corner: "bottom-right" });
+assert.strictEqual(r.dndPassCritical, false);
+assert.deepStrictEqual(r.muted, ["a", "b"]);
+
+// Timeouts above a day are out of range, as in the daemon: the default.
+assert.strictEqual(C.resolve({ timeouts: { critical: 100000 } }).timeouts.critical, 0);
+assert.strictEqual(C.resolve({ timeouts: { low: 86400 } }).timeouts.low, 86400);
+
+// Suppression: muted always, DND per dndPassCritical.
+const n = (app, urgency) => ({ app, urgency });
+const cfg = C.resolve({ muted: ["spam"] });
+assert.strictEqual(C.withheld(cfg, n("spam", "critical"), false), true);
+assert.strictEqual(C.withheld(cfg, n("ok", "normal"), false), false);
+assert.strictEqual(C.withheld(cfg, n("ok", "normal"), true), true);
+assert.strictEqual(C.withheld(cfg, n("ok", "critical"), true), false);
+assert.strictEqual(C.withheld(C.resolve({ dndPassCritical: false }), n("ok", "critical"), true), true);
+
+console.log("ok");