diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-19 09:18:49 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-19 09:18:49 +0200 |
| commit | a6e3e42d0687dbc4a5b939faa3c2ca355e690e2e (patch) | |
| tree | 50765e6b0b8ca5a70cde48d219702066596941cd | |
| parent | 61ac7f2e62375c0644ffea30c8d93b81c2f29b96 (diff) | |
| download | quickshell-a6e3e42d0687dbc4a5b939faa3c2ca355e690e2e.tar.gz quickshell-a6e3e42d0687dbc4a5b939faa3c2ca355e690e2e.zip | |
notifications: resolve notifyd's config.json in a shared library
One resolver for both shells and the settings page, with the defaults that
reproduce today's behaviour, and the mute/DND decision beside it so the
node check covers it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| l--------- | desktop/NotifyConfig.js | 1 | ||||
| l--------- | notifications/NotifyConfig.js | 1 | ||||
| -rw-r--r-- | shared/NotifyConfig.js | 63 | ||||
| -rw-r--r-- | shared/test_notify_config.js | 51 |
4 files changed, 116 insertions, 0 deletions
diff --git a/desktop/NotifyConfig.js b/desktop/NotifyConfig.js new file mode 120000 index 0000000..e98c214 --- /dev/null +++ b/desktop/NotifyConfig.js @@ -0,0 +1 @@ +../shared/NotifyConfig.js
\ No newline at end of file diff --git a/notifications/NotifyConfig.js b/notifications/NotifyConfig.js new file mode 120000 index 0000000..e98c214 --- /dev/null +++ b/notifications/NotifyConfig.js @@ -0,0 +1 @@ +../shared/NotifyConfig.js
\ No newline at end of file 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"); |
