// 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 calendars, through calctl. Holds the occurrences of the range the // panel shows and reloads them when a calendar directory changes, whoever // changed it: the panel, khal, or the cron sync bringing the phone's edits. Singleton { id: root readonly property string calctl: Qt.resolvedUrl("calctl").toString().replace(/^file:\/\//, "") property var calendars: [] // [{name, color, path}] property string defaultCalendar: "" property var events: [] // occurrences in [from, to) property string from: "" property string to: "" property string error: "" // A list that finishes after a newer one started is dropped. property int generation: 0 readonly property var paths: calendars.map(c => c.path) function load(fromDay, toDay) { root.from = fromDay; root.to = toDay; root.refresh(); } function refresh() { if (!root.from) return; const g = ++root.generation; root.run(["list", root.from, root.to], r => { if (g !== root.generation) return; if (r.ok) { root.events = r.data; root.error = ""; } else root.error = r.error; }); } // `cal` is the calendar the event is in now: one UID can be in two. function get(uid, cal, cb) { root.run(["get", uid, cal], cb); } function create(event, cb) { root.run(["new", JSON.stringify(event)], cb); } function update(uid, cal, event, cb) { root.run(["edit", uid, cal, JSON.stringify(event)], cb); } function remove(uid, cal, cb) { root.run(["delete", uid, cal], cb); } // One Process per call, so a list and a save never share one (a running // Process ignores a second start, AGENTS.md). cb gets calctl's envelope. function run(args, cb) { op.createObject(root, { command: [root.calctl].concat(args), cb: cb }).running = true; } Component { id: op Process { id: proc property var cb: null stdout: StdioCollector { onStreamFinished: { let r; try { r = JSON.parse(text); } catch (e) { r = { ok: false, error: "calctl: " + (text.trim() || "no output") }; } try { if (proc.cb) proc.cb(r); } finally { proc.destroy(); } } } } } Component.onCompleted: root.run(["calendars"], r => { if (!r.ok) { root.error = r.error; return; } root.calendars = r.data.calendars; root.defaultCalendar = r.data.default; }) // Same shape as the mail count's xapian watch: one burst of file events // (a write is a temp file plus a rename; a sync touches several) becomes // one reload, and a dead watcher comes back. Process { id: watcher running: root.paths.length > 0 command: ["inotifywait", "-m", "-q", "-e", "close_write,moved_to,delete", "--format", "x"] .concat(root.paths) stdout: SplitParser { onRead: debounce.restart() } onExited: respawn.start() } Timer { id: debounce; interval: 300; onTriggered: root.refresh() } Timer { id: respawn; interval: 5000; onTriggered: watcher.running = root.paths.length > 0 } }