aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/notifications/NotificationsModule.qml
blob: 01eea1850cc1d8172e8bea9c906fedb1b59489d6 (plain)
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 }
        }
    }
}