▍ humdrum codex / ticktock v0.0.2
license AGPL-3.0
2.3 KB raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#!/usr/bin/env swift
// cal-events — dump calendar events for a date range as JSON, for tock logging.
//
// Usage: cal-events <from YYYY-MM-DD> [<to YYYY-MM-DD>]   (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 <from YYYY-MM-DD> [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)