summary refs log tree commit diff
path: root/dot_local/bin/executable_reminders.swift
diff options
context:
space:
mode:
Diffstat (limited to 'dot_local/bin/executable_reminders.swift')
-rw-r--r--dot_local/bin/executable_reminders.swift384
1 files changed, 384 insertions, 0 deletions
diff --git a/dot_local/bin/executable_reminders.swift b/dot_local/bin/executable_reminders.swift
new file mode 100644
index 0000000..18e1135
--- /dev/null
+++ b/dot_local/bin/executable_reminders.swift
@@ -0,0 +1,384 @@
+#!/usr/bin/env swift
+
+import EventKit
+import Foundation
+
+
+let eventStore = EKEventStore()
+
+
+private func requestAccess() -> Bool {
+    let semaphore = DispatchSemaphore(value: 0)
+    var grantedAccess = false
+    eventStore.requestAccess(to: .reminder) { granted, _ in
+        grantedAccess = granted
+        semaphore.signal()
+    }
+    semaphore.wait()
+    return grantedAccess
+}
+
+
+struct CodableSummary : Codable {
+    let currentTime: Date
+    let source: String
+    let reminders: [CodableReminder]
+}
+
+
+struct CodableReminder : Codable {
+    let uid: String
+    let title: String?
+    let notes: String?
+    let completed: Bool
+    let priority: Int
+    let completionDate: Date?
+    let creationDate: Date?
+    let dueDate: Date?
+    let url: URL?
+}
+
+
+private func toCodable(_ ekReminder: EKReminder) -> CodableReminder {
+    let dueDate: Date?
+    if let dueDateComponents = ekReminder.dueDateComponents {
+        if let actualDueDate = dueDateComponents.date {
+            dueDate = actualDueDate
+        } else {
+            let calendar = dueDateComponents.calendar ?? Calendar.current
+            dueDate = calendar.date(from: dueDateComponents)
+        }
+    } else {
+        dueDate = nil
+    }
+    return CodableReminder(
+        uid: ekReminder.calendarItemIdentifier,
+        title: ekReminder.title,
+        notes: ekReminder.notes,
+        completed: ekReminder.isCompleted,
+        priority: ekReminder.priority,
+        completionDate: ekReminder.completionDate,
+        creationDate: ekReminder.creationDate,
+        dueDate: dueDate,
+        url: ekReminder.url?.absoluteURL
+    )
+}
+
+
+private func intToPriority(_ value: Int) -> String {
+    if let priority = EKReminderPriority(rawValue: UInt(value)) {
+        switch priority {
+        case EKReminderPriority.high:
+            return "high"
+        case EKReminderPriority.medium:
+            return "medium"
+        case EKReminderPriority.low:
+            return "low"
+        case EKReminderPriority.none:
+            return "none"
+        @unknown default:
+            return "unknown"
+        }
+    }
+    return "unknown"
+}
+
+
+private func componentsToIsoDate(_ value: DateComponents) -> String {
+    if let year = value.year {
+        if let month = value.month {
+            if let day = value.day {
+                return String(format: "%04d-%02d-%02-d", year, month, day)
+            }
+        }
+    }
+    return "0000-00-00"
+}
+
+
+private func nicerDebugEkReminder(_ ekReminder: EKReminder) -> String {
+    if let title = ekReminder.title {
+        if title.count > 0 {
+            return String(format: "\"%@\" [%@]", title.replacingOccurrences(of: "\n", with: " "), ekReminder.calendarItemIdentifier)
+        }
+    }
+    if ekReminder.description.count > 0 {
+        return String(format: "\"%@\" [%@]", ekReminder.description.replacingOccurrences(of: "\n", with: " "), ekReminder.calendarItemIdentifier)
+    }
+    return String(format: "\"\" [%@]", ekReminder.calendarItemIdentifier)
+}
+
+
+private func dateCloseEnough(_ one: Date?, _ two: Date?) -> Bool {
+    if let one_nn = one {
+        if let two_nn = two {
+            if abs(one_nn.distance(to: two_nn)) >= 1.0 {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    } else if two != nil {
+        return false;
+    }
+    return true;
+}
+
+
+private func updateEkReminder(ekReminder: EKReminder, reference: CodableReminder, timeZone: TimeZone) {
+    if (ekReminder.title ?? "").trimmingCharacters(in: .whitespacesAndNewlines) != (reference.title ?? "").trimmingCharacters(in: .whitespacesAndNewlines) {
+        print("Updating title on \(nicerDebugEkReminder(ekReminder))")
+        print("  old: [\(ekReminder.title ?? "<nil>")]")
+        print("  new: [\(reference.title ?? "<nil>")]")
+        ekReminder.title = reference.title
+    }
+    if (ekReminder.notes ?? "").trimmingCharacters(in: .whitespacesAndNewlines) != (reference.notes ?? "").trimmingCharacters(in: .whitespacesAndNewlines) {
+        print("Updating notes on \(nicerDebugEkReminder(ekReminder))")
+        print("  old: [\(ekReminder.notes ?? "<nil>")]")
+        print("  new: [\(reference.notes ?? "<nil>")]")
+        ekReminder.notes = reference.notes
+    }
+    if ekReminder.isCompleted != reference.completed {
+        print("Updating isCompleted on \(nicerDebugEkReminder(ekReminder))")
+        print("  old: \(ekReminder.isCompleted)")
+        print("  new: \(reference.completed)")
+        ekReminder.isCompleted = reference.completed
+    }
+    if ekReminder.priority != reference.priority {
+        print("Updating priority on \(nicerDebugEkReminder(ekReminder))")
+        print("  old: \(intToPriority(ekReminder.priority)) [\(ekReminder.priority)]")
+        print("  new: \(intToPriority(reference.priority)) [\(reference.priority)]")
+        ekReminder.priority = reference.priority
+    }
+    if !dateCloseEnough(ekReminder.completionDate, reference.completionDate) {
+        print("Updating completionDate on \(nicerDebugEkReminder(ekReminder))")
+        if let date = ekReminder.completionDate {
+            print("  old: \(date) [\(date.timeIntervalSince1970)]")
+        } else {
+            print("  old: <nil>")
+        }
+        if let date = reference.completionDate {
+            print("  new: \(date) [\(date.timeIntervalSince1970)]")
+        } else {
+            print("  new: <nil>")
+        }
+        ekReminder.completionDate = reference.completionDate
+    }
+    if !dateCloseEnough(ekReminder.creationDate, reference.creationDate) {
+        print("Different creationDate on \(nicerDebugEkReminder(ekReminder))")
+        if let date = ekReminder.completionDate {
+            print("  old: \(date) [\(date.timeIntervalSince1970)]")
+        } else {
+            print("  old: <nil>")
+        }
+        if let date = reference.completionDate {
+            print("  new: \(date) [\(date.timeIntervalSince1970)]")
+        } else {
+            print("  new: <nil>")
+        }
+        print("  cannot change creationDate!")
+    }
+    
+    if let dueDate = reference.dueDate {
+        let calendar = ekReminder.dueDateComponents?.calendar ?? Calendar(identifier: .gregorian)
+        var dueDateComponents = calendar.dateComponents(in: timeZone, from: dueDate)
+        if ((dueDateComponents.hour == 0 || dueDateComponents.hour == nil)
+                && (dueDateComponents.minute == 0 || dueDateComponents.minute == nil)
+                && (dueDateComponents.second == 0 || dueDateComponents.second == nil)
+                && (dueDateComponents.nanosecond == 0 || dueDateComponents.nanosecond == nil)) {
+
+            dueDateComponents.hour = nil
+            dueDateComponents.minute = nil
+            dueDateComponents.second = nil
+            dueDateComponents.nanosecond = nil
+            dueDateComponents.timeZone = nil
+            dueDateComponents.weekday = nil
+            dueDateComponents.weekdayOrdinal = nil
+            dueDateComponents.quarter = nil
+            dueDateComponents.weekOfMonth = nil
+            dueDateComponents.weekOfYear = nil
+            dueDateComponents.yearForWeekOfYear = nil
+            dueDateComponents.isLeapMonth = nil
+        }
+        if let oldDueDateComponents = ekReminder.dueDateComponents {
+            if oldDueDateComponents != dueDateComponents {
+                print("Updating dueDate on \(nicerDebugEkReminder(ekReminder))")
+                print("  old: \(componentsToIsoDate(oldDueDateComponents)) [\(oldDueDateComponents)]")
+                print("  new: \(componentsToIsoDate(dueDateComponents)) [\(dueDateComponents)]")
+                ekReminder.dueDateComponents = dueDateComponents
+            }
+        } else {
+            print("Updating dueDate on \(nicerDebugEkReminder(ekReminder))")
+            print("  old: <nil>")
+            print("  new: \(componentsToIsoDate(dueDateComponents)) [\(dueDateComponents)]")
+            ekReminder.dueDateComponents = dueDateComponents
+        }
+    } else if let oldDueDateComponents = ekReminder.dueDateComponents {
+        print("Updating dueDate on \(nicerDebugEkReminder(ekReminder))")
+        print("  old: \(componentsToIsoDate(oldDueDateComponents)) [\(oldDueDateComponents)]")
+        print("  new: <nil>")
+    }
+}
+
+
+private func newReminder(store: EKEventStore, reference: CodableReminder, timeZone: TimeZone) -> EKReminder {
+    let reminder = EKReminder(eventStore: store)
+    reminder.title = reference.title
+    reminder.notes = reference.notes
+    reminder.isCompleted = reference.completed
+    reminder.priority = reference.priority
+    reminder.completionDate = reference.completionDate
+    reminder.url = reference.url
+    reminder.calendar = eventStore.defaultCalendarForNewReminders()
+    
+    print("New reminder \(reminder.calendarItemIdentifier)")
+    print("  title: [\(reminder.title ?? "<nil>")]")
+    print("  notes: [\(reminder.notes ?? "<nil>")]")
+    print("  isCompleted: \(reminder.isCompleted)")
+    print("  priority: \(reminder.priority)")
+    if let date = reminder.completionDate {
+        print("  completionDate: \(date) [\(date.timeIntervalSince1970)]")
+    } else {
+        print("  completionDate: <nil>")
+    }
+    if let date = reminder.creationDate {
+        print("  creationDate: \(date) [\(date.timeIntervalSince1970)]")
+    } else {
+        print("  creationDate: <nil>")
+    }
+    
+    if let dueDate = reference.dueDate {
+        var dueDateComponents = Calendar(identifier: .gregorian).dateComponents(in: timeZone, from: dueDate)
+        if ((dueDateComponents.hour == 0 || dueDateComponents.hour == nil)
+                && (dueDateComponents.minute == 0 || dueDateComponents.minute == nil)
+                && (dueDateComponents.second == 0 || dueDateComponents.second == nil)
+                && (dueDateComponents.nanosecond == 0 || dueDateComponents.nanosecond == nil)) {
+            dueDateComponents.hour = nil
+            dueDateComponents.minute = nil
+            dueDateComponents.second = nil
+            dueDateComponents.nanosecond = nil
+            dueDateComponents.timeZone = nil
+            dueDateComponents.weekday = nil
+            dueDateComponents.weekdayOrdinal = nil
+            dueDateComponents.quarter = nil
+            dueDateComponents.weekOfMonth = nil
+            dueDateComponents.weekOfYear = nil
+            dueDateComponents.yearForWeekOfYear = nil
+            dueDateComponents.isLeapMonth = nil
+        }
+        reminder.dueDateComponents = dueDateComponents
+        print("  dueDateComponents: \(componentsToIsoDate(dueDateComponents)) [\(dueDateComponents)]")
+    } else {
+        print("  dueDateComponents: <nil>")
+    }
+    
+    print("  url: \(reminder.url?.absoluteString ?? "<nil>")")
+    
+    return reminder
+}
+
+
+private func dump(timeZone: TimeZone) {
+    let calendars = eventStore.calendars(for: EKEntityType.reminder)
+    let predicate = eventStore.predicateForReminders(in: calendars)
+    let semaphore = DispatchSemaphore(value: 0)
+    var remindersList: [CodableReminder] = []
+    eventStore.fetchReminders(matching: predicate) { (ekReminders) in
+        for reminder in ekReminders ?? [EKReminder]() {
+            remindersList.append(toCodable(reminder))
+        }
+        semaphore.signal()
+    }
+    semaphore.wait()
+    
+    let currentHost = Host.current().name ?? "unknown-host"
+    let currentUser = NSUserName()
+    let source = "\(currentUser)@\(currentHost)"
+    let encoder = JSONEncoder()
+    let customTzDateFormatter = DateFormatter()
+    customTzDateFormatter.timeZone = timeZone
+    customTzDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX"
+    encoder.dateEncodingStrategy = .formatted(customTzDateFormatter)
+    encoder.outputFormatting = .prettyPrinted
+    if let json = try? encoder.encode(CodableSummary(currentTime: Date(), source: source, reminders: remindersList)) {
+        print(String(data: json, encoding: .utf8)!)
+        exit(0)
+    } else {
+        print("Could not encode: " + remindersList.debugDescription)
+        exit(2)
+    }
+}
+
+
+private func load(timeZone: TimeZone, commit: Bool) {
+    let standardInput = FileHandle.standardInput
+    let decoder = JSONDecoder()
+    let customTzDateFormatter = DateFormatter()
+    customTzDateFormatter.timeZone = timeZone
+    customTzDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX"
+    decoder.dateDecodingStrategy = .formatted(customTzDateFormatter)
+    let decoded: CodableSummary
+    
+    var input: Data = Data()
+    var input_: Data
+    repeat {
+        input_ = standardInput.availableData
+        input.append(input_)
+    } while (input_.count > 0)
+    
+    do {
+        decoded = try decoder.decode(CodableSummary.self, from: input)
+    } catch {
+        print("Could not decode: \(error)")
+        exit(4)
+    }
+    
+    print("Updating with data from \(decoded.source) at \(decoded.currentTime)")
+    var success = true
+    for reminder in decoded.reminders {
+        if let ekReminder = eventStore.calendarItem(withIdentifier: reminder.uid) {
+            updateEkReminder(ekReminder: ekReminder as! EKReminder, reference: reminder, timeZone: timeZone)
+            try! eventStore.save(ekReminder as! EKReminder, commit: false)
+        } else {
+            if reminder.uid == "undefined" {
+                let newReminder = newReminder(store: eventStore, reference: reminder, timeZone: timeZone)
+                try! eventStore.save(newReminder, commit: false)
+            } else {
+                print("Could not find matching reminder with identifier \(reminder.uid)")
+                success = false
+            }
+        }
+    }
+    if success && commit {
+        print("Comitting...")
+        try! eventStore.commit()
+    }
+}
+
+
+if requestAccess() {
+    let timeZone: TimeZone
+    if let tzIndex = CommandLine.arguments.firstIndex(of: "--timezone") {
+        if let userTimeZone = TimeZone.init(identifier: CommandLine.arguments[tzIndex + 1]) {
+            timeZone = userTimeZone
+        } else {
+            print("Invalid time zone \(CommandLine.arguments[tzIndex + 1])")
+            exit(5)
+        }
+    } else {
+        timeZone = TimeZone.current
+    }
+    
+    if (CommandLine.arguments.contains("--load")) {
+        load(timeZone: timeZone, commit: CommandLine.arguments.contains("--commit"))
+    } else if (CommandLine.arguments.contains("--dump")) {
+        dump(timeZone: timeZone)
+    } else if (isatty(fileno(stdin)) == 0) {
+        load(timeZone: timeZone, commit: CommandLine.arguments.contains("--commit"))
+    } else {
+        dump(timeZone: timeZone)
+    }
+} else {
+    print("You need to grant reminders access")
+    exit(1)
+}