diff options
| l--------- | desktop/Notify.qml | 1 | ||||
| -rw-r--r-- | shared/Notify.qml | 123 |
2 files changed, 124 insertions, 0 deletions
diff --git a/desktop/Notify.qml b/desktop/Notify.qml new file mode 120000 index 0000000..635f31d --- /dev/null +++ b/desktop/Notify.qml @@ -0,0 +1 @@ +../shared/Notify.qml
\ No newline at end of file diff --git a/shared/Notify.qml b/shared/Notify.qml new file mode 100644 index 0000000..ff07575 --- /dev/null +++ b/shared/Notify.qml @@ -0,0 +1,123 @@ +// 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 + + 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: 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 + } +} |
