aboutsummaryrefslogtreecommitdiffstats
path: root/shared/Notify.qml
blob: 897e50b5eed029521e829c1ac10ff90344ca4c0e (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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// 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 Singleton

import Quickshell
import Quickshell.Io
import QtQuick

// The notification daemon's state, read from the files it publishes under
// $XDG_RUNTIME_DIR/notifyd/. The files are the interface, the same convention
// the status registry set: the daemon writes them, this reads them, and
// notifyctl is the one path back for a mutation. Nothing here speaks D-Bus.
//
// A shell restart loses nothing: the daemon keeps running, the files stay, and
// this singleton reads them back.
Singleton {
    id: root

    readonly property string dir: (Quickshell.env("XDG_RUNTIME_DIR") || "/tmp") + "/notifyd"

    // Parsed whole on every change. Malformed or missing JSON is treated as
    // empty rather than propagated: a renderer with a bad array is worse than
    // a renderer that briefly shows nothing.
    property var queue: []
    property var history: []

    // Written by the drawer, read here so the balloons know to stand down.
    property bool drawerOpen: false

    // Epoch milliseconds; 0 means not snoozing.
    property double snoozeUntil: 0

    // Read from the status registry's own file rather than from the Status
    // singleton. The renderers only read DND; referencing Status here would
    // instantiate it, and its onPresentationChanged writes status.dnd and
    // shells out to breaktimer.sh, side effects a read-only consumer must not
    // trigger. The file is the interface, so reading it directly is the same
    // value with none of the write side.
    property bool dnd: false

    function parseQueue() {
        try { root.queue = JSON.parse(queueFile.text() || "[]"); }
        catch (e) { root.queue = []; }
    }

    function parseHistory() {
        try { root.history = JSON.parse(historyFile.text() || "[]"); }
        catch (e) { root.history = []; }
    }

    // Mutations go through notifyctl, the only thing that talks to the daemon.
    function run(args) {
        ctl.command = ["notifyctl"].concat(args);
        ctl.running = false;
        ctl.running = true;
    }
    function close(id) { root.run(["close", String(id)]); }
    function closeAll() { root.run(["close-all"]); }
    function action(id, key) { root.run(["action", String(id), key]); }
    function clearHistory() { root.run(["clear-history"]); }

    // The rofi picker lives in the notifications component; a caller passes
    // the notification id and its [key, label] pairs. Only that component
    // uses this, but the singleton owns the Process so there is one place a
    // notifyctl-adjacent command is built.
    function actions(id, pairs) {
        const cmd = ["notify-actions.sh", String(id)];
        for (const pair of pairs) {
            cmd.push(pair[1]);
            cmd.push(pair[0]);
        }
        actProc.command = cmd;
        actProc.running = false;
        actProc.running = true;
    }

    Process { id: ctl }
    Process { id: actProc }

    FileView {
        id: queueFile
        path: root.dir + "/queue.json"
        watchChanges: true
        printErrors: false
        onFileChanged: reload()
        onLoaded: root.parseQueue()
        onLoadFailed: root.queue = []
    }

    FileView {
        id: historyFile
        path: root.dir + "/history.json"
        watchChanges: true
        printErrors: false
        onFileChanged: reload()
        onLoaded: root.parseHistory()
        onLoadFailed: root.history = []
    }

    FileView {
        id: drawerFile
        path: root.dir + "/drawer"
        watchChanges: true
        printErrors: false
        onFileChanged: reload()
        onLoaded: root.drawerOpen = drawerFile.text().trim() === "1"
        onLoadFailed: root.drawerOpen = false
    }

    FileView {
        id: dndFile
        path: (Quickshell.env("XDG_RUNTIME_DIR") || "/tmp") + "/status.dnd"
        watchChanges: true
        printErrors: false
        onFileChanged: reload()
        onLoaded: root.dnd = dndFile.text().trim() === "1"
        onLoadFailed: root.dnd = false
    }

    FileView {
        id: snoozeFile
        path: root.dir + "/snooze"
        watchChanges: true
        printErrors: false
        onFileChanged: reload()
        onLoaded: {
            const v = parseInt(snoozeFile.text().trim(), 10);
            root.snoozeUntil = isNaN(v) ? 0 : v * 1000;
        }
        onLoadFailed: root.snoozeUntil = 0
    }
}