From a4941436232924194f8f45c0829fb0393c500973 Mon Sep 17 00:00:00 2001 From: Bill Cromie Date: Fri, 25 Oct 2024 13:59:47 -0400 Subject: [PATCH 1/5] now we are outputting unique ids for lists --- Sources/RemindersLibrary/CLI.swift | 60 +++++----- .../EKReminder+Encodable.swift | 6 +- .../RemindersLibrary/NaturalLanguage.swift | 2 +- Sources/RemindersLibrary/Reminders.swift | 105 ++++++++++++------ 4 files changed, 105 insertions(+), 68 deletions(-) diff --git a/Sources/RemindersLibrary/CLI.swift b/Sources/RemindersLibrary/CLI.swift index 329b88e..c90d8c2 100644 --- a/Sources/RemindersLibrary/CLI.swift +++ b/Sources/RemindersLibrary/CLI.swift @@ -61,9 +61,8 @@ private struct Show: ParsableCommand { abstract: "Print the items on the given list") @Argument( - help: "The list to print items from, see 'show-lists' for names", - completion: .custom(listNameCompletion)) - var listName: String + help: "The list to print items from, see 'show-lists' for names or IDs") + var listNameOrId: String @Flag(help: "Show completed items only") var onlyCompleted = false @@ -107,7 +106,7 @@ private struct Show: ParsableCommand { } reminders.showListItems( - withName: self.listName, dueOn: self.dueDate, displayOptions: displayOptions, + withNameOrId: self.listNameOrId, dueOn: self.dueDate, displayOptions: displayOptions, outputFormat: format, sort: sort, sortOrder: sortOrder) } } @@ -117,9 +116,8 @@ private struct Add: ParsableCommand { abstract: "Add a reminder to a list") @Argument( - help: "The list to add to, see 'show-lists' for names", - completion: .custom(listNameCompletion)) - var listName: String + help: "The list to add to, see 'show-lists' for names or IDs") + var listNameOrId: String @Argument( parsing: .remaining, @@ -150,7 +148,7 @@ private struct Add: ParsableCommand { reminders.addReminder( string: self.reminder.joined(separator: " "), notes: self.notes, - toListNamed: self.listName, + toListNamed: self.listNameOrId, dueDateComponents: self.dueDate, priority: priority, outputFormat: format) @@ -162,16 +160,15 @@ private struct Complete: ParsableCommand { abstract: "Complete a reminder") @Argument( - help: "The list to complete a reminder on, see 'show-lists' for names", - completion: .custom(listNameCompletion)) - var listName: String + help: "The list to complete a reminder on, see 'show-lists' for names or IDs") + var listNameOrId: String @Argument( - help: "The index or id of the reminder to delete, see 'show' for indexes") - var index: String + help: "The index or id of the reminder to delete, see 'show' for indexes and IDs") + var indexOrId: String func run() { - reminders.setComplete(true, itemAtIndex: self.index, onListNamed: self.listName) + reminders.setComplete(true, itemAtIndexOrId: self.indexOrId, onListNamedOrId: self.listNameOrId) } } @@ -180,16 +177,15 @@ private struct Uncomplete: ParsableCommand { abstract: "Uncomplete a reminder") @Argument( - help: "The list to uncomplete a reminder on, see 'show-lists' for names", - completion: .custom(listNameCompletion)) - var listName: String + help: "The list to uncomplete a reminder on, see 'show-lists' for names or IDs") + var listNameOrId: String @Argument( - help: "The index or id of the reminder to delete, see 'show' for indexes") - var index: String + help: "The index or id of the reminder to delete, see 'show' for indexes and IDs") + var indexOrId: String func run() { - reminders.setComplete(false, itemAtIndex: self.index, onListNamed: self.listName) + reminders.setComplete(false, itemAtIndexOrId: self.indexOrId, onListNamedOrId: self.listNameOrId) } } @@ -198,16 +194,15 @@ private struct Delete: ParsableCommand { abstract: "Delete a reminder") @Argument( - help: "The list to delete a reminder on, see 'show-lists' for names", - completion: .custom(listNameCompletion)) - var listName: String + help: "The list to delete a reminder on, see 'show-lists' for names or IDs") + var listNameOrId: String @Argument( - help: "The index or id of the reminder to delete, see 'show' for indexes") - var index: String + help: "The index or id of the reminder to delete, see 'show' for indexes and IDs") + var indexOrId: String func run() { - reminders.delete(itemAtIndex: self.index, onListNamed: self.listName) + reminders.delete(itemAtIndexOrId: self.indexOrId, onListNamedOrId: self.listNameOrId) } } @@ -222,13 +217,12 @@ private struct Edit: ParsableCommand { abstract: "Edit the text of a reminder") @Argument( - help: "The list to edit a reminder on, see 'show-lists' for names", - completion: .custom(listNameCompletion)) - var listName: String + help: "The list to edit a reminder on, see 'show-lists' for names or IDs") + var listNameOrId: String @Argument( - help: "The index or id of the reminder to delete, see 'show' for indexes") - var index: String + help: "The index or id of the reminder to delete, see 'show' for indexes and IDs") + var indexOrId: String @Option( name: .shortAndLong, @@ -249,8 +243,8 @@ private struct Edit: ParsableCommand { func run() { let newText = self.reminder.joined(separator: " ") reminders.edit( - itemAtIndex: self.index, - onListNamed: self.listName, + itemAtIndexOrId: self.indexOrId, + onListNamedOrId: self.listNameOrId, newText: newText.isEmpty ? nil : newText, newNotes: self.notes ) diff --git a/Sources/RemindersLibrary/EKReminder+Encodable.swift b/Sources/RemindersLibrary/EKReminder+Encodable.swift index b06f46c..d88f582 100644 --- a/Sources/RemindersLibrary/EKReminder+Encodable.swift +++ b/Sources/RemindersLibrary/EKReminder+Encodable.swift @@ -1,6 +1,6 @@ import EventKit -extension EKReminder: Encodable { +extension EKReminder: @retroactive Encodable { private enum EncodingKeys: String, CodingKey { case externalId case title @@ -24,7 +24,7 @@ extension EKReminder: Encodable { try container.encode(self.priority, forKey: .priority) try container.encode(self.calendar.title, forKey: .list) try container.encodeIfPresent(self.notes, forKey: .notes) - + // url field is nil // https://developer.apple.com/forums/thread/128140 try container.encodeIfPresent(self.url, forKey: .url) @@ -49,7 +49,7 @@ extension EKReminder: Encodable { try container.encodeIfPresent(format(dueDateComponents.date), forKey: .dueDate) } } - + private func format(_ date: Date?) -> String? { if #available(macOS 12.0, *) { return date?.ISO8601Format() diff --git a/Sources/RemindersLibrary/NaturalLanguage.swift b/Sources/RemindersLibrary/NaturalLanguage.swift index e38ab3c..b9f9baf 100644 --- a/Sources/RemindersLibrary/NaturalLanguage.swift +++ b/Sources/RemindersLibrary/NaturalLanguage.swift @@ -44,7 +44,7 @@ private func components(from string: String) -> DateComponents? { } } -extension DateComponents: ExpressibleByArgument { +extension DateComponents: @retroactive ExpressibleByArgument { public init?(argument: String) { if let components = components(from: argument) { self = components diff --git a/Sources/RemindersLibrary/Reminders.swift b/Sources/RemindersLibrary/Reminders.swift index cab7d77..a4d6714 100644 --- a/Sources/RemindersLibrary/Reminders.swift +++ b/Sources/RemindersLibrary/Reminders.swift @@ -90,12 +90,13 @@ public final class Reminders { } func showLists(outputFormat: OutputFormat) { + let calendars = self.getCalendars() switch (outputFormat) { case .json: - print(encodeToJson(data: self.getListNames())) + print(encodeToJson(data: calendars)) default: - for name in self.getListNames() { - print(name) + for calendar in calendars { + print("\(calendar.title) (\(calendar.calendarIdentifier))") } } } @@ -119,7 +120,7 @@ public final class Reminders { } let sameDay = calendar.compare( - reminderDueDate, to: dueDate, toGranularity: .day) == .orderedSame + reminderDueDate, to: dueDate, toGranularity: Calendar.Component.day) == .orderedSame if sameDay { matchingReminders.append((reminder, i, listName)) } @@ -140,13 +141,14 @@ public final class Reminders { semaphore.wait() } - func showListItems(withName name: String, dueOn dueDate: DateComponents?, displayOptions: DisplayOptions, + func showListItems(withNameOrId nameOrId: String, dueOn dueDate: DateComponents?, displayOptions: DisplayOptions, outputFormat: OutputFormat, sort: Sort, sortOrder: CustomSortOrder) { + let reminderCalendar = self.calendar(withNameOrId: nameOrId) let semaphore = DispatchSemaphore(value: 0) - let calendar = Calendar.current + let currentCalendar = Calendar.current - self.reminders(on: [self.calendar(withName: name)], displayOptions: displayOptions) { reminders in + self.reminders(on: [reminderCalendar], displayOptions: displayOptions) { reminders in var matchingReminders = [(EKReminder, Int?)]() let reminders = sort == .none ? reminders : reminders.sorted(by: sort.sortFunction(order: sortOrder)) for (i, reminder) in reminders.enumerated() { @@ -160,8 +162,8 @@ public final class Reminders { continue } - let sameDay = calendar.compare( - reminderDueDate, to: dueDate, toGranularity: .day) == .orderedSame + let sameDay = currentCalendar.compare( + reminderDueDate, to: dueDate, toGranularity: Calendar.Component.day) == .orderedSame if sameDay { matchingReminders.append((reminder, index)) } @@ -223,13 +225,18 @@ public final class Reminders { } } - func edit(itemAtIndex index: String, onListNamed name: String, newText: String?, newNotes: String?) { - let calendar = self.calendar(withName: name) + func edit( + itemAtIndexOrId indexOrId: String, + onListNamedOrId nameOrId: String, + newText: String?, + newNotes: String?) + { + let calendar = self.calendar(withNameOrId: nameOrId) let semaphore = DispatchSemaphore(value: 0) self.reminders(on: [calendar], displayOptions: .incomplete) { reminders in - guard let reminder = self.getReminder(from: reminders, at: index) else { - print("No reminder at index \(index) on \(name)") + guard let reminder = self.getReminder(from: reminders, atIndexOrId: indexOrId) else { + print("No reminder at index or with ID \(indexOrId) on \(nameOrId)") exit(1) } @@ -249,16 +256,16 @@ public final class Reminders { semaphore.wait() } - func setComplete(_ complete: Bool, itemAtIndex index: String, onListNamed name: String) { - let calendar = self.calendar(withName: name) + func setComplete(_ complete: Bool, itemAtIndexOrId indexOrId: String, onListNamedOrId nameOrId: String) { + let calendar = self.calendar(withNameOrId: nameOrId) let semaphore = DispatchSemaphore(value: 0) let displayOptions = complete ? DisplayOptions.incomplete : .complete let action = complete ? "Completed" : "Uncompleted" self.reminders(on: [calendar], displayOptions: displayOptions) { reminders in print(reminders.map { $0.title! }) - guard let reminder = self.getReminder(from: reminders, at: index) else { - print("No reminder at index \(index) on \(name)") + guard let reminder = self.getReminder(from: reminders, atIndexOrId: indexOrId) else { + print("No reminder at index or with ID \(indexOrId) on \(nameOrId)") exit(1) } @@ -277,13 +284,13 @@ public final class Reminders { semaphore.wait() } - func delete(itemAtIndex index: String, onListNamed name: String) { - let calendar = self.calendar(withName: name) + func delete(itemAtIndexOrId indexOrId: String, onListNamedOrId nameOrId: String) { + let calendar = self.calendar(withNameOrId: nameOrId) let semaphore = DispatchSemaphore(value: 0) self.reminders(on: [calendar], displayOptions: .incomplete) { reminders in - guard let reminder = self.getReminder(from: reminders, at: index) else { - print("No reminder at index \(index) on \(name)") + guard let reminder = self.getReminder(from: reminders, atIndexOrId: indexOrId) else { + print("No reminder at index or with ID \(indexOrId) on \(nameOrId)") exit(1) } @@ -309,15 +316,50 @@ public final class Reminders { priority: Priority, outputFormat: OutputFormat) { - let calendar = self.calendar(withName: name) + let calendar = self.calendar(withNameOrId: name) let reminder = EKReminder(eventStore: Store) reminder.calendar = calendar reminder.title = string reminder.notes = notes reminder.dueDateComponents = dueDateComponents reminder.priority = Int(priority.value.rawValue) - if let dueDate = dueDateComponents?.date, dueDateComponents?.hour != nil { - reminder.addAlarm(EKAlarm(absoluteDate: dueDate)) + if let dueDate = dueDateComponents, dueDate.hour != nil { + if let absoluteDate = dueDate.date { + reminder.addAlarm(EKAlarm(absoluteDate: absoluteDate)) + } + } + + do { + try Store.save(reminder, commit: true) + switch (outputFormat) { + case .json: + print(encodeToJson(data: reminder)) + default: + print("Added '\(reminder.title!)' to '\(calendar.title)'") + } + } catch let error { + print("Failed to save reminder with error: \(error)") + exit(1) + } + } + + func add( + _ text: String, + to name: String, + notes: String?, + dueDate: DateComponents?, + outputFormat: OutputFormat) + { + let calendar = self.calendar(withNameOrId: name) + let reminder = EKReminder(eventStore: Store) + reminder.calendar = calendar + reminder.title = text + reminder.notes = notes + reminder.dueDateComponents = dueDate + if let dueDate = dueDate, dueDate.hour != nil { + if let absoluteDate = dueDate.date { + reminder.addAlarm(EKAlarm(absoluteDate: absoluteDate)) + } } do { @@ -360,11 +402,13 @@ public final class Reminders { } } - private func calendar(withName name: String) -> EKCalendar { - if let calendar = self.getCalendars().find(where: { $0.title.lowercased() == name.lowercased() }) { + private func calendar(withNameOrId nameOrId: String) -> EKCalendar { + if let calendar = self.getCalendars().first(where: { $0.calendarIdentifier == nameOrId }) { + return calendar + } else if let calendar = self.getCalendars().first(where: { $0.title.lowercased() == nameOrId.lowercased() }) { return calendar } else { - print("No reminders list matching \(name)") + print("No reminders list matching \(nameOrId)") exit(1) } } @@ -374,12 +418,11 @@ public final class Reminders { .filter { $0.allowsContentModifications } } - private func getReminder(from reminders: [EKReminder], at index: String) -> EKReminder? { - precondition(!index.isEmpty, "Index cannot be empty, argument parser must be misconfigured") - if let index = Int(index) { + private func getReminder(from reminders: [EKReminder], atIndexOrId indexOrId: String) -> EKReminder? { + if let index = Int(indexOrId) { return reminders[safe: index] } else { - return reminders.first { $0.calendarItemExternalIdentifier == index } + return reminders.first { $0.calendarItemExternalIdentifier == indexOrId } } } From 2b0cc0b7b195e5ab122ad3286eae442649ff069c Mon Sep 17 00:00:00 2001 From: Bill Cromie Date: Fri, 25 Oct 2024 14:02:47 -0400 Subject: [PATCH 2/5] showing list id in show-all --- Sources/RemindersLibrary/EKReminder+Encodable.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/RemindersLibrary/EKReminder+Encodable.swift b/Sources/RemindersLibrary/EKReminder+Encodable.swift index d88f582..35c8cdb 100644 --- a/Sources/RemindersLibrary/EKReminder+Encodable.swift +++ b/Sources/RemindersLibrary/EKReminder+Encodable.swift @@ -14,6 +14,7 @@ extension EKReminder: @retroactive Encodable { case startDate case dueDate case list + case listId } public func encode(to encoder: Encoder) throws { @@ -23,6 +24,7 @@ extension EKReminder: @retroactive Encodable { try container.encode(self.isCompleted, forKey: .isCompleted) try container.encode(self.priority, forKey: .priority) try container.encode(self.calendar.title, forKey: .list) + try container.encode(self.calendar.calendarIdentifier, forKey: .listId) try container.encodeIfPresent(self.notes, forKey: .notes) // url field is nil From 1f7ac31baa264b7017c20649b9aead0539a30f65 Mon Sep 17 00:00:00 2001 From: Bill Cromie Date: Fri, 25 Oct 2024 14:05:29 -0400 Subject: [PATCH 3/5] can create reminders using uuids --- Sources/RemindersLibrary/CLI.swift | 2 +- Sources/RemindersLibrary/Reminders.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/RemindersLibrary/CLI.swift b/Sources/RemindersLibrary/CLI.swift index c90d8c2..94cd1c0 100644 --- a/Sources/RemindersLibrary/CLI.swift +++ b/Sources/RemindersLibrary/CLI.swift @@ -148,7 +148,7 @@ private struct Add: ParsableCommand { reminders.addReminder( string: self.reminder.joined(separator: " "), notes: self.notes, - toListNamed: self.listNameOrId, + toListNameOrId: self.listNameOrId, dueDateComponents: self.dueDate, priority: priority, outputFormat: format) diff --git a/Sources/RemindersLibrary/Reminders.swift b/Sources/RemindersLibrary/Reminders.swift index a4d6714..11aa7ee 100644 --- a/Sources/RemindersLibrary/Reminders.swift +++ b/Sources/RemindersLibrary/Reminders.swift @@ -311,12 +311,12 @@ public final class Reminders { func addReminder( string: String, notes: String?, - toListNamed name: String, + toListNameOrId nameOrId: String, dueDateComponents: DateComponents?, priority: Priority, outputFormat: OutputFormat) { - let calendar = self.calendar(withNameOrId: name) + let calendar = self.calendar(withNameOrId: nameOrId) let reminder = EKReminder(eventStore: Store) reminder.calendar = calendar reminder.title = string @@ -331,14 +331,14 @@ public final class Reminders { do { try Store.save(reminder, commit: true) - switch (outputFormat) { + switch outputFormat { case .json: print(encodeToJson(data: reminder)) - default: - print("Added '\(reminder.title!)' to '\(calendar.title)'") + case .plain: + print("Added reminder '\(reminder.title!)' to list '\(calendar.title)'") } } catch let error { - print("Failed to save reminder with error: \(error)") + print("Failed to add reminder with error: \(error)") exit(1) } } From db382c90edb076b5d8a5cd3d73c391f23db3d32a Mon Sep 17 00:00:00 2001 From: Bill Cromie Date: Fri, 25 Oct 2024 14:09:07 -0400 Subject: [PATCH 4/5] now can complete with IDs --- Sources/RemindersLibrary/CLI.swift | 26 +++++++++++++++++++--- Sources/RemindersLibrary/Reminders.swift | 28 +++++++++++++++--------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/Sources/RemindersLibrary/CLI.swift b/Sources/RemindersLibrary/CLI.swift index 94cd1c0..26d95d7 100644 --- a/Sources/RemindersLibrary/CLI.swift +++ b/Sources/RemindersLibrary/CLI.swift @@ -167,8 +167,15 @@ private struct Complete: ParsableCommand { help: "The index or id of the reminder to delete, see 'show' for indexes and IDs") var indexOrId: String + @Option( + name: .shortAndLong, + help: "Output format (plain or json)") + var format: OutputFormat = .plain + func run() { - reminders.setComplete(true, itemAtIndexOrId: self.indexOrId, onListNamedOrId: self.listNameOrId) + reminders.setComplete(true, itemAtIndexOrId: self.indexOrId, + onListNamedOrId: self.listNameOrId, + outputFormat: format) } } @@ -184,8 +191,15 @@ private struct Uncomplete: ParsableCommand { help: "The index or id of the reminder to delete, see 'show' for indexes and IDs") var indexOrId: String + @Option( + name: .shortAndLong, + help: "Output format (plain or json)") + var format: OutputFormat = .plain + func run() { - reminders.setComplete(false, itemAtIndexOrId: self.indexOrId, onListNamedOrId: self.listNameOrId) + reminders.setComplete(false, itemAtIndexOrId: self.indexOrId, + onListNamedOrId: self.listNameOrId, + outputFormat: format) } } @@ -234,6 +248,11 @@ private struct Edit: ParsableCommand { help: "The new reminder contents") var reminder: [String] = [] + @Option( + name: .shortAndLong, + help: "Output format (plain or json)") + var format: OutputFormat = .plain + func validate() throws { if self.reminder.isEmpty && self.notes == nil { throw ValidationError("Must specify either new reminder content or new notes") @@ -246,7 +265,8 @@ private struct Edit: ParsableCommand { itemAtIndexOrId: self.indexOrId, onListNamedOrId: self.listNameOrId, newText: newText.isEmpty ? nil : newText, - newNotes: self.notes + newNotes: self.notes, + outputFormat: format ) } } diff --git a/Sources/RemindersLibrary/Reminders.swift b/Sources/RemindersLibrary/Reminders.swift index 11aa7ee..2d22511 100644 --- a/Sources/RemindersLibrary/Reminders.swift +++ b/Sources/RemindersLibrary/Reminders.swift @@ -229,7 +229,8 @@ public final class Reminders { itemAtIndexOrId indexOrId: String, onListNamedOrId nameOrId: String, newText: String?, - newNotes: String?) + newNotes: String?, + outputFormat: OutputFormat) { let calendar = self.calendar(withNameOrId: nameOrId) let semaphore = DispatchSemaphore(value: 0) @@ -244,7 +245,12 @@ public final class Reminders { reminder.title = newText ?? reminder.title reminder.notes = newNotes ?? reminder.notes try Store.save(reminder, commit: true) - print("Updated reminder '\(reminder.title!)'") + switch outputFormat { + case .json: + print(encodeToJson(data: reminder)) + case .plain: + print("Updated reminder '\(reminder.title!)'") + } } catch let error { print("Failed to update reminder with error: \(error)") exit(1) @@ -252,18 +258,15 @@ public final class Reminders { semaphore.signal() } - semaphore.wait() } - func setComplete(_ complete: Bool, itemAtIndexOrId indexOrId: String, onListNamedOrId nameOrId: String) { + func setComplete(_ complete: Bool, itemAtIndexOrId indexOrId: String, onListNamedOrId nameOrId: String, outputFormat: OutputFormat) { let calendar = self.calendar(withNameOrId: nameOrId) let semaphore = DispatchSemaphore(value: 0) - let displayOptions = complete ? DisplayOptions.incomplete : .complete let action = complete ? "Completed" : "Uncompleted" - self.reminders(on: [calendar], displayOptions: displayOptions) { reminders in - print(reminders.map { $0.title! }) + self.reminders(on: [calendar], displayOptions: complete ? .incomplete : .complete) { reminders in guard let reminder = self.getReminder(from: reminders, atIndexOrId: indexOrId) else { print("No reminder at index or with ID \(indexOrId) on \(nameOrId)") exit(1) @@ -272,15 +275,19 @@ public final class Reminders { do { reminder.isCompleted = complete try Store.save(reminder, commit: true) - print("\(action) '\(reminder.title!)'") + switch outputFormat { + case .json: + print(encodeToJson(data: reminder)) + case .plain: + print("\(action) '\(reminder.title!)'") + } } catch let error { - print("Failed to save reminder with error: \(error)") + print("Failed to update reminder with error: \(error)") exit(1) } semaphore.signal() } - semaphore.wait() } @@ -434,3 +441,4 @@ private func encodeToJson(data: Encodable) -> String { let encoded = try! encoder.encode(data) return String(data: encoded, encoding: .utf8) ?? "" } + From 9c05a672af547935f03c93334cf7ca5b164bb314 Mon Sep 17 00:00:00 2001 From: Bill Cromie Date: Fri, 25 Oct 2024 14:23:17 -0400 Subject: [PATCH 5/5] adding ids to lists --- .../RemindersLibrary/EKCalendar+Encodable.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Sources/RemindersLibrary/EKCalendar+Encodable.swift diff --git a/Sources/RemindersLibrary/EKCalendar+Encodable.swift b/Sources/RemindersLibrary/EKCalendar+Encodable.swift new file mode 100644 index 0000000..1d6a227 --- /dev/null +++ b/Sources/RemindersLibrary/EKCalendar+Encodable.swift @@ -0,0 +1,14 @@ +import EventKit + +extension EKCalendar: @retroactive Encodable { + private enum EncodingKeys: String, CodingKey { + case title + case calendarIdentifier + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: EncodingKeys.self) + try container.encode(self.title, forKey: .title) + try container.encode(self.calendarIdentifier, forKey: .calendarIdentifier) + } +}