#!/usr/bin/env swift // cal-events — dump calendar events for a date range as JSON, for tock logging. // // Usage: cal-events [] (to defaults to from) // Output: JSON array of {title, start, end, calendar, allDay} with ISO-8601 // local-time start/end. Non-all-day events only. All event calendars included; // filter downstream (the day tool defaults to the work calendar). import EventKit import Foundation let args = CommandLine.arguments guard args.count >= 2 else { fputs("usage: cal-events [to YYYY-MM-DD]\n", stderr) exit(2) } let dateOnly = DateFormatter() dateOnly.dateFormat = "yyyy-MM-dd" dateOnly.timeZone = TimeZone.current guard let fromDate = dateOnly.date(from: args[1]) else { fputs("bad from-date: \(args[1])\n", stderr); exit(2) } let toDate = args.count >= 3 ? (dateOnly.date(from: args[2]) ?? fromDate) : fromDate let cal = Calendar.current let start = cal.startOfDay(for: fromDate) let end = cal.date(byAdding: .day, value: 1, to: cal.startOfDay(for: toDate))! let store = EKEventStore() let sema = DispatchSemaphore(value: 0) var output = "[]" store.requestFullAccessToEvents { granted, _ in defer { sema.signal() } guard granted else { fputs("Calendar access denied — grant in System Settings > Privacy > Calendars\n", stderr) return } let iso = ISO8601DateFormatter() iso.timeZone = TimeZone.current iso.formatOptions = [.withInternetDateTime] let pred = store.predicateForEvents(withStart: start, end: end, calendars: nil) let events = store.events(matching: pred) .filter { !$0.isAllDay } .sorted { $0.startDate < $1.startDate } func esc(_ s: String) -> String { var r = s.replacingOccurrences(of: "\\", with: "\\\\") r = r.replacingOccurrences(of: "\"", with: "\\\"") r = r.replacingOccurrences(of: "\n", with: " ") return r } let items = events.map { e -> String in let title = esc(e.title ?? "(no title)") let calT = esc(e.calendar.title) return " {\"title\": \"\(title)\", \"start\": \"\(iso.string(from: e.startDate))\", " + "\"end\": \"\(iso.string(from: e.endDate))\", \"calendar\": \"\(calT)\"}" } output = items.isEmpty ? "[]" : "[\n" + items.joined(separator: ",\n") + "\n]" } sema.wait() print(output)