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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ format:

lint:
swift format lint --recursive Sources Tests
swiftlint
swiftlint lint --no-cache

test:
scripts/generate-version.sh
Expand Down
19 changes: 19 additions & 0 deletions Sources/RemindCore/EventKitStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ public actor RemindersStore {
if let dueDate = draft.dueDate {
reminder.dueDateComponents = calendarComponents(from: dueDate)
}
if let recurrence = draft.recurrence {
reminder.recurrenceRules = [RecurrenceAdapter.rule(from: recurrence)]
}
try eventStore.save(reminder, commit: true)
return ReminderItem(
id: reminder.calendarItemIdentifier,
Expand All @@ -112,6 +115,7 @@ public actor RemindersStore {
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
recurrence: reminder.recurrenceRules?.compactMap(RecurrenceAdapter.recurrence(from:)).first,
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
Expand All @@ -136,6 +140,13 @@ public actor RemindersStore {
if let priority = update.priority {
reminder.priority = priority.eventKitValue
}
if let recurrenceUpdate = update.recurrence {
if let recurrence = recurrenceUpdate {
reminder.recurrenceRules = [RecurrenceAdapter.rule(from: recurrence)]
} else {
reminder.recurrenceRules = nil
}
}
if let listName = update.listName {
reminder.calendar = try calendar(named: listName)
}
Expand All @@ -153,6 +164,7 @@ public actor RemindersStore {
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
recurrence: reminder.recurrenceRules?.compactMap(RecurrenceAdapter.recurrence(from:)).first,
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
Expand All @@ -173,6 +185,7 @@ public actor RemindersStore {
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
recurrence: reminder.recurrenceRules?.compactMap(RecurrenceAdapter.recurrence(from:)).first,
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
Expand All @@ -190,7 +203,9 @@ public actor RemindersStore {
}
return deleted
}
}

extension RemindersStore {
private func requestFullAccess() async throws -> Bool {
try await withCheckedThrowingContinuation { continuation in
eventStore.requestFullAccessToReminders { granted, error in
Expand All @@ -212,6 +227,7 @@ public actor RemindersStore {
let completionDate: Date?
let priority: Int
let dueDateComponents: DateComponents?
let recurrence: ReminderRecurrence?
let listID: String
let listName: String
}
Expand All @@ -228,6 +244,7 @@ public actor RemindersStore {
completionDate: reminder.completionDate,
priority: Int(reminder.priority),
dueDateComponents: reminder.dueDateComponents,
recurrence: reminder.recurrenceRules?.compactMap(RecurrenceAdapter.recurrence(from:)).first,
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
Expand All @@ -245,6 +262,7 @@ public actor RemindersStore {
completionDate: data.completionDate,
priority: ReminderPriority(eventKitValue: data.priority),
dueDate: date(from: data.dueDateComponents),
recurrence: data.recurrence,
listID: data.listID,
listName: data.listName
)
Expand Down Expand Up @@ -284,6 +302,7 @@ public actor RemindersStore {
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
recurrence: reminder.recurrenceRules?.compactMap(RecurrenceAdapter.recurrence(from:)).first,
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
Expand Down
42 changes: 41 additions & 1 deletion Sources/RemindCore/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,32 @@ public enum ReminderPriority: String, Codable, CaseIterable, Sendable {
}
}

public enum ReminderRecurrenceFrequency: String, Codable, CaseIterable, Sendable {
case daily
case weekly
}

public enum ReminderRecurrenceEnd: Codable, Sendable, Equatable {
case count(Int)
case until(Date)
}

public struct ReminderRecurrence: Codable, Sendable, Equatable {
public let frequency: ReminderRecurrenceFrequency
public let interval: Int
public let end: ReminderRecurrenceEnd?

public init(
frequency: ReminderRecurrenceFrequency,
interval: Int = 1,
end: ReminderRecurrenceEnd? = nil
) {
self.frequency = frequency
self.interval = interval
self.end = end
}
}

public struct ReminderList: Identifiable, Codable, Sendable, Equatable {
public let id: String
public let title: String
Expand All @@ -51,6 +77,7 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
public let completionDate: Date?
public let priority: ReminderPriority
public let dueDate: Date?
public let recurrence: ReminderRecurrence?
public let listID: String
public let listName: String

Expand All @@ -62,6 +89,7 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
completionDate: Date?,
priority: ReminderPriority,
dueDate: Date?,
recurrence: ReminderRecurrence? = nil,
listID: String,
listName: String
) {
Expand All @@ -72,6 +100,7 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
self.completionDate = completionDate
self.priority = priority
self.dueDate = dueDate
self.recurrence = recurrence
self.listID = listID
self.listName = listName
}
Expand All @@ -82,12 +111,20 @@ public struct ReminderDraft: Sendable {
public let notes: String?
public let dueDate: Date?
public let priority: ReminderPriority
public let recurrence: ReminderRecurrence?

public init(title: String, notes: String?, dueDate: Date?, priority: ReminderPriority) {
public init(
title: String,
notes: String?,
dueDate: Date?,
priority: ReminderPriority,
recurrence: ReminderRecurrence? = nil
) {
self.title = title
self.notes = notes
self.dueDate = dueDate
self.priority = priority
self.recurrence = recurrence
}
}

Expand All @@ -96,6 +133,7 @@ public struct ReminderUpdate: Sendable {
public let notes: String?
public let dueDate: Date??
public let priority: ReminderPriority?
public let recurrence: ReminderRecurrence??
public let listName: String?
public let isCompleted: Bool?

Expand All @@ -104,13 +142,15 @@ public struct ReminderUpdate: Sendable {
notes: String? = nil,
dueDate: Date?? = nil,
priority: ReminderPriority? = nil,
recurrence: ReminderRecurrence?? = nil,
listName: String? = nil,
isCompleted: Bool? = nil
) {
self.title = title
self.notes = notes
self.dueDate = dueDate
self.priority = priority
self.recurrence = recurrence
self.listName = listName
self.isCompleted = isCompleted
}
Expand Down
59 changes: 59 additions & 0 deletions Sources/RemindCore/RecurrenceAdapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import EventKit
import Foundation

enum RecurrenceAdapter {
static func rule(from recurrence: ReminderRecurrence) -> EKRecurrenceRule {
let frequency = eventKitFrequency(from: recurrence.frequency)
let interval = max(recurrence.interval, 1)
let end = recurrence.end.map(recurrenceEnd(from:))
return EKRecurrenceRule(recurrenceWith: frequency, interval: interval, end: end)
}

static func recurrence(from rule: EKRecurrenceRule) -> ReminderRecurrence? {
guard let frequency = reminderFrequency(from: rule.frequency) else {
return nil
}
let interval = max(rule.interval, 1)
let end = rule.recurrenceEnd.flatMap(reminderEnd(from:))
return ReminderRecurrence(frequency: frequency, interval: interval, end: end)
}

private static func eventKitFrequency(from frequency: ReminderRecurrenceFrequency) -> EKRecurrenceFrequency {
switch frequency {
case .daily:
return .daily
case .weekly:
return .weekly
}
}

private static func reminderFrequency(from frequency: EKRecurrenceFrequency) -> ReminderRecurrenceFrequency? {
switch frequency {
case .daily:
return .daily
case .weekly:
return .weekly
default:
return nil
}
}

private static func recurrenceEnd(from end: ReminderRecurrenceEnd) -> EKRecurrenceEnd {
switch end {
case .count(let count):
return EKRecurrenceEnd(occurrenceCount: max(count, 1))
case .until(let date):
return EKRecurrenceEnd(end: date)
}
}

private static func reminderEnd(from end: EKRecurrenceEnd) -> ReminderRecurrenceEnd? {
if end.occurrenceCount > 0 {
return .count(end.occurrenceCount)
}
if let endDate = end.endDate {
return .until(endDate)
}
return nil
}
}
34 changes: 32 additions & 2 deletions Sources/remindctl/Commands/AddCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ enum AddCommand {
.make(label: "list", names: [.short("l"), .long("list")], help: "List name", parsing: .singleValue),
.make(label: "due", names: [.short("d"), .long("due")], help: "Due date", parsing: .singleValue),
.make(label: "notes", names: [.short("n"), .long("notes")], help: "Notes", parsing: .singleValue),
.make(label: "repeat", names: [.long("repeat")], help: "daily|weekly", parsing: .singleValue),
.make(label: "interval", names: [.long("interval")], help: "Repeat interval", parsing: .singleValue),
.make(label: "count", names: [.long("count")], help: "Repeat occurrence count", parsing: .singleValue),
.make(label: "until", names: [.long("until")], help: "Repeat end date", parsing: .singleValue),
.make(
label: "priority",
names: [.short("p"), .long("priority")],
Expand Down Expand Up @@ -55,9 +59,29 @@ enum AddCommand {
let listName = values.option("list")
let notes = values.option("notes")
let dueValue = values.option("due")
let repeatValue = values.option("repeat")
let intervalValue = values.option("interval")
let countValue = values.option("count")
let untilValue = values.option("until")
let priorityValue = values.option("priority")

let dueDate = try dueValue.map(CommandHelpers.parseDueDate)
if repeatValue == nil && (intervalValue != nil || countValue != nil || untilValue != nil) {
throw RemindCoreError.operationFailed("Use --repeat with --interval, --count, or --until")
}

var dueDate = try dueValue.map(CommandHelpers.parseDueDate)
let recurrence = try repeatValue.map {
try RepeatParsing.parseRecurrence(
frequency: $0,
interval: intervalValue,
count: countValue,
until: untilValue
)
}

if recurrence != nil && dueDate == nil {
dueDate = Date()
}
let priority = try priorityValue.map(CommandHelpers.parsePriority) ?? .none

let store = RemindersStore()
Expand All @@ -73,7 +97,13 @@ enum AddCommand {
throw RemindCoreError.operationFailed("No default list found. Specify --list.")
}

let draft = ReminderDraft(title: title, notes: notes, dueDate: dueDate, priority: priority)
let draft = ReminderDraft(
title: title,
notes: notes,
dueDate: dueDate,
priority: priority,
recurrence: recurrence
)
let reminder = try await store.createReminder(draft, listName: targetList)
OutputRenderer.printReminder(reminder, format: runtime.outputFormat)
}
Expand Down
37 changes: 36 additions & 1 deletion Sources/remindctl/Commands/EditCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ enum EditCommand {
.make(label: "list", names: [.short("l"), .long("list")], help: "Move to list", parsing: .singleValue),
.make(label: "due", names: [.short("d"), .long("due")], help: "Set due date", parsing: .singleValue),
.make(label: "notes", names: [.short("n"), .long("notes")], help: "Set notes", parsing: .singleValue),
.make(label: "repeat", names: [.long("repeat")], help: "daily|weekly", parsing: .singleValue),
.make(label: "interval", names: [.long("interval")], help: "Repeat interval", parsing: .singleValue),
.make(label: "count", names: [.long("count")], help: "Repeat occurrence count", parsing: .singleValue),
.make(label: "until", names: [.long("until")], help: "Repeat end date", parsing: .singleValue),
.make(
label: "priority",
names: [.short("p"), .long("priority")],
Expand Down Expand Up @@ -54,6 +58,10 @@ enum EditCommand {
let title = values.option("title")
let listName = values.option("list")
let notes = values.option("notes")
let repeatValue = values.option("repeat")
let intervalValue = values.option("interval")
let countValue = values.option("count")
let untilValue = values.option("until")

var dueUpdate: Date??
if let dueValue = values.option("due") {
Expand All @@ -71,14 +79,40 @@ enum EditCommand {
priority = try CommandHelpers.parsePriority(priorityValue)
}

if repeatValue == nil && (intervalValue != nil || countValue != nil || untilValue != nil) {
throw RemindCoreError.operationFailed("Use --repeat with --interval, --count, or --until")
}

let recurrenceUpdate: ReminderRecurrence?? = try repeatValue.map {
try RepeatParsing.parseRecurrence(
frequency: $0,
interval: intervalValue,
count: countValue,
until: untilValue
)
}

let completeFlag = values.flag("complete")
let incompleteFlag = values.flag("incomplete")
if completeFlag && incompleteFlag {
throw RemindCoreError.operationFailed("Use either --complete or --incomplete, not both")
}
let isCompleted: Bool? = completeFlag ? true : (incompleteFlag ? false : nil)

if title == nil && listName == nil && notes == nil && dueUpdate == nil && priority == nil && isCompleted == nil {
if recurrenceUpdate != nil && dueUpdate == nil && reminder.dueDate == nil {
dueUpdate = .some(Date())
}

let hasChanges =
title != nil
|| listName != nil
|| notes != nil
|| dueUpdate != nil
|| priority != nil
|| recurrenceUpdate != nil
|| isCompleted != nil

if !hasChanges {
throw RemindCoreError.operationFailed("No changes specified")
}

Expand All @@ -87,6 +121,7 @@ enum EditCommand {
notes: notes,
dueDate: dueUpdate,
priority: priority,
recurrence: recurrenceUpdate,
listName: listName,
isCompleted: isCompleted
)
Expand Down
Loading