Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ $ reminders show Soon
3: Something really important (priority: high)
```

#### Add a recurring reminder

```
$ reminders add Soon Daily standup --due-date "tomorrow 9am" --repeat daily
Added 'Daily standup' to 'Soon' (repeats: daily)
$ reminders add Soon Weekly review --due-date "next friday 3pm" --repeat weekly
Added 'Weekly review' to 'Soon' (repeats: weekly)
$ reminders show Soon
0: Ship reminders-cli
1: Daily standup (in 10 hours) (repeats: daily)
2: Weekly review (in 4 days) (repeats: weekly)
```

Supported values: `daily`, `weekly`, `monthly`, `yearly`

#### Show reminders due on or by a date

```
Expand Down
6 changes: 6 additions & 0 deletions Sources/RemindersLibrary/CLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,19 @@ private struct Add: ParsableCommand {
help: "The notes to add to the reminder")
var notes: String?

@Option(
name: .shortAndLong,
help: "Set a recurrence rule: daily, weekly, monthly, yearly")
var `repeat`: Recurrence?

func run() {
reminders.addReminder(
string: self.reminder.joined(separator: " "),
notes: self.notes,
toListNamed: self.listName,
dueDateComponents: self.dueDate,
priority: priority,
recurrence: self.repeat,
outputFormat: format)
}
}
Expand Down
14 changes: 14 additions & 0 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 recurrence
}

public func encode(to encoder: Encoder) throws {
Expand Down Expand Up @@ -58,6 +59,19 @@ extension EKReminder: @retroactive Encodable {
if let creationDate = self.creationDate {
try container.encode(format(creationDate), forKey: .creationDate)
}

if let rule = self.recurrenceRules?.first {
let frequencyName: String
switch rule.frequency {
case .daily: frequencyName = "daily"
case .weekly: frequencyName = "weekly"
case .monthly: frequencyName = "monthly"
case .yearly: frequencyName = "yearly"
@unknown default: frequencyName = "unknown"
}
let recurrenceValue = rule.interval == 1 ? frequencyName : "every \(rule.interval) \(frequencyName)"
try container.encode(recurrenceValue, forKey: .recurrence)
}
}

private func format(_ date: Date?) -> String? {
Expand Down
65 changes: 63 additions & 2 deletions Sources/RemindersLibrary/Reminders.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,42 @@ private extension EKReminder {
}
}

private func formattedRecurrence(from reminder: EKReminder) -> String? {
guard let rule = reminder.recurrenceRules?.first else { return nil }
switch rule.frequency {
case .daily:
if rule.interval == 1 {
return "daily"
}
return "every \(rule.interval) days"
case .weekly:
if rule.interval == 1 {
return "weekly"
}
return "every \(rule.interval) weeks"
case .monthly:
if rule.interval == 1 {
return "monthly"
}
return "every \(rule.interval) months"
case .yearly:
if rule.interval == 1 {
return "yearly"
}
return "every \(rule.interval) years"
@unknown default:
return "repeating"
}
}

private func format(_ reminder: EKReminder, at index: Int?, listName: String? = nil) -> String {
let dateString = formattedDueDate(from: reminder).map { " (\($0))" } ?? ""
let priorityString = Priority(reminder.mappedPriority).map { " (priority: \($0))" } ?? ""
let recurrenceString = formattedRecurrence(from: reminder).map { " (repeats: \($0))" } ?? ""
let listString = listName.map { "\($0): " } ?? ""
let notesString = reminder.notes.map { " (\($0))" } ?? ""
let indexString = index.map { "\($0): " } ?? ""
return "\(listString)\(indexString)\(reminder.title ?? "<unknown>")\(notesString)\(dateString)\(priorityString)"
return "\(listString)\(indexString)\(reminder.title ?? "<unknown>")\(notesString)\(dateString)\(priorityString)\(recurrenceString)"
}

public enum OutputFormat: String, ExpressibleByArgument {
Expand All @@ -35,6 +64,30 @@ public enum DisplayOptions: String, Decodable {
case complete
}

public enum Recurrence: String, ExpressibleByArgument, CaseIterable {
case daily
case weekly
case monthly
case yearly

var frequency: EKRecurrenceFrequency {
switch self {
case .daily: return .daily
case .weekly: return .weekly
case .monthly: return .monthly
case .yearly: return .yearly
}
}

func toRecurrenceRule() -> EKRecurrenceRule {
return EKRecurrenceRule(
recurrenceWith: self.frequency,
interval: 1,
end: nil
)
}
}

public enum Priority: String, ExpressibleByArgument {
case none
case low
Expand Down Expand Up @@ -314,6 +367,7 @@ public final class Reminders {
toListNamed name: String,
dueDateComponents: DateComponents?,
priority: Priority,
recurrence: Recurrence?,
outputFormat: OutputFormat)
{
let calendar = self.calendar(withName: name)
Expand All @@ -326,14 +380,21 @@ public final class Reminders {
if let dueDate = dueDateComponents?.date, dueDateComponents?.hour != nil {
reminder.addAlarm(EKAlarm(absoluteDate: dueDate))
}
if let recurrence = recurrence {
reminder.addRecurrenceRule(recurrence.toRecurrenceRule())
}

do {
try Store.save(reminder, commit: true)
switch (outputFormat) {
case .json:
print(encodeToJson(data: reminder))
default:
print("Added '\(reminder.title!)' to '\(calendar.title)'")
var message = "Added '\(reminder.title!)' to '\(calendar.title)'"
if let recurrence = recurrence {
message += " (repeats: \(recurrence.rawValue))"
}
print(message)
}
} catch let error {
print("Failed to save reminder with error: \(error)")
Expand Down