src / macos.ts
import { execSync } from "child_process";
import type { CalEvent } from "./ical";
export function fetchMacOSEvents(fromDate: Date, toDate: Date): CalEvent[] {
if (process.platform !== "darwin") {
throw new Error("macOS calendar source is only available on macOS.");
}
const fromOffset = Math.floor((Date.now() - fromDate.getTime()) / 1000);
const toOffset = Math.floor((toDate.getTime() - Date.now()) / 1000);
const script = `
set fromDate to (current date) - ${fromOffset} * seconds
set toDate to (current date) + ${toOffset} * seconds
set output to ""
tell application "Calendar"
repeat with cal in calendars
set evts to (every event of cal whose start date >= fromDate and start date <= toDate)
repeat with e in evts
set eName to summary of e
set eStart to start date of e as string
set eEnd to end date of e as string
set eLoc to ""
try
set eLoc to location of e
end try
set output to output & eName & "|||" & eStart & "|||" & eEnd & "|||" & eLoc & "\\n"
end repeat
end repeat
end tell
return output
`.trim();
let raw = "";
try {
raw = execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, {
timeout: 10000,
encoding: "utf-8",
});
} catch (err) {
throw new Error(`AppleScript failed: ${err instanceof Error ? err.message : String(err)}`);
}
const events: CalEvent[] = [];
for (const line of raw.trim().split("\n")) {
if (!line.trim()) continue;
const parts = line.split("|||");
if (parts.length < 3) continue;
const [summary, startStr, endStr, location] = parts;
const start = new Date(startStr);
const end = new Date(endStr);
if (isNaN(start.getTime()) || isNaN(end.getTime())) continue;
events.push({
uid: `${summary}-${start.toISOString()}`,
summary: summary.trim(),
start,
end,
location: location?.trim() || undefined,
allDay: false,
});
}
return events.sort((a, b) => a.start.getTime() - b.start.getTime());
}