diff --git a/Sources/RemindCore/DateParsing.swift b/Sources/RemindCore/DateParsing.swift index b3be087..8925f8f 100644 --- a/Sources/RemindCore/DateParsing.swift +++ b/Sources/RemindCore/DateParsing.swift @@ -38,6 +38,15 @@ public enum DateParsing { return formatter.string(from: date) } + public static func formatDisplayAllDay(_ date: Date, calendar: Calendar = .current) -> String { + let formatter = DateFormatter() + formatter.locale = Locale.current + formatter.timeZone = calendar.timeZone + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter.string(from: date) + } + private static func parseRelativeDate(_ input: String, now: Date, calendar: Calendar) -> Date? { switch input { case "today": diff --git a/Sources/RemindCore/EventKitStore.swift b/Sources/RemindCore/EventKitStore.swift index e44dc89..d9e5b8b 100644 --- a/Sources/RemindCore/EventKitStore.swift +++ b/Sources/RemindCore/EventKitStore.swift @@ -101,7 +101,7 @@ public actor RemindersStore { reminder.calendar = calendar reminder.priority = draft.priority.eventKitValue if let dueDate = draft.dueDate { - reminder.dueDateComponents = calendarComponents(from: dueDate) + reminder.dueDateComponents = calendarComponents(from: dueDate, isAllDay: draft.isAllDay) } try eventStore.save(reminder, commit: true) return ReminderItem( @@ -113,7 +113,8 @@ public actor RemindersStore { priority: ReminderPriority(eventKitValue: Int(reminder.priority)), dueDate: date(from: reminder.dueDateComponents), listID: reminder.calendar.calendarIdentifier, - listName: reminder.calendar.title + listName: reminder.calendar.title, + isAllDay: isAllDay(components: reminder.dueDateComponents) ) } @@ -128,10 +129,16 @@ public actor RemindersStore { } if let dueDateUpdate = update.dueDate { if let dueDate = dueDateUpdate { - reminder.dueDateComponents = calendarComponents(from: dueDate) + let isAllDay = update.isAllDay ?? false + reminder.dueDateComponents = calendarComponents(from: dueDate, isAllDay: isAllDay) } else { reminder.dueDateComponents = nil } + } else if let isAllDay = update.isAllDay, let existingComponents = reminder.dueDateComponents { + // Update existing due date to change all-day status + if let existingDate = calendar.date(from: existingComponents) { + reminder.dueDateComponents = calendarComponents(from: existingDate, isAllDay: isAllDay) + } } if let priority = update.priority { reminder.priority = priority.eventKitValue @@ -154,7 +161,8 @@ public actor RemindersStore { priority: ReminderPriority(eventKitValue: Int(reminder.priority)), dueDate: date(from: reminder.dueDateComponents), listID: reminder.calendar.calendarIdentifier, - listName: reminder.calendar.title + listName: reminder.calendar.title, + isAllDay: isAllDay(components: reminder.dueDateComponents) ) } @@ -174,7 +182,8 @@ public actor RemindersStore { priority: ReminderPriority(eventKitValue: Int(reminder.priority)), dueDate: date(from: reminder.dueDateComponents), listID: reminder.calendar.calendarIdentifier, - listName: reminder.calendar.title + listName: reminder.calendar.title, + isAllDay: isAllDay(components: reminder.dueDateComponents) ) ) } @@ -214,6 +223,7 @@ public actor RemindersStore { let dueDateComponents: DateComponents? let listID: String let listName: String + let isAllDay: Bool } let reminderData = await withCheckedContinuation { (continuation: CheckedContinuation<[ReminderData], Never>) in @@ -229,7 +239,8 @@ public actor RemindersStore { priority: Int(reminder.priority), dueDateComponents: reminder.dueDateComponents, listID: reminder.calendar.calendarIdentifier, - listName: reminder.calendar.title + listName: reminder.calendar.title, + isAllDay: Self.checkIsAllDay(components: reminder.dueDateComponents) ) } continuation.resume(returning: data) @@ -246,7 +257,8 @@ public actor RemindersStore { priority: ReminderPriority(eventKitValue: data.priority), dueDate: date(from: data.dueDateComponents), listID: data.listID, - listName: data.listName + listName: data.listName, + isAllDay: data.isAllDay ) } } @@ -266,8 +278,12 @@ public actor RemindersStore { return calendar } - private func calendarComponents(from date: Date) -> DateComponents { - calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date) + private func calendarComponents(from date: Date, isAllDay: Bool = false) -> DateComponents { + if isAllDay { + return calendar.dateComponents([.year, .month, .day], from: date) + } else { + return calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date) + } } private func date(from components: DateComponents?) -> Date? { @@ -275,6 +291,17 @@ public actor RemindersStore { return calendar.date(from: components) } + private func isAllDay(components: DateComponents?) -> Bool { + guard let components else { return false } + // A reminder is all-day if it has date components but no time components + return components.hour == nil && components.minute == nil && components.second == nil + } + + private static func checkIsAllDay(components: DateComponents?) -> Bool { + guard let components else { return false } + return components.hour == nil && components.minute == nil && components.second == nil + } + private func item(from reminder: EKReminder) -> ReminderItem { ReminderItem( id: reminder.calendarItemIdentifier, @@ -285,7 +312,8 @@ public actor RemindersStore { priority: ReminderPriority(eventKitValue: Int(reminder.priority)), dueDate: date(from: reminder.dueDateComponents), listID: reminder.calendar.calendarIdentifier, - listName: reminder.calendar.title + listName: reminder.calendar.title, + isAllDay: isAllDay(components: reminder.dueDateComponents) ) } } diff --git a/Sources/RemindCore/Models.swift b/Sources/RemindCore/Models.swift index 5f4fe90..a0146d5 100644 --- a/Sources/RemindCore/Models.swift +++ b/Sources/RemindCore/Models.swift @@ -53,6 +53,7 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable { public let dueDate: Date? public let listID: String public let listName: String + public let isAllDay: Bool public init( id: String, @@ -63,7 +64,8 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable { priority: ReminderPriority, dueDate: Date?, listID: String, - listName: String + listName: String, + isAllDay: Bool = false ) { self.id = id self.title = title @@ -74,6 +76,7 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable { self.dueDate = dueDate self.listID = listID self.listName = listName + self.isAllDay = isAllDay } } @@ -82,12 +85,14 @@ public struct ReminderDraft: Sendable { public let notes: String? public let dueDate: Date? public let priority: ReminderPriority + public let isAllDay: Bool - public init(title: String, notes: String?, dueDate: Date?, priority: ReminderPriority) { + public init(title: String, notes: String?, dueDate: Date?, priority: ReminderPriority, isAllDay: Bool = false) { self.title = title self.notes = notes self.dueDate = dueDate self.priority = priority + self.isAllDay = isAllDay } } @@ -98,6 +103,7 @@ public struct ReminderUpdate: Sendable { public let priority: ReminderPriority? public let listName: String? public let isCompleted: Bool? + public let isAllDay: Bool? public init( title: String? = nil, @@ -105,7 +111,8 @@ public struct ReminderUpdate: Sendable { dueDate: Date?? = nil, priority: ReminderPriority? = nil, listName: String? = nil, - isCompleted: Bool? = nil + isCompleted: Bool? = nil, + isAllDay: Bool? = nil ) { self.title = title self.notes = notes @@ -113,5 +120,6 @@ public struct ReminderUpdate: Sendable { self.priority = priority self.listName = listName self.isCompleted = isCompleted + self.isAllDay = isAllDay } } diff --git a/Sources/remindctl/Commands/AddCommand.swift b/Sources/remindctl/Commands/AddCommand.swift index 571bc25..63b4f2e 100644 --- a/Sources/remindctl/Commands/AddCommand.swift +++ b/Sources/remindctl/Commands/AddCommand.swift @@ -24,6 +24,9 @@ enum AddCommand { help: "none|low|medium|high", parsing: .singleValue ), + ], + flags: [ + .make(label: "allDay", names: [.long("all-day")], help: "Create all-day reminder (no specific time)"), ] ) ), @@ -31,6 +34,7 @@ enum AddCommand { "remindctl add \"Buy milk\"", "remindctl add --title \"Call mom\" --list Personal --due tomorrow", "remindctl add \"Review docs\" --priority high", + "remindctl add \"Meeting prep\" --due today --all-day", ] ) { values, runtime in let titleOption = values.option("title") @@ -59,6 +63,7 @@ enum AddCommand { let dueDate = try dueValue.map(CommandHelpers.parseDueDate) let priority = try priorityValue.map(CommandHelpers.parsePriority) ?? .none + let isAllDay = values.flag("allDay") let store = RemindersStore() try await store.requestAccess() @@ -73,7 +78,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, isAllDay: isAllDay) let reminder = try await store.createReminder(draft, listName: targetList) OutputRenderer.printReminder(reminder, format: runtime.outputFormat) } diff --git a/Sources/remindctl/OutputFormatting.swift b/Sources/remindctl/OutputFormatting.swift index ee85c61..580887f 100644 --- a/Sources/remindctl/OutputFormatting.swift +++ b/Sources/remindctl/OutputFormatting.swift @@ -50,7 +50,7 @@ enum OutputRenderer { static func printReminder(_ reminder: ReminderItem, format: OutputFormat) { switch format { case .standard: - let due = reminder.dueDate.map { DateParsing.formatDisplay($0) } ?? "no due date" + let due = formatDueDate(reminder.dueDate, isAllDay: reminder.isAllDay) Swift.print("✓ \(reminder.title) [\(reminder.listName)] — \(due)") case .plain: Swift.print(plainLine(for: reminder)) @@ -96,12 +96,17 @@ 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 = formatDueDate(reminder.dueDate, isAllDay: reminder.isAllDay) let priority = reminder.priority == .none ? "" : " priority=\(reminder.priority.rawValue)" Swift.print("[\(index + 1)] [\(status)] \(reminder.title) [\(reminder.listName)] — \(due)\(priority)") } } + private static func formatDueDate(_ dueDate: Date?, isAllDay: Bool) -> String { + guard let dueDate else { return "no due date" } + return isAllDay ? DateParsing.formatDisplayAllDay(dueDate) : DateParsing.formatDisplay(dueDate) + } + private static func printRemindersPlain(_ reminders: [ReminderItem]) { let sorted = ReminderFiltering.sort(reminders) for reminder in sorted {