aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/notifications/NotificationsModule.qml
blob: f1fac3aa90f67fe156ff04bd501ea83b6bf27bbf (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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// 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: ""
    property string error: ""
    // The installed sound themes (directories with an index.theme), and the
    // system theme as gsettings has it, "" when unreadable. Both reread on
    // every refresh, so a change made outside the page shows.
    property var themes: []
    property string systemTheme: ""
    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();
        nm.themeLister.running = false; nm.themeLister.running = true;
        nm.gsGet.running = false; nm.gsGet.running = true;
    }
    function discard() { nm.follow(); nm.notice = ""; nm.error = ""; }
    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, and 3
    // the gsettings theme (2 wins when both fail). gsettings is written only
    // when the theme itself changed: saving a timeout must not reset a system
    // theme changed elsewhere, which the page flags instead.
    // Written in onStarted, then stdinEnabled cleared to give cat its EOF:
    // the sequence notes' bodyWriter uses (NotesModule.qml).
    function save() {
        nm.notice = "";
        nm.error = "";
        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),
            sounds: Object.assign({}, obj(raw.sounds), nm.draft.sounds),
        });
        nm.saver.theme = nm.draft.soundTheme !== Notify.config.soundTheme ? nm.draft.soundTheme : "";
        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: ""
        property string theme: ""
        command: ["sh", "-c",
            'mkdir -p "$(dirname "$1")" && cat > "$1" || exit 1; '
            + '{ [ -z "$2" ] || gsettings set org.gnome.desktop.sound theme-name "$2"; } || g=3; '
            + 'notifyctl reload || exit 2; exit ${g:-0}',
            "sh", Notify.configPath, nm.saver.theme]
        onStarted: {
            nm.saver.write(nm.saver.data);
            nm.saver.stdinEnabled = false;
        }
        onExited: code => {
            if (code === 0) nm.notice = "saved";
            else if (code === 2) nm.notice = "saved; notifyd not running, it reads it at start";
            else if (code === 3) nm.notice = "saved; system sound theme not set";
            else nm.error = code === 1 ? "could not write config.json" : "save interrupted";
            nm.gsGet.running = false; nm.gsGet.running = true;
        }
    }
    property Process themeLister: Process {
        command: ["sh", "-c", 'find "$HOME/.local/share/sounds" /usr/share/sounds -mindepth 2 -maxdepth 2 -name index.theme 2>/dev/null']
        stdout: StdioCollector {
            onStreamFinished: nm.themes = [...new Set(text.split("\n").filter(l => l)
                .map(l => l.split("/").slice(-2)[0]))].sort()
        }
    }
    // gsettings prints the string quoted: 'freedesktop'.
    property Process gsGet: Process {
        command: ["gsettings", "get", "org.gnome.desktop.sound", "theme-name"]
        stdout: StdioCollector {
            onStreamFinished: nm.systemTheme = text.trim().replace(/^'(.*)'$/, "$1")
        }
        onExited: code => { if (code !== 0) nm.systemTheme = ""; }
    }

    page: Component {
        Page {
            title: "Notifications"
            actions: Component {
                SaveBar { dirty: nm.dirty; valid: nm.valid; note: "caps are 1 to 100"; busy: nm.saver.running
                          notice: nm.notice; noticeColor: nm.notice === "saved" ? Theme.green : Theme.yellow
                          error: nm.error
                          onSave: nm.save(); onDiscard: nm.discard() }
            }
            NotificationsPage { width: parent.width; mod: nm }
        }
    }
}