1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
// 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 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] || "";
}
|