mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
support khal as calendar data provider
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Location
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Python scripts
|
||||
readonly property string checkCalendarAvailableScript: Quickshell.shellDir + '/Scripts/calendar/check-calendar.py'
|
||||
readonly property string listCalendarsScript: Quickshell.shellDir + '/Scripts/calendar/list-calendars.py'
|
||||
readonly property string calendarEventsScript: Quickshell.shellDir + '/Scripts/calendar/calendar-events.py'
|
||||
|
||||
function init(service) {
|
||||
CalendarService = service;
|
||||
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));
|
||||
const endDate = new Date(now.getTime() + (daysAhead * 24 * 60 * 60 * 1000));
|
||||
|
||||
loadEventsProcess.startTime = Math.floor(startDate.getTime() / 1000);
|
||||
loadEventsProcess.endTime = Math.floor(endDate.getTime() / 1000);
|
||||
loadEventsProcess.running = true;
|
||||
|
||||
Logger.d("Calendar", `Loading events (${daysBehind} days behind, ${daysAhead} days ahead): ${startDate.toLocaleDateString()} to ${endDate.toLocaleDateString()}`);
|
||||
}
|
||||
|
||||
// Process to check for evolution-data-server libraries
|
||||
Process {
|
||||
id: availabilityCheckProcess
|
||||
running: false
|
||||
command: ["sh", "-c", "command -v python3 >/dev/null 2>&1 && python3 " +
|
||||
root.checkCalendarAvailableScript + " || echo 'unavailable: python3 not installed'"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const result = text.trim();
|
||||
if (result === "available") {
|
||||
CalendarService.dataProvider = CalendarService.dataProvider || root;
|
||||
CalendarService.available = true;
|
||||
}
|
||||
|
||||
if (CalendarService.available) {
|
||||
Logger.i("Calendar", "EDS libraries available");
|
||||
loadCalendars();
|
||||
} else {
|
||||
Logger.w("Calendar", "EDS libraries not available: " + result);
|
||||
CalendarService.lastError = "Evolution Data Server libraries not installed";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
Logger.d("Calendar", "Availability check error: " + text);
|
||||
CalendarService.lastError = "Failed to check library availability";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process to list available calendars
|
||||
Process {
|
||||
id: listCalendarsProcess
|
||||
running: false
|
||||
command: ["python3", root.listCalendarsScript]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const result = JSON.parse(text.trim());
|
||||
CalendarService.setCalendars(result);
|
||||
Logger.d("Calendar", `Found ${result.length} calendar(s)`);
|
||||
|
||||
// Auto-load events after discovering calendars
|
||||
// Only load if we have calendars and no cached events
|
||||
if (result.length > 0 && CalendarService.events.length === 0) {
|
||||
loadEvents();
|
||||
} else if (result.length > 0) {
|
||||
// If we already have cached events, load in background
|
||||
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 int startTime: 0
|
||||
property int endTime: 0
|
||||
|
||||
command: ["python3", root.calendarEventsScript, startTime.toString(), endTime.toString()]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
CalendarService.loading = false;
|
||||
|
||||
try {
|
||||
const events = JSON.parse(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Location
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function init(service) {
|
||||
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 evolution-data-server libraries
|
||||
Process {
|
||||
id: availabilityCheckProcess
|
||||
running: false
|
||||
command: ["sh", "-c", "command -v khal >/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
|
||||
// Only load if we have calendars and no cached events
|
||||
if (calendars.length > 0 && CalendarService.events.length === 0) {
|
||||
loadEvents();
|
||||
} else if (calendars.length > 0) {
|
||||
// If we already have cached events, load in background
|
||||
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: [
|
||||
"khal", "list", "--once",
|
||||
"--json", "uid",
|
||||
"--json", "title",
|
||||
"--json", "start-long-full",
|
||||
"--json", "end-long-full",
|
||||
"--json", "calendar",
|
||||
"--json", "description",
|
||||
"--json", "location",
|
||||
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 = [];
|
||||
|
||||
for (const line of text.split("\n")) {
|
||||
if (!line.trim()) continue;
|
||||
const dayEvents = JSON.parse(line);
|
||||
for (const event of dayEvents) {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseTimestamp(timeStr) {
|
||||
// The actual timeStr format depends on user's khal configuration
|
||||
// for longdatetimeformat. Here we assume it's a reasonable to be
|
||||
// recognized by js Date parser.
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Location.Calendar
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -15,14 +16,11 @@ Singleton {
|
||||
property string lastError: ""
|
||||
property var calendars: ([])
|
||||
|
||||
property var dataProvider: null
|
||||
|
||||
// Persistent cache
|
||||
property string cacheFile: Settings.cacheDir + "calendar.json"
|
||||
|
||||
// Python scripts
|
||||
readonly property string checkCalendarAvailableScript: Quickshell.shellDir + '/Scripts/calendar/check-calendar.py'
|
||||
readonly property string listCalendarsScript: Quickshell.shellDir + '/Scripts/calendar/list-calendars.py'
|
||||
readonly property string calendarEventsScript: Quickshell.shellDir + '/Scripts/calendar/calendar-events.py'
|
||||
|
||||
// Cache file handling
|
||||
FileView {
|
||||
id: cacheFileView
|
||||
@@ -60,6 +58,25 @@ Singleton {
|
||||
onTriggered: cacheFileView.writeAdapter()
|
||||
}
|
||||
|
||||
function setEvents(newEvents) {
|
||||
root.events = newEvents;
|
||||
cacheAdapter.cachedEvents = newEvents;
|
||||
cacheAdapter.lastUpdate = new Date().toISOString();
|
||||
saveCache();
|
||||
}
|
||||
|
||||
function setCalendars(newCalendars) {
|
||||
root.calendars = newCalendars;
|
||||
cacheAdapter.cachedCalendars = newCalendars;
|
||||
saveCache();
|
||||
}
|
||||
|
||||
function loadCachedEvents() {
|
||||
if (cacheAdapter.cachedEvents.length > 0) {
|
||||
root.events = cacheAdapter.cachedEvents;
|
||||
}
|
||||
}
|
||||
|
||||
function saveCache() {
|
||||
saveDebounce.restart();
|
||||
}
|
||||
@@ -92,37 +109,28 @@ Singleton {
|
||||
|
||||
// Core functions
|
||||
function checkAvailability() {
|
||||
if (Settings.data.location.showCalendarEvents) {
|
||||
availabilityCheckProcess.running = true;
|
||||
} else {
|
||||
if (!Settings.data.location.showCalendarEvents) {
|
||||
root.available = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// EvolutionDataServer.init(root);
|
||||
Khal.init(root);
|
||||
}
|
||||
|
||||
function loadCalendars() {
|
||||
listCalendarsProcess.running = true;
|
||||
}
|
||||
|
||||
function loadEvents(daysAhead = 31, daysBehind = 14) {
|
||||
if (!Settings.data.location.showCalendarEvents) {
|
||||
root.loading = false;
|
||||
root.events = [];
|
||||
if (!root.available || !dataProvider) {
|
||||
return;
|
||||
}
|
||||
if (loading)
|
||||
|
||||
dataProvider.loadCalendars()
|
||||
}
|
||||
function loadEvents(daysAhead = 31, daysBehind = 14) {
|
||||
if (!root.available || !dataProvider) {
|
||||
return;
|
||||
loading = true;
|
||||
lastError = "";
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const startDate = new Date(now.getTime() - (daysBehind * 24 * 60 * 60 * 1000));
|
||||
const endDate = new Date(now.getTime() + (daysAhead * 24 * 60 * 60 * 1000));
|
||||
|
||||
loadEventsProcess.startTime = Math.floor(startDate.getTime() / 1000);
|
||||
loadEventsProcess.endTime = Math.floor(endDate.getTime() / 1000);
|
||||
loadEventsProcess.running = true;
|
||||
|
||||
Logger.d("Calendar", `Loading events (${daysBehind} days behind, ${daysAhead} days ahead): ${startDate.toLocaleDateString()} to ${endDate.toLocaleDateString()}`);
|
||||
dataProvider.loadEvents(daysAhead, daysBehind)
|
||||
}
|
||||
|
||||
// Helper to format date/time
|
||||
@@ -130,129 +138,4 @@ Singleton {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return Qt.formatDateTime(date, "yyyy-MM-dd hh:mm");
|
||||
}
|
||||
|
||||
// Process to check for evolution-data-server libraries
|
||||
Process {
|
||||
id: availabilityCheckProcess
|
||||
running: false
|
||||
command: ["sh", "-c", "command -v python3 >/dev/null 2>&1 && python3 " + root.checkCalendarAvailableScript + " || echo 'unavailable: python3 not installed'"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const result = text.trim();
|
||||
root.available = result === "available";
|
||||
|
||||
if (root.available) {
|
||||
Logger.i("Calendar", "EDS libraries available");
|
||||
loadCalendars();
|
||||
} else {
|
||||
Logger.w("Calendar", "EDS libraries not available: " + result);
|
||||
root.lastError = "Evolution Data Server libraries not installed";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
Logger.d("Calendar", "Availability check error: " + text);
|
||||
root.available = false;
|
||||
root.lastError = "Failed to check library availability";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process to list available calendars
|
||||
Process {
|
||||
id: listCalendarsProcess
|
||||
running: false
|
||||
command: ["python3", root.listCalendarsScript]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const result = JSON.parse(text.trim());
|
||||
root.calendars = result;
|
||||
cacheAdapter.cachedCalendars = result;
|
||||
saveCache();
|
||||
|
||||
Logger.d("Calendar", `Found ${result.length} calendar(s)`);
|
||||
|
||||
// Auto-load events after discovering calendars
|
||||
// Only load if we have calendars and no cached events
|
||||
if (result.length > 0 && root.events.length === 0) {
|
||||
loadEvents();
|
||||
} else if (result.length > 0) {
|
||||
// If we already have cached events, load in background
|
||||
loadEvents();
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.d("Calendar", "Failed to parse calendars: " + e);
|
||||
root.lastError = "Failed to parse calendar list";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
Logger.d("Calendar", "List calendars error: " + text);
|
||||
root.lastError = text.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process to load events
|
||||
Process {
|
||||
id: loadEventsProcess
|
||||
running: false
|
||||
property int startTime: 0
|
||||
property int endTime: 0
|
||||
|
||||
command: ["python3", root.calendarEventsScript, startTime.toString(), endTime.toString()]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.loading = false;
|
||||
|
||||
try {
|
||||
const result = JSON.parse(text.trim());
|
||||
root.events = result;
|
||||
cacheAdapter.cachedEvents = result;
|
||||
cacheAdapter.lastUpdate = new Date().toISOString();
|
||||
saveCache();
|
||||
|
||||
Logger.d("Calendar", `Loaded ${result.length} event(s)`);
|
||||
} catch (e) {
|
||||
Logger.d("Calendar", "Failed to parse events: " + e);
|
||||
root.lastError = "Failed to parse events";
|
||||
|
||||
// Fall back to cached events if available
|
||||
if (cacheAdapter.cachedEvents.length > 0) {
|
||||
root.events = cacheAdapter.cachedEvents;
|
||||
Logger.d("Calendar", "Using cached events");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.loading = false;
|
||||
|
||||
if (text.trim()) {
|
||||
Logger.d("Calendar", "Load events error: " + text);
|
||||
root.lastError = text.trim();
|
||||
|
||||
// Fall back to cached events if available
|
||||
if (cacheAdapter.cachedEvents.length > 0) {
|
||||
root.events = cacheAdapter.cachedEvents;
|
||||
Logger.d("Calendar", "Using cached events due to error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,13 @@ Singleton {
|
||||
|
||||
// Programs to check - maps property names to commands
|
||||
readonly property var programsToCheck: ({
|
||||
"nmcliAvailable": ["sh", "-c", "command -v nmcli"],
|
||||
"wlsunsetAvailable": ["sh", "-c", "command -v wlsunset"],
|
||||
"app2unitAvailable": ["sh", "-c", "command -v app2unit"],
|
||||
"gnomeCalendarAvailable": ["sh", "-c", "command -v gnome-calendar"],
|
||||
"gnomeCalendarAvailable": ["sh", "-c", "command -v gnome-calendar"],
|
||||
"wtypeAvailable": ["sh", "-c", "command -v wtype"],
|
||||
"pythonAvailable": ["sh", "-c", "command -v python3"]
|
||||
})
|
||||
"nmcliAvailable": ["sh", "-c", "command -v nmcli"],
|
||||
"wlsunsetAvailable": ["sh", "-c", "command -v wlsunset"],
|
||||
"app2unitAvailable": ["sh", "-c", "command -v app2unit"],
|
||||
"gnomeCalendarAvailable": ["sh", "-c", "command -v gnome-calendar"],
|
||||
"wtypeAvailable": ["sh", "-c", "command -v wtype"],
|
||||
"pythonAvailable": ["sh", "-c", "command -v python3"]
|
||||
})
|
||||
|
||||
// Discord client auto-detection
|
||||
property var availableDiscordClients: []
|
||||
|
||||
Reference in New Issue
Block a user