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
70 changes: 29 additions & 41 deletions Sources/RemindCore/EventKitStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,7 @@ public actor RemindersStore {
reminder.dueDateComponents = calendarComponents(from: dueDate)
}
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 @@ -145,17 +135,7 @@ public actor RemindersStore {

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 +144,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,24 +180,31 @@ public actor RemindersStore {
let completionDate: Date?
let priority: Int
let dueDateComponents: DateComponents?
let isAllDay: Bool
let listID: String
let listName: String
let lastModifiedDate: Date?
let creationDate: Date?
}

let reminderData = await withCheckedContinuation { (continuation: CheckedContinuation<[ReminderData], Never>) in
let predicate = eventStore.predicateForReminders(in: calendars)
eventStore.fetchReminders(matching: predicate) { reminders in
let data = (reminders ?? []).map { reminder in
ReminderData(
let isAllDay = reminder.dueDateComponents?.hour == nil && reminder.dueDateComponents?.minute == nil
return ReminderData(
Comment on lines +194 to +195
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isAllDay is computed as dueDateComponents?.hour == nil && ...minute == nil, which evaluates to true when dueDateComponents itself is nil. That will mark reminders with no due date as all-day (e.g., JSON output will show isAllDay: true while dueDate is null). Consider requiring dueDateComponents != nil (or checking .date/.year presence) before treating it as all-day.

Copilot uses AI. Check for mistakes.
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: Int(reminder.priority),
dueDateComponents: reminder.dueDateComponents,
isAllDay: isAllDay,
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
listName: reminder.calendar.title,
lastModifiedDate: reminder.lastModifiedDate,
creationDate: reminder.creationDate
)
}
continuation.resume(returning: data)
Expand All @@ -245,8 +220,11 @@ public actor RemindersStore {
completionDate: data.completionDate,
priority: ReminderPriority(eventKitValue: data.priority),
dueDate: date(from: data.dueDateComponents),
isAllDay: data.isAllDay,
listID: data.listID,
listName: data.listName
listName: data.listName,
lastModifiedDate: data.lastModifiedDate,
creationDate: data.creationDate
)
}
}
Expand All @@ -267,7 +245,13 @@ public actor RemindersStore {
}

private func calendarComponents(from date: Date) -> DateComponents {
calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date)
var components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date)
// If the time is exactly 00:00, we treat it as an all-day (date-only) reminder
if components.hour == 0 && components.minute == 0 {
components.hour = nil
components.minute = nil
}
Comment on lines +248 to +253
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calendarComponents(from:) currently converts any due date at exactly 00:00 into a date-only (all-day) reminder by nil-ing hour/minute. This makes it impossible to create an explicit midnight reminder (e.g., user input yyyy-MM-dd 00:00 parses to midnight and will be saved as all-day). Consider threading an explicit isAllDay flag through the API or returning richer parsing results so you only strip time when the user provided a date-only input.

Suggested change
var components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date)
// If the time is exactly 00:00, we treat it as an all-day (date-only) reminder
if components.hour == 0 && components.minute == 0 {
components.hour = nil
components.minute = nil
}
let components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date)

Copilot uses AI. Check for mistakes.
return components
}

private func date(from components: DateComponents?) -> Date? {
Expand All @@ -276,16 +260,20 @@ public actor RemindersStore {
}

private func item(from reminder: EKReminder) -> ReminderItem {
ReminderItem(
let isAllDay = reminder.dueDateComponents?.hour == nil && reminder.dueDateComponents?.minute == nil
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same isAllDay calculation issue here: with optional chaining, a missing dueDateComponents will be treated as all-day. This can lead to inconsistent ReminderItem state (isAllDay == true but dueDate == nil). Gate the all-day check on reminder.dueDateComponents != nil first.

Suggested change
let isAllDay = reminder.dueDateComponents?.hour == nil && reminder.dueDateComponents?.minute == nil
let dueComponents = reminder.dueDateComponents
let isAllDay = dueComponents != nil && dueComponents?.hour == nil && dueComponents?.minute == nil

Copilot uses AI. Check for mistakes.
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),
isAllDay: isAllDay,
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
listName: reminder.calendar.title,
lastModifiedDate: reminder.lastModifiedDate,
creationDate: reminder.creationDate
)
}
}
11 changes: 10 additions & 1 deletion Sources/RemindCore/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
public let completionDate: Date?
public let priority: ReminderPriority
public let dueDate: Date?
public let isAllDay: Bool
public let listID: String
public let listName: String
public let lastModifiedDate: Date?
public let creationDate: Date?

public init(
id: String,
Expand All @@ -62,8 +65,11 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
completionDate: Date?,
priority: ReminderPriority,
dueDate: Date?,
isAllDay: Bool = false,
listID: String,
listName: String
listName: String,
lastModifiedDate: Date? = nil,
creationDate: Date? = nil
) {
self.id = id
self.title = title
Expand All @@ -72,8 +78,11 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
self.completionDate = completionDate
self.priority = priority
self.dueDate = dueDate
self.isAllDay = isAllDay
self.listID = listID
self.listName = listName
self.lastModifiedDate = lastModifiedDate
self.creationDate = creationDate
}
}

Expand Down
14 changes: 13 additions & 1 deletion Sources/remindctl/OutputFormatting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,19 @@ enum OutputRenderer {
}
for (index, reminder) in sorted.enumerated() {
let status = reminder.isCompleted ? "x" : " "
let due = reminder.dueDate.map { DateParsing.formatDisplay($0) } ?? "no due date"
let due: String
if let dueDate = reminder.dueDate {
if reminder.isAllDay {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
due = formatter.string(from: dueDate)
Comment on lines +101 to +105
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new DateFormatter is created inside the loop for each all-day reminder. DateFormatter initialization is relatively expensive; consider hoisting a single formatter (or using a static cached formatter) outside the loop and reusing it to avoid repeated allocations when printing many reminders.

Copilot uses AI. Check for mistakes.
} else {
due = DateParsing.formatDisplay(dueDate)
}
} else {
Comment on lines +99 to +109
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The all-day due-date rendering behavior is new/changed here, but there are currently no tests covering OutputRenderer.printRemindersStandard formatting (remindctl tests exist for other output). Adding a small unit test for an all-day reminder vs. a timed reminder would help prevent regressions in CLI output.

Copilot uses AI. Check for mistakes.
due = "no due date"
}
let priority = reminder.priority == .none ? "" : " priority=\(reminder.priority.rawValue)"
Swift.print("[\(index + 1)] [\(status)] \(reminder.title) [\(reminder.listName)] — \(due)\(priority)")
}
Expand Down
Loading