Skip to content
Merged
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ A simple CLI for interacting with OS X reminders.

```
$ reminders show-lists
Soon
Eventually
Soon (2A29C8B1-3D0F-4A9E-9C8D-5B6E7F8A9B0C)
Eventually (7E1F2A3B-4C5D-6E7F-8A9B-0C1D2E3F4A5B)
```

Every list also has a stable identifier, shown above in parentheses (and available as
`calendarIdentifier` with `--format json`). Anywhere a list name is accepted — `show`, `show-all`,
`add`, `complete`, `uncomplete`, `edit`, `delete` — a list ID works too, which is useful for
scripting against a list whose name might change or contains characters that are awkward on the
command line.

#### Show reminders on a specific list

```
Expand All @@ -29,6 +35,9 @@ $ reminders show Soon
0 Ship reminders-cli
```

`complete`, `uncomplete`, and `edit` also accept `--format json` to print the affected reminder as
JSON instead of the plain-text confirmation shown above.

#### Undo a completed item

```
Expand Down Expand Up @@ -81,6 +90,16 @@ $ reminders show Soon
0 Ship reminders-cli
```

The index argument above only matches against incomplete reminders, the same set `show` displays
by default. To delete a reminder that's already been completed, pass its ID (from `show
--only-completed --format json`, or the `externalId` field) instead of an index — an ID is looked
up regardless of completion state:

```
$ reminders delete Soon 44C111DE-0B69-4E96-8C93-6A5D0A6C2A17
Deleted 'Write README'
```

#### Add a reminder to a list

```
Expand Down
76 changes: 48 additions & 28 deletions Sources/RemindersLibrary/CLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ 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",
help: "The list to print items from, see 'show-lists' for names or IDs",
completion: .custom(listNameCompletion))
var listName: String
var listNameOrId: String

@Flag(help: "Show completed items only")
var onlyCompleted = false
Expand Down Expand Up @@ -170,7 +170,7 @@ private struct Show: ParsableCommand {
}

reminders.showListItems(
withName: self.listName, dueOn: self.dueDate, includeOverdue: self.includeOverdue,
withNameOrId: self.listNameOrId, dueOn: self.dueDate, includeOverdue: self.includeOverdue,
overdue: self.overdue, dueBefore: self.dueBefore, dueAfter: self.dueAfter,
noDueDate: self.noDueDate, priorities: self.priority, search: self.search,
displayOptions: displayOptions, outputFormat: format, sort: sort, sortOrder: sortOrder)
Expand All @@ -182,9 +182,9 @@ private struct Add: ParsableCommand {
abstract: "Add a reminder to a list")

@Argument(
help: "The list to add to, see 'show-lists' for names",
help: "The list to add to, see 'show-lists' for names or IDs",
completion: .custom(listNameCompletion))
var listName: String
var listNameOrId: String

@Argument(
parsing: .remaining,
Expand Down Expand Up @@ -255,7 +255,7 @@ private struct Add: ParsableCommand {
reminders.addReminder(
string: self.reminder.joined(separator: " "),
notes: self.notes,
toListNamed: self.listName,
toListNameOrId: self.listNameOrId,
dueDateComponents: self.dueDate,
priority: priority,
recurrence: self.repeat_,
Expand All @@ -270,16 +270,23 @@ private struct Complete: ParsableCommand {
abstract: "Complete a reminder")

@Argument(
help: "The list to complete a reminder on, see 'show-lists' for names",
help: "The list to complete a reminder on, see 'show-lists' for names or IDs",
completion: .custom(listNameCompletion))
var listName: String
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,
help: "Output format (plain or json)")
var format: OutputFormat = .plain

func run() {
reminders.setComplete(true, itemAtIndex: self.index, onListNamed: self.listName)
reminders.setComplete(true, itemAtIndexOrId: self.indexOrId,
onListNamedOrId: self.listNameOrId,
outputFormat: format)
}
}

Expand All @@ -288,16 +295,23 @@ private struct Uncomplete: ParsableCommand {
abstract: "Uncomplete a reminder")

@Argument(
help: "The list to uncomplete a reminder on, see 'show-lists' for names",
help: "The list to uncomplete a reminder on, see 'show-lists' for names or IDs",
completion: .custom(listNameCompletion))
var listName: String
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,
help: "Output format (plain or json)")
var format: OutputFormat = .plain

func run() {
reminders.setComplete(false, itemAtIndex: self.index, onListNamed: self.listName)
reminders.setComplete(false, itemAtIndexOrId: self.indexOrId,
onListNamedOrId: self.listNameOrId,
outputFormat: format)
}
}

Expand All @@ -306,16 +320,16 @@ private struct Delete: ParsableCommand {
abstract: "Delete a reminder")

@Argument(
help: "The list to delete a reminder on, see 'show-lists' for names",
help: "The list to delete a reminder on, see 'show-lists' for names or IDs",
completion: .custom(listNameCompletion))
var listName: String
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)
}
}

Expand All @@ -330,13 +344,13 @@ 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",
help: "The list to edit a reminder on, see 'show-lists' for names or IDs",
completion: .custom(listNameCompletion))
var listName: String
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,
Expand Down Expand Up @@ -395,6 +409,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.dueDate != nil && self.clearDueDate {
throw ValidationError("Cannot specify both --due-date and --clear-due-date")
Expand Down Expand Up @@ -437,8 +456,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,
newDueDateComponents: self.dueDate,
Expand All @@ -450,7 +469,8 @@ private struct Edit: ParsableCommand {
newRecurrenceInterval: self.repeatInterval,
newRecurrenceEndDate: self.repeatUntil,
clearRecurrenceEnd: self.clearRepeatEnd,
clearRecurrence: self.clearRepeat
clearRecurrence: self.clearRepeat,
outputFormat: format
)
}
}
Expand Down
6 changes: 0 additions & 6 deletions Sources/RemindersLibrary/CollectionType+Extension.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
extension Collection {
func find(where predicate: (Iterator.Element) throws -> Bool) rethrows -> Iterator.Element? {
return try self.firstIndex(where: predicate).flatMap { self[$0] }
}
}

extension Collection where Index == Int {
subscript(safe index: Int) -> Iterator.Element? {
return index < self.count && index >= 0 ? self[index] : nil
Expand Down
14 changes: 14 additions & 0 deletions Sources/RemindersLibrary/EKCalendar+Encodable.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
6 changes: 4 additions & 2 deletions Sources/RemindersLibrary/EKReminder+Encodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ extension EKReminder: @retroactive Encodable {
case startDate
case dueDate
case list
case listId
case recurrence
case recurrenceInterval
case recurrenceEnd
Expand All @@ -31,8 +32,9 @@ 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
// https://developer.apple.com/forums/thread/128140
try container.encodeIfPresent(self.url, forKey: .url)
Expand Down Expand Up @@ -87,7 +89,7 @@ extension EKReminder: @retroactive Encodable {
@unknown default: return nil
}
}

private func format(_ date: Date?) -> String? {
if #available(macOS 12.0, *) {
return date?.ISO8601Format()
Expand Down
Loading
Loading