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
93 changes: 57 additions & 36 deletions Sources/RemindCore/EventKitStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,11 @@ public actor RemindersStore {
if let dueDate = draft.dueDate {
reminder.dueDateComponents = calendarComponents(from: dueDate)
}
if let rule = draft.recurrenceRule {
applyRecurrence(rule, to: reminder)
}
try eventStore.save(reminder, commit: true)
return ReminderItem(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
return item(from: reminder)
}

public func updateReminder(id: String, update: ReminderUpdate) async throws -> ReminderItem {
Expand Down Expand Up @@ -142,20 +135,14 @@ public actor RemindersStore {
if let isCompleted = update.isCompleted {
reminder.isCompleted = isCompleted
}
if let recurrenceUpdate = update.recurrenceRule {
// .some(nil) = clear recurrence, .some(rule) = set recurrence
applyRecurrence(recurrenceUpdate, to: reminder)
}

try eventStore.save(reminder, commit: true)

return ReminderItem(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
return item(from: reminder)
}

public func completeReminders(ids: [String]) async throws -> [ReminderItem] {
Expand All @@ -164,19 +151,7 @@ public actor RemindersStore {
let reminder = try reminder(withID: id)
reminder.isCompleted = true
try eventStore.save(reminder, commit: true)
updated.append(
ReminderItem(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
)
updated.append(item(from: reminder))
}
return updated
}
Expand Down Expand Up @@ -212,6 +187,7 @@ public actor RemindersStore {
let completionDate: Date?
let priority: Int
let dueDateComponents: DateComponents?
let recurrenceRule: RecurrenceRule?
let listID: String
let listName: String
}
Expand All @@ -220,14 +196,27 @@ public actor RemindersStore {
let predicate = eventStore.predicateForReminders(in: calendars)
eventStore.fetchReminders(matching: predicate) { reminders in
let data = (reminders ?? []).map { reminder in
ReminderData(
let rule: RecurrenceRule? = {
guard let ekRule = reminder.recurrenceRules?.first else { return nil }
let freq: RecurrenceFrequency
switch ekRule.frequency {
case .daily: freq = .daily
case .weekly: freq = .weekly
case .monthly: freq = .monthly
case .yearly: freq = .yearly
@unknown default: return nil
}
return RecurrenceRule(frequency: freq, interval: ekRule.interval)
}()
return ReminderData(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: Int(reminder.priority),
dueDateComponents: reminder.dueDateComponents,
recurrenceRule: rule,
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
Expand All @@ -245,6 +234,7 @@ public actor RemindersStore {
completionDate: data.completionDate,
priority: ReminderPriority(eventKitValue: data.priority),
dueDate: date(from: data.dueDateComponents),
recurrenceRule: data.recurrenceRule,
listID: data.listID,
listName: data.listName
)
Expand Down Expand Up @@ -284,8 +274,39 @@ public actor RemindersStore {
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
recurrenceRule: recurrenceRule(from: reminder),
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
}

private func recurrenceRule(from reminder: EKReminder) -> RecurrenceRule? {
guard let ekRule = reminder.recurrenceRules?.first else { return nil }
let frequency: RecurrenceFrequency
switch ekRule.frequency {
case .daily: frequency = .daily
case .weekly: frequency = .weekly
case .monthly: frequency = .monthly
case .yearly: frequency = .yearly
@unknown default: return nil
}
return RecurrenceRule(frequency: frequency, interval: ekRule.interval)
}

private func applyRecurrence(_ rule: RecurrenceRule?, to reminder: EKReminder) {
// Remove existing rules
if let existing = reminder.recurrenceRules {
for r in existing { reminder.removeRecurrenceRule(r) }
}
guard let rule else { return }
let ekFrequency: EKRecurrenceFrequency
switch rule.frequency {
case .daily: ekFrequency = .daily
case .weekly: ekFrequency = .weekly
case .monthly: ekFrequency = .monthly
case .yearly: ekFrequency = .yearly
}
let ekRule = EKRecurrenceRule(recurrenceWith: ekFrequency, interval: rule.interval, end: nil)
reminder.addRecurrenceRule(ekRule)
}
}
49 changes: 47 additions & 2 deletions Sources/RemindCore/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,37 @@ public enum ReminderPriority: String, Codable, CaseIterable, Sendable {
}
}

public enum RecurrenceFrequency: String, Codable, Sendable, CaseIterable {
case daily
case weekly
case monthly
case yearly
}

public struct RecurrenceRule: Codable, Sendable, Equatable {
public let frequency: RecurrenceFrequency
public let interval: Int

public init(frequency: RecurrenceFrequency, interval: Int) {
self.frequency = frequency
self.interval = interval
}

public var displayString: String {
if interval == 1 {
return frequency.rawValue
}
let unit: String
switch frequency {
case .daily: unit = interval == 1 ? "day" : "days"
case .weekly: unit = interval == 1 ? "week" : "weeks"
case .monthly: unit = interval == 1 ? "month" : "months"
case .yearly: unit = interval == 1 ? "year" : "years"
}
return "every \(interval) \(unit)"
}
}

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

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

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

Expand All @@ -98,20 +140,23 @@ public struct ReminderUpdate: Sendable {
public let priority: ReminderPriority?
public let listName: String?
public let isCompleted: Bool?
public let recurrenceRule: RecurrenceRule??

public init(
title: String? = nil,
notes: String? = nil,
dueDate: Date?? = nil,
priority: ReminderPriority? = nil,
listName: String? = nil,
isCompleted: Bool? = nil
isCompleted: Bool? = nil,
recurrenceRule: RecurrenceRule?? = nil
) {
self.title = title
self.notes = notes
self.dueDate = dueDate
self.priority = priority
self.listName = listName
self.isCompleted = isCompleted
self.recurrenceRule = recurrenceRule
}
}
34 changes: 34 additions & 0 deletions Sources/remindctl/CommandHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,38 @@ enum CommandHelpers {
}
return date
}

static func parseRecurrence(_ value: String) throws -> RecurrenceRule {
let lower = value.lowercased().trimmingCharacters(in: .whitespaces)
switch lower {
case "daily":
return RecurrenceRule(frequency: .daily, interval: 1)
case "weekly":
return RecurrenceRule(frequency: .weekly, interval: 1)
case "biweekly":
return RecurrenceRule(frequency: .weekly, interval: 2)
case "monthly":
return RecurrenceRule(frequency: .monthly, interval: 1)
case "yearly":
return RecurrenceRule(frequency: .yearly, interval: 1)
default:
// Parse "every N days/weeks/months/years"
let pattern = #/^every\s+(\d+)\s+(days?|weeks?|months?|years?)$/#
if let match = lower.firstMatch(of: pattern) {
guard let n = Int(match.1), n > 0 else {
throw RemindCoreError.operationFailed("Invalid repeat interval: \"\(value)\"")
}
let unit = String(match.2)
let freq: RecurrenceFrequency
if unit.hasPrefix("day") { freq = .daily }
else if unit.hasPrefix("week") { freq = .weekly }
else if unit.hasPrefix("month") { freq = .monthly }
else { freq = .yearly }
return RecurrenceRule(frequency: freq, interval: n)
}
throw RemindCoreError.operationFailed(
"Invalid repeat value: \"\(value)\" (use daily|weekly|biweekly|monthly|yearly or \"every N days/weeks/months/years\")"
)
}
}
}
11 changes: 10 additions & 1 deletion Sources/remindctl/Commands/AddCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@ enum AddCommand {
help: "none|low|medium|high",
parsing: .singleValue
),
.make(
label: "repeat",
names: [.short("r"), .long("repeat")],
help: "daily|weekly|biweekly|monthly|yearly|every N days/weeks/months",
parsing: .singleValue
),
]
)
),
usageExamples: [
"remindctl add \"Buy milk\"",
"remindctl add --title \"Call mom\" --list Personal --due tomorrow",
"remindctl add \"Review docs\" --priority high",
"remindctl add \"Take vitamins\" --due tomorrow --repeat daily",
]
) { values, runtime in
let titleOption = values.option("title")
Expand Down Expand Up @@ -59,6 +66,8 @@ enum AddCommand {

let dueDate = try dueValue.map(CommandHelpers.parseDueDate)
let priority = try priorityValue.map(CommandHelpers.parsePriority) ?? .none
let repeatValue = values.option("repeat")
let recurrenceRule = try repeatValue.map(CommandHelpers.parseRecurrence)

let store = RemindersStore()
try await store.requestAccess()
Expand All @@ -73,7 +82,7 @@ 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, recurrenceRule: recurrenceRule)
let reminder = try await store.createReminder(draft, listName: targetList)
OutputRenderer.printReminder(reminder, format: runtime.outputFormat)
}
Expand Down
Loading