diff --git a/Package.swift b/Package.swift index 1d90247..f58e575 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Sources/reminder-cli/OutputFormatter.swift b/Sources/reminder-cli/OutputFormatter.swift index f4e8bcc..069379c 100644 --- a/Sources/reminder-cli/OutputFormatter.swift +++ b/Sources/reminder-cli/OutputFormatter.swift @@ -37,6 +37,12 @@ struct AlarmOutput: Codable { let proximity: String? // "arriving", "leaving" for location-based alarms } +struct ListOutput: Codable { + let name: String + let reminderCount: Int + let isDefault: Bool +} + struct RecurrenceRuleOutput: Codable { let frequency: String let interval: Int @@ -321,6 +327,29 @@ struct OutputFormatter { try outputYAML(data: reminders) } + // MARK: - Lists Output + + func outputLists(_ lists: [[String: Any]]) throws { + let codableLists = lists.map { dict -> ListOutput in + ListOutput( + name: dict["name"] as? String ?? "", + reminderCount: dict["reminderCount"] as? Int ?? 0, + isDefault: dict["isDefault"] as? Bool ?? false + ) + } + + switch format { + case .text: + break // handled by caller + case .json: + try outputJSON(data: codableLists, pretty: false) + case .prettyJson: + try outputJSON(data: codableLists, pretty: true) + case .yaml: + try outputYAML(data: codableLists) + } + } + // MARK: - Helper Methods private func priorityIndicator(for priority: Int) -> String { diff --git a/Sources/reminder-cli/ReminderStore.swift b/Sources/reminder-cli/ReminderStore.swift index 9e401b3..d014a70 100644 --- a/Sources/reminder-cli/ReminderStore.swift +++ b/Sources/reminder-cli/ReminderStore.swift @@ -9,6 +9,8 @@ enum ReminderStoreError: LocalizedError { case noAvailableLists case invalidDateFormat(String) case invalidPriority(Int) + case listAlreadyExists(String) + case cannotDeleteList(String) var errorDescription: String? { switch self { @@ -34,6 +36,10 @@ enum ReminderStoreError: LocalizedError { return "Invalid date format: \(format). Use YYYY-MM-DD or YYYY-MM-DD HH:MM" case .invalidPriority(let priority): return "Invalid priority: \(priority). Use 0-9 (0=none, 1-4=high, 5=medium, 6-9=low)" + case .listAlreadyExists(let name): + return "List already exists: \(name)" + case .cannotDeleteList(let name): + return "Failed to delete list: \(name)" } } } @@ -190,6 +196,84 @@ class ReminderStore { } } + // MARK: - Lists Operations + + func showAllLists(format: OutputFormat = .text) async throws { + let calendars = eventStore.calendars(for: .reminder) + + if format == .text { + print("\nšŸ“‹ Reminder Lists") + print("─────────────────────────────────────") + + for calendar in calendars { + let predicate = eventStore.predicateForReminders(in: [calendar]) + let reminders = try await fetchReminders(matching: predicate) + let incompleteCount = reminders.filter { !$0.isCompleted }.count + let isDefault = calendar == eventStore.defaultCalendarForNewReminders() + let defaultMark = isDefault ? " (default)" : "" + print(" \(calendar.title)\(defaultMark) — \(incompleteCount) reminder(s)") + } + print() + } else { + var listOutputs: [[String: Any]] = [] + for calendar in calendars { + let predicate = eventStore.predicateForReminders(in: [calendar]) + let reminders = try await fetchReminders(matching: predicate) + let incompleteCount = reminders.filter { !$0.isCompleted }.count + let isDefault = calendar == eventStore.defaultCalendarForNewReminders() + listOutputs.append([ + "name": calendar.title, + "reminderCount": incompleteCount, + "isDefault": isDefault, + ]) + } + + let formatter = OutputFormatter(format: format) + try formatter.outputLists(listOutputs) + } + } + + func createList(name: String) throws { + if findCalendar(named: name) != nil { + throw ReminderStoreError.listAlreadyExists(name) + } + + let newCalendar = EKCalendar(for: .reminder, eventStore: eventStore) + newCalendar.title = name + + if let defaultCalendar = eventStore.defaultCalendarForNewReminders() { + newCalendar.source = defaultCalendar.source + } else if let source = eventStore.sources.first(where: { $0.sourceType == .calDAV }) { + newCalendar.source = source + } else if let source = eventStore.sources.first(where: { $0.sourceType == .local }) { + newCalendar.source = source + } + + try eventStore.saveCalendar(newCalendar, commit: true) + print("āœ… Created list: \(name)") + } + + func deleteList(name: String, force: Bool) throws { + guard let calendar = findCalendar(named: name) else { + throw ReminderStoreError.listNotFound(name) + } + + if !force { + print("Are you sure you want to delete list '\(calendar.title)' and all its reminders? [y/N]: ", terminator: "") + guard let response = readLine()?.lowercased(), response == "y" || response == "yes" else { + print("Cancelled.") + return + } + } + + do { + try eventStore.removeCalendar(calendar, commit: true) + print("šŸ—‘ļø Deleted list: \(name)") + } catch { + throw ReminderStoreError.cannotDeleteList(name) + } + } + // MARK: - Show Operation func showReminder(identifier: String, format: OutputFormat = .text) async throws { diff --git a/Sources/reminder-cli/reminder_cli.swift b/Sources/reminder-cli/reminder_cli.swift index 1d9453c..4e6a27e 100644 --- a/Sources/reminder-cli/reminder_cli.swift +++ b/Sources/reminder-cli/reminder_cli.swift @@ -41,6 +41,7 @@ struct ReminderCLI: AsyncParsableCommand { version: "1.0.1", subcommands: [ List.self, + Lists.self, Show.self, Create.self, Update.self, @@ -93,6 +94,66 @@ extension ReminderCLI { } } +// MARK: - Lists Command Group +extension ReminderCLI { + struct Lists: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Manage reminder lists", + subcommands: [ListAll.self, Create.self, Delete.self], + defaultSubcommand: ListAll.self + ) + + struct ListAll: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "list", + abstract: "Show all reminder lists" + ) + + @Option(name: .shortAndLong, help: "Output format (text, json, pretty-json, yaml)") + var format: OutputFormat = .text + + mutating func run() async throws { + let store = ReminderStore() + try await store.requestAccess() + try await store.showAllLists(format: format) + } + } + + struct Create: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Create a new reminder list" + ) + + @Argument(help: "The name of the list to create") + var name: String + + mutating func run() async throws { + let store = ReminderStore() + try await store.requestAccess() + try store.createList(name: name) + } + } + + struct Delete: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Delete a reminder list" + ) + + @Argument(help: "The name of the list to delete") + var name: String + + @Flag(name: .shortAndLong, help: "Skip confirmation") + var force: Bool = false + + mutating func run() async throws { + let store = ReminderStore() + try await store.requestAccess() + try store.deleteList(name: name, force: force) + } + } + } +} + // MARK: - Show Command extension ReminderCLI { struct Show: AsyncParsableCommand {