diff options
author | Alen <alen@dotfiles.xyz> | 2023-09-14 02:55:44 +0400 |
---|---|---|
committer | Alen <alen@dotfiles.xyz> | 2023-09-14 02:55:44 +0400 |
commit | b58b9ce1c165dc136f0f05a5f31ddc6e4d1d2ced (patch) | |
tree | 6d513fa5c7a9969a9b3361d579f99c25c0472ab3 | |
parent | f80537fe2b6b7d5d83a9cc472dffe1a4aace7c73 (diff) |
Add reminders.swift to local bin
-rw-r--r-- | .chezmoiignore | 3 | ||||
-rw-r--r-- | dot_local/bin/executable_reminders.swift | 384 |
2 files changed, 387 insertions, 0 deletions
diff --git a/.chezmoiignore b/.chezmoiignore index 36e7889..9bf243b 100644 --- a/.chezmoiignore +++ b/.chezmoiignore @@ -26,3 +26,6 @@ /OneDrive/Documents/PowerShell/profile.ps1 /Documents/PowerShell/profile.ps1 {{ end }} + +# Ignore reminders.swift for non-macOS +{{ if ne .chezmoi.os "darwin" -}} /.local/bin/reminders.swift {{- end }} 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) +} |