Files
noctalia-shell/Services/Location/Calendar/Khal.qml
T
Florian Boulay 1e7db6c325 Fix: Khal event lasting multiple days
Multiple days events are now displayed only once. This fix is also
compatible with a previous fix, which was meant to deal with recurring
events.

fix #1908
2026-02-23 17:37:39 +01:00

184 lines
5.5 KiB
QML

pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Location
Singleton {
id: root
readonly property string khalEventsScript: Quickshell.shellDir + '/Scripts/python/src/calendar/khal-events.py'
function init() {
availabilityCheckProcess.running = true;
}
function loadCalendars() {
listCalendarsProcess.running = true;
}
function loadEvents(daysAhead = 31, daysBehind = 14) {
CalendarService.loading = true;
CalendarService.lastError = "";
const now = new Date();
const startDate = new Date(now.getTime() - (daysBehind * 24 * 60 * 60 * 1000));
loadEventsProcess.startTime = formatDateYMD(startDate);
loadEventsProcess.duration = `${daysAhead + daysBehind}d`;
loadEventsProcess.running = true;
Logger.d("Calendar", `Loading events (${daysBehind} days behind, ${daysAhead} days ahead): ${loadEventsProcess.startTime} ${loadEventsProcess.duration}`);
}
// Process to check for khal installation
Process {
id: availabilityCheckProcess
running: false
command: ["sh", "-c", "command -v khal >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1"]
onExited: function (exitCode) {
if (exitCode === 0) {
CalendarService.available = true;
CalendarService.dataProvider = CalendarService.dataProvider || root;
}
if (CalendarService.available) {
Logger.i("Calendar", "Khal available");
loadCalendars();
} else {
Logger.w("Calendar", "Khal not available");
CalendarService.lastError = "khal binary not installed";
}
}
}
// Process to list available calendars
Process {
id: listCalendarsProcess
running: false
command: ["khal", "printcalendars"]
stdout: StdioCollector {
onStreamFinished: {
try {
const calendars = text.trim().split("\n");
CalendarService.setCalendars(calendars);
Logger.d("Calendar", `Found ${calendars.length} calendar(s)`);
// Auto-load events after discovering calendars
if (calendars.length > 0) {
loadEvents();
}
} catch (e) {
Logger.d("Calendar", "Failed to parse calendars: " + e);
CalendarService.lastError = "Failed to parse calendar list";
}
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
Logger.d("Calendar", "List calendars error: " + text);
CalendarService.lastError = text.trim();
}
}
}
}
// Process to load events
Process {
id: loadEventsProcess
running: false
property string startTime: ""
property string duration: ""
command: ["python3", root.khalEventsScript, startTime, duration]
stdout: StdioCollector {
onStreamFinished: {
CalendarService.loading = false;
try {
const events = parseEvents(text.trim());
CalendarService.setEvents(events);
Logger.d("Calendar", `Loaded ${events.length} events(s)`);
} catch (e) {
Logger.d("Calendar", "Failed to parse events: " + e);
CalendarService.lastError = "Failed to parse events";
// Fall back to cached events
CalendarService.loadCachedEvents();
Logger.d("Calendar", "Using cached events");
}
}
}
stderr: StdioCollector {
onStreamFinished: {
CalendarService.loading = false;
if (text.trim()) {
Logger.d("Calendar", "Load events error: " + text);
CalendarService.lastError = text.trim();
// Fall back to cached events
CalendarService.loadCachedEvents();
Logger.d("Calendar", "Using cached events");
}
}
}
}
function parseEvents(text) {
const result = [];
const duplicates = new Set();
for (const line of text.split("\n")) {
if (!line.trim())
continue;
const dayEvents = JSON.parse(line);
for (const event of dayEvents) {
if (event["repeat-pattern"] !== "") {
// if there is a repeat pattern, the event must be included each time
result.push({
uid: event.uid,
calendar: event.calendar,
summary: event.title,
start: parseTimestamp(event["start-long-full"]),
end: parseTimestamp(event["end-long-full"]),
location: event.location,
description: event.description
});
}
else if (!duplicates.has(event.uid) ) {
// in any other cases, we must remove duplicates using the uid of the event
result.push({
uid: event.uid,
calendar: event.calendar,
summary: event.title,
start: parseTimestamp(event["start-long-full"]),
end: parseTimestamp(event["end-long-full"]),
location: event.location,
description: event.description
});
duplicates.add(event.uid)
}
}
}
return result;
}
function parseTimestamp(timeStr) {
// expects ISO8601 format
return Math.floor((Date.parse(timeStr)).valueOf() / 1000);
}
function formatDateYMD(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return y + "-" + m + "-" + d;
}
}