#!/usr/bin/env swift // Fast Reminders query via EventKit — handles large reminder databases // Usage: swift reminders_helper.swift [args...] // lists — list all reminder lists // get [--list NAME] [--overdue] [--due-today] [--due-this-week] [--limit N] // search [--limit N] import EventKit import Foundation let store = EKEventStore() let semaphore = DispatchSemaphore(value: 0) // Request access var accessGranted = false if #available(macOS 14.0, *) { store.requestFullAccessToReminders { granted, error in accessGranted = granted semaphore.signal() } } else { store.requestAccess(to: .reminder) { granted, error in accessGranted = granted semaphore.signal() } } semaphore.wait() guard accessGranted else { let err: [String: Any] = ["error": "Reminders access denied. Grant permission in System Settings > Privacy & Security > Reminders."] print(String(data: try! JSONSerialization.data(withJSONObject: err), encoding: .utf8)!) exit(1) } let args = CommandLine.arguments let command = args.count > 1 ? args[1] : "lists" func jsonDate(_ date: Date?) -> Any { guard let d = date else { return NSNull() } let f = ISO8601DateFormatter() f.formatOptions = [.withInternetDateTime] return f.string(from: d) } func printJSON(_ obj: Any) { let data = try! JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted, .sortedKeys]) print(String(data: data, encoding: .utf8)!) } func getCalendars() -> [EKCalendar] { return store.calendars(for: .reminder) } func argValue(_ flag: String) -> String? { if let idx = args.firstIndex(of: flag), idx + 1 < args.count { return args[idx + 1] } return nil } func hasFlag(_ flag: String) -> Bool { return args.contains(flag) } switch command { case "lists": let cals = getCalendars() let result = cals.map { cal -> [String: Any] in // Count incomplete reminders using a predicate let pred = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: [cal]) var count = 0 let countSem = DispatchSemaphore(value: 0) store.fetchReminders(matching: pred) { reminders in count = reminders?.count ?? 0 countSem.signal() } countSem.wait() return ["name": cal.title, "id": cal.calendarIdentifier, "incomplete_count": count] } printJSON(result) case "get": let listName = argValue("--list") let isOverdue = hasFlag("--overdue") let isDueToday = hasFlag("--due-today") let isDueThisWeek = hasFlag("--due-this-week") let limit = Int(argValue("--limit") ?? "50") ?? 50 var calendars: [EKCalendar]? = nil if let name = listName { calendars = getCalendars().filter { $0.title.lowercased() == name.lowercased() } if calendars?.isEmpty == true { calendars = nil } } let now = Date() let calendar = Calendar.current var startDate: Date? = nil var endDate: Date? = nil if isOverdue { // Reminders due before now endDate = now startDate = calendar.date(byAdding: .year, value: -10, to: now) } else if isDueToday { startDate = calendar.startOfDay(for: now) endDate = calendar.date(byAdding: .day, value: 1, to: startDate!) } else if isDueThisWeek { startDate = calendar.startOfDay(for: now) endDate = calendar.date(byAdding: .day, value: 7, to: startDate!) } let pred = store.predicateForIncompleteReminders( withDueDateStarting: startDate, ending: endDate, calendars: calendars ) let fetchSem = DispatchSemaphore(value: 0) var fetchedReminders: [EKReminder] = [] store.fetchReminders(matching: pred) { reminders in fetchedReminders = reminders ?? [] fetchSem.signal() } fetchSem.wait() // Sort by due date (overdue first, then soonest) fetchedReminders.sort { a, b in let da = a.dueDateComponents?.date ?? Date.distantFuture let db = b.dueDateComponents?.date ?? Date.distantFuture return da < db } let limited = Array(fetchedReminders.prefix(limit)) let result = limited.map { r -> [String: Any] in let dueComps = r.dueDateComponents let dueDate: Date? = dueComps?.date return [ "name": r.title ?? "Untitled", "list": r.calendar.title, "due_date": jsonDate(dueDate), "priority": r.priority, "completed": r.isCompleted, "has_notes": (r.notes != nil && !r.notes!.isEmpty), "id": r.calendarItemIdentifier ] } printJSON(["count": fetchedReminders.count, "showing": limited.count, "reminders": result]) case "search": let query = args.count > 2 ? args[2].lowercased() : "" let limit = Int(argValue("--limit") ?? "50") ?? 50 guard !query.isEmpty else { printJSON(["error": "Search query required"]) exit(1) } let pred = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: nil) let fetchSem = DispatchSemaphore(value: 0) var fetchedReminders: [EKReminder] = [] store.fetchReminders(matching: pred) { reminders in fetchedReminders = (reminders ?? []).filter { ($0.title ?? "").lowercased().contains(query) } fetchSem.signal() } fetchSem.wait() let limited = Array(fetchedReminders.prefix(limit)) let result = limited.map { r -> [String: Any] in let dueComps = r.dueDateComponents let dueDate: Date? = dueComps?.date return [ "name": r.title ?? "Untitled", "list": r.calendar.title, "due_date": jsonDate(dueDate), "completed": r.isCompleted, "id": r.calendarItemIdentifier ] } printJSON(["count": fetchedReminders.count, "showing": limited.count, "reminders": result]) default: printJSON(["error": "Unknown command: \(command). Use: lists, get, search"]) exit(1) }