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
81
82
83
84
|
// 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.
import Quickshell.Io
import QtQuick
import "../.."
// notifyd's config.json: the daemon's timeouts and caps, and the balloon
// shell's placement, DND rule and muted apps. The draft lives here, not in
// the page, so the sidebar's unsaved mark survives the page unloading.
Module {
id: nm
name: "notifications"
label: "Notifications"
// nf-fa-bell, present in Inconsolata Nerd Font.
icon: ""
settings: true
group: "Desktop"
keywords: ["notifyd", "timeout", "history", "queue", "mute", "dnd", "corner", "monitor", "balloon"]
property var draft: Notify.config
property string notice: ""
dirty: JSON.stringify(nm.draft) !== JSON.stringify(Notify.config)
readonly property bool valid: [nm.draft.liveCap, nm.draft.historyCap].every(v => v >= 1 && v <= 100)
// An edit replaces the draft's binding with a value; these put the
// binding back, so a change to the file shows up while nothing is
// edited. The window calls refresh() on every open, which covers the
// value a save leaves behind. Not over unsaved edits.
function follow() { nm.draft = Qt.binding(() => Notify.config); }
function refresh() { if (!nm.dirty) nm.follow(); }
function discard() { nm.follow(); nm.notice = ""; }
function edit(patch) { nm.draft = Object.assign({}, nm.draft, patch); }
// Unknown keys in the file survive, including inside timeouts and
// balloons, which are merged one level down. The text goes on stdin to
// cat (the notes pattern: the exit code is the outcome); 1 is the write,
// 2 the reload, so a stopped daemon reads as saved-but-not-applied.
// Written in onStarted, then stdinEnabled cleared to give cat its EOF:
// the sequence notes' bodyWriter uses (NotesModule.qml).
function save() {
nm.notice = "";
const raw = Notify.rawConfig;
const obj = v => v && typeof v === "object" && !Array.isArray(v) ? v : {};
const out = Object.assign({}, raw, nm.draft, {
timeouts: Object.assign({}, obj(raw.timeouts), nm.draft.timeouts),
balloons: Object.assign({}, obj(raw.balloons), nm.draft.balloons),
});
nm.saver.data = JSON.stringify(out, null, 2) + "\n";
nm.saver.stdinEnabled = true;
nm.saver.running = false;
nm.saver.running = true;
}
property Process saver: Process {
property string data: ""
command: ["sh", "-c", 'mkdir -p "$(dirname "$1")" && cat > "$1" || exit 1; notifyctl reload || exit 2',
"sh", Notify.configPath]
onStarted: {
nm.saver.write(nm.saver.data);
nm.saver.stdinEnabled = false;
}
onExited: code => nm.notice = code === 0 ? "saved"
: code === 2 ? "saved; notifyd not reachable, it reads the file at its next start"
: "could not write " + Notify.configPath
}
page: Component {
Page {
title: "Notifications"
actions: Component {
SaveBar { dirty: nm.dirty; valid: nm.valid; note: "caps are 1 to 100"; busy: nm.saver.running
onSave: nm.save(); onDiscard: nm.discard() }
}
NotificationsPage { width: parent.width; mod: nm }
}
}
}
|