// 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. .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 and `sounds` 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: [], sounds: { low: "", normal: "", critical: "" }, soundTheme: "freedesktop", }; } // 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 || {}; const s = raw.sounds || {}; 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, sounds: { low: typeof s.low === "string" ? s.low : d.sounds.low, normal: typeof s.normal === "string" ? s.normal : d.sounds.normal, critical: typeof s.critical === "string" ? s.critical : d.sounds.critical, }, soundTheme: typeof raw.soundTheme === "string" && raw.soundTheme !== "" ? raw.soundTheme : d.soundTheme, }; } // 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); } // The file a balloon plays, "" for none: the app's suppress-sound wins, then // the app's own sound (notifyd resolved its hints), then the urgency's. function soundFor(cfg, n) { if (n.silent) return ""; if (n.sound) return n.sound; return cfg.sounds[n.urgency] || ""; }