// Copyright (C) 2026 Danilo M. // // 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 } }