@mattheworiordan/remi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/cli/commands/add.d.ts +7 -0
- package/dist/cli/commands/add.js +36 -0
- package/dist/cli/commands/add.js.map +1 -0
- package/dist/cli/commands/authorize.d.ts +1 -0
- package/dist/cli/commands/authorize.js +75 -0
- package/dist/cli/commands/authorize.js.map +1 -0
- package/dist/cli/commands/complete.d.ts +3 -0
- package/dist/cli/commands/complete.js +9 -0
- package/dist/cli/commands/complete.js.map +1 -0
- package/dist/cli/commands/create-list.d.ts +1 -0
- package/dist/cli/commands/create-list.js +7 -0
- package/dist/cli/commands/create-list.js.map +1 -0
- package/dist/cli/commands/create-section.d.ts +1 -0
- package/dist/cli/commands/create-section.js +7 -0
- package/dist/cli/commands/create-section.js.map +1 -0
- package/dist/cli/commands/delete-list.d.ts +3 -0
- package/dist/cli/commands/delete-list.js +11 -0
- package/dist/cli/commands/delete-list.js.map +1 -0
- package/dist/cli/commands/delete-section.d.ts +1 -0
- package/dist/cli/commands/delete-section.js +7 -0
- package/dist/cli/commands/delete-section.js.map +1 -0
- package/dist/cli/commands/delete.d.ts +4 -0
- package/dist/cli/commands/delete.js +14 -0
- package/dist/cli/commands/delete.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +4 -0
- package/dist/cli/commands/doctor.js +180 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/list.d.ts +4 -0
- package/dist/cli/commands/list.js +11 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/lists.d.ts +1 -0
- package/dist/cli/commands/lists.js +7 -0
- package/dist/cli/commands/lists.js.map +1 -0
- package/dist/cli/commands/move.d.ts +3 -0
- package/dist/cli/commands/move.js +11 -0
- package/dist/cli/commands/move.js.map +1 -0
- package/dist/cli/commands/overdue.d.ts +1 -0
- package/dist/cli/commands/overdue.js +11 -0
- package/dist/cli/commands/overdue.js.map +1 -0
- package/dist/cli/commands/search.d.ts +1 -0
- package/dist/cli/commands/search.js +11 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/sections.d.ts +1 -0
- package/dist/cli/commands/sections.js +7 -0
- package/dist/cli/commands/sections.js.map +1 -0
- package/dist/cli/commands/today.d.ts +1 -0
- package/dist/cli/commands/today.js +11 -0
- package/dist/cli/commands/today.js.map +1 -0
- package/dist/cli/commands/upcoming.d.ts +3 -0
- package/dist/cli/commands/upcoming.js +12 -0
- package/dist/cli/commands/upcoming.js.map +1 -0
- package/dist/cli/commands/update.d.ts +7 -0
- package/dist/cli/commands/update.js +17 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +232 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.d.ts +26 -0
- package/dist/cli/output.js +234 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/core/checksum.d.ts +9 -0
- package/dist/core/checksum.js +13 -0
- package/dist/core/checksum.js.map +1 -0
- package/dist/core/dateparse.d.ts +7 -0
- package/dist/core/dateparse.js +27 -0
- package/dist/core/dateparse.js.map +1 -0
- package/dist/core/errors.d.ts +26 -0
- package/dist/core/errors.js +34 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/eventkit.d.ts +38 -0
- package/dist/core/eventkit.js +127 -0
- package/dist/core/eventkit.js.map +1 -0
- package/dist/core/lookup.d.ts +10 -0
- package/dist/core/lookup.js +32 -0
- package/dist/core/lookup.js.map +1 -0
- package/dist/core/membership.d.ts +30 -0
- package/dist/core/membership.js +92 -0
- package/dist/core/membership.js.map +1 -0
- package/dist/core/recurrence.d.ts +14 -0
- package/dist/core/recurrence.js +90 -0
- package/dist/core/recurrence.js.map +1 -0
- package/dist/core/reminderkit.d.ts +33 -0
- package/dist/core/reminderkit.js +154 -0
- package/dist/core/reminderkit.js.map +1 -0
- package/dist/core/sqlite.d.ts +65 -0
- package/dist/core/sqlite.js +175 -0
- package/dist/core/sqlite.js.map +1 -0
- package/dist/core/tokenmap.d.ts +29 -0
- package/dist/core/tokenmap.js +52 -0
- package/dist/core/tokenmap.js.map +1 -0
- package/dist/reminders-helper +0 -0
- package/dist/section-helper +0 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
- package/src/swift/Info.plist +16 -0
- package/src/swift/build.sh +47 -0
- package/src/swift/reminders-helper.swift +697 -0
- package/src/swift/section-helper.swift +794 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
#!/usr/bin/env swift
|
|
2
|
+
/**
|
|
3
|
+
* remi - Apple Reminders EventKit helper
|
|
4
|
+
*
|
|
5
|
+
* Usage: swift reminders-helper.swift <command> [json-args]
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* list-lists List all reminder lists
|
|
9
|
+
* get-reminders <json> Get reminders with filtering
|
|
10
|
+
* search <json> Search reminders across all lists
|
|
11
|
+
* create <json> Create a reminder
|
|
12
|
+
* edit <json> Edit an existing reminder
|
|
13
|
+
* complete <json> Mark reminder complete
|
|
14
|
+
* delete <json> Delete a reminder
|
|
15
|
+
* create-list <json> Create a new reminder list
|
|
16
|
+
* delete-list <json> Delete a reminder list
|
|
17
|
+
*
|
|
18
|
+
* All output is JSON.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import EventKit
|
|
22
|
+
import Foundation
|
|
23
|
+
|
|
24
|
+
// MARK: - Types
|
|
25
|
+
|
|
26
|
+
struct ReminderList: Encodable {
|
|
27
|
+
let id: String
|
|
28
|
+
let title: String
|
|
29
|
+
let reminderCount: Int
|
|
30
|
+
let overdueCount: Int
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
struct ReminderItem: Encodable {
|
|
34
|
+
let id: String
|
|
35
|
+
let title: String
|
|
36
|
+
let isCompleted: Bool
|
|
37
|
+
let listID: String
|
|
38
|
+
let listName: String
|
|
39
|
+
let priority: String
|
|
40
|
+
let dueDate: String?
|
|
41
|
+
let completionDate: String?
|
|
42
|
+
let notes: String?
|
|
43
|
+
let isRecurring: Bool
|
|
44
|
+
let recurrence: String? // Human-readable recurrence description, e.g. "every 2 weeks"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
struct ResultResponse: Encodable {
|
|
48
|
+
let success: Bool
|
|
49
|
+
let data: AnyCodable?
|
|
50
|
+
let error: String?
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
struct AnyCodable: Encodable {
|
|
54
|
+
let value: Any
|
|
55
|
+
|
|
56
|
+
func encode(to encoder: Encoder) throws {
|
|
57
|
+
var container = encoder.singleValueContainer()
|
|
58
|
+
if let arr = value as? [Encodable] {
|
|
59
|
+
try container.encode(arr.map { AnyEncodableWrapper($0) })
|
|
60
|
+
} else if let str = value as? String {
|
|
61
|
+
try container.encode(str)
|
|
62
|
+
} else if let bool = value as? Bool {
|
|
63
|
+
try container.encode(bool)
|
|
64
|
+
} else if let int = value as? Int {
|
|
65
|
+
try container.encode(int)
|
|
66
|
+
} else {
|
|
67
|
+
try container.encodeNil()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
struct AnyEncodableWrapper: Encodable {
|
|
73
|
+
let wrapped: Encodable
|
|
74
|
+
init(_ wrapped: Encodable) { self.wrapped = wrapped }
|
|
75
|
+
func encode(to encoder: Encoder) throws { try wrapped.encode(to: encoder) }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// MARK: - Helpers
|
|
79
|
+
|
|
80
|
+
let dateFormatter: DateFormatter = {
|
|
81
|
+
let f = DateFormatter()
|
|
82
|
+
f.dateFormat = "yyyy-MM-dd"
|
|
83
|
+
f.timeZone = TimeZone.current
|
|
84
|
+
return f
|
|
85
|
+
}()
|
|
86
|
+
|
|
87
|
+
let isoFormatter: ISO8601DateFormatter = {
|
|
88
|
+
let f = ISO8601DateFormatter()
|
|
89
|
+
f.formatOptions = [.withInternetDateTime]
|
|
90
|
+
return f
|
|
91
|
+
}()
|
|
92
|
+
|
|
93
|
+
func priorityString(_ p: Int) -> String {
|
|
94
|
+
switch p {
|
|
95
|
+
case 1...4: return "high"
|
|
96
|
+
case 5: return "medium"
|
|
97
|
+
case 6...9: return "low"
|
|
98
|
+
default: return "none"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func priorityInt(_ s: String) -> Int {
|
|
103
|
+
switch s {
|
|
104
|
+
case "high": return 1
|
|
105
|
+
case "medium": return 5
|
|
106
|
+
case "low": return 9
|
|
107
|
+
default: return 0
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func output(_ result: ResultResponse) {
|
|
112
|
+
let encoder = JSONEncoder()
|
|
113
|
+
encoder.outputFormatting = .sortedKeys
|
|
114
|
+
if let data = try? encoder.encode(result), let json = String(data: data, encoding: .utf8) {
|
|
115
|
+
print(json)
|
|
116
|
+
} else {
|
|
117
|
+
print("{\"success\":false,\"error\":\"Failed to encode result\"}")
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func outputList(_ lists: [ReminderList]) {
|
|
122
|
+
let encoder = JSONEncoder()
|
|
123
|
+
encoder.outputFormatting = .sortedKeys
|
|
124
|
+
if let data = try? encoder.encode(lists), let json = String(data: data, encoding: .utf8) {
|
|
125
|
+
print(json)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func outputReminders(_ items: [ReminderItem]) {
|
|
130
|
+
let encoder = JSONEncoder()
|
|
131
|
+
encoder.outputFormatting = .sortedKeys
|
|
132
|
+
if let data = try? encoder.encode(items), let json = String(data: data, encoding: .utf8) {
|
|
133
|
+
print(json)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func reminderToItem(_ r: EKReminder) -> ReminderItem {
|
|
138
|
+
var dueDateStr: String? = nil
|
|
139
|
+
if let due = r.dueDateComponents, let year = due.year, let month = due.month, let day = due.day {
|
|
140
|
+
dueDateStr = String(format: "%04d-%02d-%02d", year, month, day)
|
|
141
|
+
}
|
|
142
|
+
var completionStr: String? = nil
|
|
143
|
+
if let cd = r.completionDate {
|
|
144
|
+
completionStr = isoFormatter.string(from: cd)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Build human-readable recurrence description
|
|
148
|
+
var recurrenceStr: String? = nil
|
|
149
|
+
if let rules = r.recurrenceRules, let rule = rules.first {
|
|
150
|
+
let freqName: String
|
|
151
|
+
switch rule.frequency {
|
|
152
|
+
case .daily: freqName = "day"
|
|
153
|
+
case .weekly: freqName = "week"
|
|
154
|
+
case .monthly: freqName = "month"
|
|
155
|
+
case .yearly: freqName = "year"
|
|
156
|
+
@unknown default: freqName = "?"
|
|
157
|
+
}
|
|
158
|
+
if rule.interval == 1 {
|
|
159
|
+
// "daily", "weekly", "monthly", "yearly"
|
|
160
|
+
recurrenceStr = freqName == "day" ? "daily" : freqName + "ly"
|
|
161
|
+
} else {
|
|
162
|
+
recurrenceStr = "every \(rule.interval) \(freqName)s"
|
|
163
|
+
}
|
|
164
|
+
// Add day-of-week info for weekly rules
|
|
165
|
+
if rule.frequency == .weekly, let days = rule.daysOfTheWeek, !days.isEmpty {
|
|
166
|
+
let dayNames = days.map { d -> String in
|
|
167
|
+
switch d.dayOfTheWeek {
|
|
168
|
+
case .sunday: return "Sun"
|
|
169
|
+
case .monday: return "Mon"
|
|
170
|
+
case .tuesday: return "Tue"
|
|
171
|
+
case .wednesday: return "Wed"
|
|
172
|
+
case .thursday: return "Thu"
|
|
173
|
+
case .friday: return "Fri"
|
|
174
|
+
case .saturday: return "Sat"
|
|
175
|
+
@unknown default: return "?"
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
recurrenceStr! += " on " + dayNames.joined(separator: ", ")
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return ReminderItem(
|
|
183
|
+
id: r.calendarItemIdentifier,
|
|
184
|
+
title: r.title ?? "",
|
|
185
|
+
isCompleted: r.isCompleted,
|
|
186
|
+
listID: r.calendar.calendarIdentifier,
|
|
187
|
+
listName: r.calendar.title,
|
|
188
|
+
priority: priorityString(Int(r.priority)),
|
|
189
|
+
dueDate: dueDateStr,
|
|
190
|
+
completionDate: completionStr,
|
|
191
|
+
notes: r.notes,
|
|
192
|
+
isRecurring: (r.recurrenceRules ?? []).count > 0,
|
|
193
|
+
recurrence: recurrenceStr
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// MARK: - Commands
|
|
198
|
+
|
|
199
|
+
func listLists(_ store: EKEventStore) {
|
|
200
|
+
let calendars = store.calendars(for: .reminder)
|
|
201
|
+
var results: [ReminderList] = []
|
|
202
|
+
let group = DispatchGroup()
|
|
203
|
+
|
|
204
|
+
for cal in calendars {
|
|
205
|
+
group.enter()
|
|
206
|
+
let pred = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: [cal])
|
|
207
|
+
store.fetchReminders(matching: pred) { reminders in
|
|
208
|
+
let items = reminders ?? []
|
|
209
|
+
let now = Date()
|
|
210
|
+
let overdue = items.filter { r in
|
|
211
|
+
guard let due = r.dueDateComponents,
|
|
212
|
+
let year = due.year, let month = due.month, let day = due.day else { return false }
|
|
213
|
+
var comps = DateComponents()
|
|
214
|
+
comps.year = year; comps.month = month; comps.day = day
|
|
215
|
+
guard let dueDate = Calendar.current.date(from: comps) else { return false }
|
|
216
|
+
return dueDate < now
|
|
217
|
+
}.count
|
|
218
|
+
|
|
219
|
+
results.append(ReminderList(
|
|
220
|
+
id: cal.calendarIdentifier,
|
|
221
|
+
title: cal.title,
|
|
222
|
+
reminderCount: items.count,
|
|
223
|
+
overdueCount: overdue
|
|
224
|
+
))
|
|
225
|
+
group.leave()
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
group.wait()
|
|
230
|
+
outputList(results.sorted { $0.title < $1.title })
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
struct GetRemindersArgs: Decodable {
|
|
234
|
+
let filter: String?
|
|
235
|
+
let list: String?
|
|
236
|
+
let days: Int?
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
func getReminders(_ store: EKEventStore, _ args: GetRemindersArgs) {
|
|
240
|
+
let filter = args.filter ?? "upcoming"
|
|
241
|
+
let calendars: [EKCalendar]
|
|
242
|
+
|
|
243
|
+
if let listName = args.list {
|
|
244
|
+
calendars = store.calendars(for: .reminder).filter { $0.title == listName }
|
|
245
|
+
if calendars.isEmpty {
|
|
246
|
+
output(ResultResponse(success: false, data: nil, error: "List '\(listName)' not found"))
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
calendars = store.calendars(for: .reminder)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let sem = DispatchSemaphore(value: 0)
|
|
254
|
+
let pred: NSPredicate
|
|
255
|
+
|
|
256
|
+
switch filter {
|
|
257
|
+
case "completed":
|
|
258
|
+
pred = store.predicateForCompletedReminders(withCompletionDateStarting: nil, ending: nil, calendars: calendars)
|
|
259
|
+
case "all":
|
|
260
|
+
let pred1 = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: calendars)
|
|
261
|
+
let pred2 = store.predicateForCompletedReminders(withCompletionDateStarting: nil, ending: nil, calendars: calendars)
|
|
262
|
+
var allItems: [ReminderItem] = []
|
|
263
|
+
let group = DispatchGroup()
|
|
264
|
+
|
|
265
|
+
group.enter()
|
|
266
|
+
store.fetchReminders(matching: pred1) { reminders in
|
|
267
|
+
allItems.append(contentsOf: (reminders ?? []).map(reminderToItem))
|
|
268
|
+
group.leave()
|
|
269
|
+
}
|
|
270
|
+
group.enter()
|
|
271
|
+
store.fetchReminders(matching: pred2) { reminders in
|
|
272
|
+
allItems.append(contentsOf: (reminders ?? []).map(reminderToItem))
|
|
273
|
+
group.leave()
|
|
274
|
+
}
|
|
275
|
+
group.wait()
|
|
276
|
+
outputReminders(allItems)
|
|
277
|
+
return
|
|
278
|
+
default:
|
|
279
|
+
pred = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: calendars)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
store.fetchReminders(matching: pred) { reminders in
|
|
283
|
+
var items = (reminders ?? []).map(reminderToItem)
|
|
284
|
+
|
|
285
|
+
let cal = Calendar.current
|
|
286
|
+
let now = Date()
|
|
287
|
+
|
|
288
|
+
switch filter {
|
|
289
|
+
case "today":
|
|
290
|
+
items = items.filter { item in
|
|
291
|
+
guard let ds = item.dueDate, let d = dateFormatter.date(from: ds) else { return false }
|
|
292
|
+
return cal.isDateInToday(d)
|
|
293
|
+
}
|
|
294
|
+
case "tomorrow":
|
|
295
|
+
items = items.filter { item in
|
|
296
|
+
guard let ds = item.dueDate, let d = dateFormatter.date(from: ds) else { return false }
|
|
297
|
+
return cal.isDateInTomorrow(d)
|
|
298
|
+
}
|
|
299
|
+
case "week":
|
|
300
|
+
let weekEnd = cal.date(byAdding: .day, value: 7, to: now)!
|
|
301
|
+
items = items.filter { item in
|
|
302
|
+
guard let ds = item.dueDate, let d = dateFormatter.date(from: ds) else { return false }
|
|
303
|
+
return d <= weekEnd
|
|
304
|
+
}
|
|
305
|
+
case "upcoming":
|
|
306
|
+
let days = args.days ?? 7
|
|
307
|
+
let upcomingEnd = cal.date(byAdding: .day, value: days, to: now)!
|
|
308
|
+
items = items.filter { item in
|
|
309
|
+
guard let ds = item.dueDate, let d = dateFormatter.date(from: ds) else { return false }
|
|
310
|
+
return d <= upcomingEnd
|
|
311
|
+
}
|
|
312
|
+
case "overdue":
|
|
313
|
+
let startOfToday = cal.startOfDay(for: now)
|
|
314
|
+
items = items.filter { item in
|
|
315
|
+
guard let ds = item.dueDate, let d = dateFormatter.date(from: ds) else { return false }
|
|
316
|
+
return d < startOfToday
|
|
317
|
+
}
|
|
318
|
+
default:
|
|
319
|
+
// Try parsing as date
|
|
320
|
+
if let targetDate = dateFormatter.date(from: filter) {
|
|
321
|
+
items = items.filter { item in
|
|
322
|
+
guard let ds = item.dueDate, let d = dateFormatter.date(from: ds) else { return false }
|
|
323
|
+
return cal.isDate(d, inSameDayAs: targetDate)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
outputReminders(items)
|
|
329
|
+
sem.signal()
|
|
330
|
+
}
|
|
331
|
+
sem.wait()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
struct SearchArgs: Decodable {
|
|
335
|
+
let query: String
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
func searchReminders(_ store: EKEventStore, _ args: SearchArgs) {
|
|
339
|
+
let query = args.query.lowercased()
|
|
340
|
+
let calendars = store.calendars(for: .reminder)
|
|
341
|
+
|
|
342
|
+
let pred = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: calendars)
|
|
343
|
+
let sem = DispatchSemaphore(value: 0)
|
|
344
|
+
|
|
345
|
+
store.fetchReminders(matching: pred) { reminders in
|
|
346
|
+
let items = (reminders ?? [])
|
|
347
|
+
.map(reminderToItem)
|
|
348
|
+
.filter { item in
|
|
349
|
+
item.title.lowercased().contains(query) ||
|
|
350
|
+
(item.notes?.lowercased().contains(query) ?? false)
|
|
351
|
+
}
|
|
352
|
+
outputReminders(items)
|
|
353
|
+
sem.signal()
|
|
354
|
+
}
|
|
355
|
+
sem.wait()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
struct CreateArgs: Decodable {
|
|
359
|
+
let title: String
|
|
360
|
+
let listName: String
|
|
361
|
+
let due: String?
|
|
362
|
+
let notes: String?
|
|
363
|
+
let priority: String?
|
|
364
|
+
// Recurrence (optional)
|
|
365
|
+
let rruleFreq: String? // DAILY, WEEKLY, MONTHLY, YEARLY
|
|
366
|
+
let rruleInterval: Int? // defaults to 1
|
|
367
|
+
let rruleDays: [Int]? // Days of week: 1=Sun, 2=Mon, 3=Tue, 4=Wed, 5=Thu, 6=Fri, 7=Sat
|
|
368
|
+
let rruleEnd: String? // End date for recurrence (YYYY-MM-DD)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
func createReminder(_ store: EKEventStore, _ args: CreateArgs) {
|
|
372
|
+
let calendars = store.calendars(for: .reminder).filter { $0.title == args.listName }
|
|
373
|
+
guard let cal = calendars.first else {
|
|
374
|
+
output(ResultResponse(success: false, data: nil, error: "List '\(args.listName)' not found"))
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let reminder = EKReminder(eventStore: store)
|
|
379
|
+
reminder.title = args.title
|
|
380
|
+
reminder.calendar = cal
|
|
381
|
+
|
|
382
|
+
if let notes = args.notes { reminder.notes = notes }
|
|
383
|
+
if let p = args.priority { reminder.priority = Int(priorityInt(p)) }
|
|
384
|
+
|
|
385
|
+
if let dueStr = args.due, let dueDate = dateFormatter.date(from: dueStr) {
|
|
386
|
+
let comps = Calendar.current.dateComponents([.year, .month, .day], from: dueDate)
|
|
387
|
+
reminder.dueDateComponents = comps
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Set recurrence if specified
|
|
391
|
+
if let freqStr = args.rruleFreq {
|
|
392
|
+
let freq: EKRecurrenceFrequency
|
|
393
|
+
switch freqStr.uppercased() {
|
|
394
|
+
case "DAILY": freq = .daily
|
|
395
|
+
case "WEEKLY": freq = .weekly
|
|
396
|
+
case "MONTHLY": freq = .monthly
|
|
397
|
+
case "YEARLY": freq = .yearly
|
|
398
|
+
default:
|
|
399
|
+
output(ResultResponse(success: false, data: nil, error: "Invalid frequency: \(freqStr). Use: daily, weekly, monthly, yearly"))
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
let interval = args.rruleInterval ?? 1
|
|
403
|
+
|
|
404
|
+
var daysOfWeek: [EKRecurrenceDayOfWeek]? = nil
|
|
405
|
+
if let days = args.rruleDays, !days.isEmpty {
|
|
406
|
+
daysOfWeek = days.compactMap { dayNum -> EKRecurrenceDayOfWeek? in
|
|
407
|
+
guard let weekday = EKWeekday(rawValue: dayNum) else { return nil }
|
|
408
|
+
return EKRecurrenceDayOfWeek(weekday)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
var recurrenceEnd: EKRecurrenceEnd? = nil
|
|
413
|
+
if let endStr = args.rruleEnd, let endDate = dateFormatter.date(from: endStr) {
|
|
414
|
+
recurrenceEnd = EKRecurrenceEnd(end: endDate)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let rule = EKRecurrenceRule(
|
|
418
|
+
recurrenceWith: freq,
|
|
419
|
+
interval: interval,
|
|
420
|
+
daysOfTheWeek: daysOfWeek,
|
|
421
|
+
daysOfTheMonth: nil,
|
|
422
|
+
monthsOfTheYear: nil,
|
|
423
|
+
weeksOfTheYear: nil,
|
|
424
|
+
daysOfTheYear: nil,
|
|
425
|
+
setPositions: nil,
|
|
426
|
+
end: recurrenceEnd
|
|
427
|
+
)
|
|
428
|
+
reminder.addRecurrenceRule(rule)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
do {
|
|
432
|
+
try store.save(reminder, commit: true)
|
|
433
|
+
output(ResultResponse(success: true, data: AnyCodable(value: reminder.calendarItemIdentifier), error: nil))
|
|
434
|
+
} catch {
|
|
435
|
+
output(ResultResponse(success: false, data: nil, error: "Save failed: \(error.localizedDescription)"))
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
struct EditArgs: Decodable {
|
|
440
|
+
let id: String
|
|
441
|
+
let title: String?
|
|
442
|
+
let listName: String?
|
|
443
|
+
let due: String?
|
|
444
|
+
let clearDue: Bool?
|
|
445
|
+
let notes: String?
|
|
446
|
+
let priority: String?
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
func editReminder(_ store: EKEventStore, _ args: EditArgs) {
|
|
450
|
+
let pred = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: nil)
|
|
451
|
+
let sem = DispatchSemaphore(value: 0)
|
|
452
|
+
|
|
453
|
+
store.fetchReminders(matching: pred) { reminders in
|
|
454
|
+
guard let reminder = (reminders ?? []).first(where: {
|
|
455
|
+
$0.calendarItemIdentifier.uppercased().hasPrefix(args.id.uppercased())
|
|
456
|
+
}) else {
|
|
457
|
+
output(ResultResponse(success: false, data: nil, error: "Reminder '\(args.id)' not found"))
|
|
458
|
+
sem.signal()
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if let title = args.title { reminder.title = title }
|
|
463
|
+
if let notes = args.notes { reminder.notes = notes }
|
|
464
|
+
if let p = args.priority { reminder.priority = Int(priorityInt(p)) }
|
|
465
|
+
|
|
466
|
+
if args.clearDue == true {
|
|
467
|
+
reminder.dueDateComponents = nil
|
|
468
|
+
} else if let dueStr = args.due, let dueDate = dateFormatter.date(from: dueStr) {
|
|
469
|
+
let comps = Calendar.current.dateComponents([.year, .month, .day], from: dueDate)
|
|
470
|
+
reminder.dueDateComponents = comps
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if let listName = args.listName {
|
|
474
|
+
let cals = store.calendars(for: .reminder).filter { $0.title == listName }
|
|
475
|
+
if let cal = cals.first {
|
|
476
|
+
reminder.calendar = cal
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
do {
|
|
481
|
+
try store.save(reminder, commit: true)
|
|
482
|
+
output(ResultResponse(success: true, data: AnyCodable(value: reminder.title ?? ""), error: nil))
|
|
483
|
+
} catch {
|
|
484
|
+
output(ResultResponse(success: false, data: nil, error: "Save failed: \(error.localizedDescription)"))
|
|
485
|
+
}
|
|
486
|
+
sem.signal()
|
|
487
|
+
}
|
|
488
|
+
sem.wait()
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
struct IdArgs: Decodable {
|
|
492
|
+
let id: String
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
func completeReminder(_ store: EKEventStore, _ args: IdArgs) {
|
|
496
|
+
let pred = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: nil)
|
|
497
|
+
let sem = DispatchSemaphore(value: 0)
|
|
498
|
+
|
|
499
|
+
store.fetchReminders(matching: pred) { reminders in
|
|
500
|
+
guard let reminder = (reminders ?? []).first(where: {
|
|
501
|
+
$0.calendarItemIdentifier.uppercased().hasPrefix(args.id.uppercased())
|
|
502
|
+
}) else {
|
|
503
|
+
output(ResultResponse(success: false, data: nil, error: "Reminder '\(args.id)' not found"))
|
|
504
|
+
sem.signal()
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
reminder.isCompleted = true
|
|
509
|
+
reminder.completionDate = Date()
|
|
510
|
+
|
|
511
|
+
do {
|
|
512
|
+
try store.save(reminder, commit: true)
|
|
513
|
+
output(ResultResponse(success: true, data: AnyCodable(value: reminder.title ?? ""), error: nil))
|
|
514
|
+
} catch {
|
|
515
|
+
output(ResultResponse(success: false, data: nil, error: "Complete failed: \(error.localizedDescription)"))
|
|
516
|
+
}
|
|
517
|
+
sem.signal()
|
|
518
|
+
}
|
|
519
|
+
sem.wait()
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
func deleteReminder(_ store: EKEventStore, _ args: IdArgs) {
|
|
523
|
+
let pred = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: nil)
|
|
524
|
+
let pred2 = store.predicateForCompletedReminders(withCompletionDateStarting: nil, ending: nil, calendars: nil)
|
|
525
|
+
let group = DispatchGroup()
|
|
526
|
+
var allReminders: [EKReminder] = []
|
|
527
|
+
|
|
528
|
+
group.enter()
|
|
529
|
+
store.fetchReminders(matching: pred) { r in allReminders.append(contentsOf: r ?? []); group.leave() }
|
|
530
|
+
group.enter()
|
|
531
|
+
store.fetchReminders(matching: pred2) { r in allReminders.append(contentsOf: r ?? []); group.leave() }
|
|
532
|
+
group.wait()
|
|
533
|
+
|
|
534
|
+
guard let reminder = allReminders.first(where: {
|
|
535
|
+
$0.calendarItemIdentifier.uppercased().hasPrefix(args.id.uppercased())
|
|
536
|
+
}) else {
|
|
537
|
+
output(ResultResponse(success: false, data: nil, error: "Reminder '\(args.id)' not found"))
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
let title = reminder.title ?? ""
|
|
542
|
+
do {
|
|
543
|
+
try store.remove(reminder, commit: true)
|
|
544
|
+
output(ResultResponse(success: true, data: AnyCodable(value: title), error: nil))
|
|
545
|
+
} catch {
|
|
546
|
+
output(ResultResponse(success: false, data: nil, error: "Delete failed: \(error.localizedDescription)"))
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
struct CreateListArgs: Decodable {
|
|
551
|
+
let name: String
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
func createList(_ store: EKEventStore, _ args: CreateListArgs) {
|
|
555
|
+
// Check if list already exists (idempotent)
|
|
556
|
+
let existing = store.calendars(for: .reminder).filter { $0.title == args.name }
|
|
557
|
+
if let cal = existing.first {
|
|
558
|
+
output(ResultResponse(success: true, data: AnyCodable(value: cal.calendarIdentifier), error: nil))
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
let cal = EKCalendar(for: .reminder, eventStore: store)
|
|
563
|
+
cal.title = args.name
|
|
564
|
+
|
|
565
|
+
// Use the default reminder source (iCloud if available)
|
|
566
|
+
if let defaultCal = store.defaultCalendarForNewReminders() {
|
|
567
|
+
cal.source = defaultCal.source
|
|
568
|
+
} else {
|
|
569
|
+
output(ResultResponse(success: false, data: nil, error: "No reminder source available"))
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
do {
|
|
574
|
+
try store.saveCalendar(cal, commit: true)
|
|
575
|
+
output(ResultResponse(success: true, data: AnyCodable(value: cal.calendarIdentifier), error: nil))
|
|
576
|
+
} catch {
|
|
577
|
+
output(ResultResponse(success: false, data: nil, error: "Create list failed: \(error.localizedDescription)"))
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
struct DeleteListArgs: Decodable {
|
|
582
|
+
let name: String
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
func deleteList(_ store: EKEventStore, _ args: DeleteListArgs) {
|
|
586
|
+
let calendars = store.calendars(for: .reminder).filter { $0.title == args.name }
|
|
587
|
+
guard let cal = calendars.first else {
|
|
588
|
+
output(ResultResponse(success: false, data: nil, error: "List '\(args.name)' not found"))
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
do {
|
|
593
|
+
try store.removeCalendar(cal, commit: true)
|
|
594
|
+
output(ResultResponse(success: true, data: AnyCodable(value: args.name), error: nil))
|
|
595
|
+
} catch {
|
|
596
|
+
output(ResultResponse(success: false, data: nil, error: "Delete list failed: \(error.localizedDescription)"))
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// MARK: - Main
|
|
601
|
+
|
|
602
|
+
let store = EKEventStore()
|
|
603
|
+
let sem = DispatchSemaphore(value: 0)
|
|
604
|
+
|
|
605
|
+
store.requestFullAccessToReminders { granted, error in
|
|
606
|
+
guard granted else {
|
|
607
|
+
output(ResultResponse(success: false, data: nil, error: "Reminders access denied. Grant in System Settings > Privacy & Security > Reminders."))
|
|
608
|
+
sem.signal()
|
|
609
|
+
return
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
let args = CommandLine.arguments
|
|
613
|
+
guard args.count >= 2 else {
|
|
614
|
+
output(ResultResponse(success: false, data: nil, error: "Usage: reminders-helper.swift <command> [json-args]"))
|
|
615
|
+
sem.signal()
|
|
616
|
+
return
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
let command = args[1]
|
|
620
|
+
let jsonArg = args.count > 2 ? args[2] : "{}"
|
|
621
|
+
|
|
622
|
+
switch command {
|
|
623
|
+
case "list-lists":
|
|
624
|
+
listLists(store)
|
|
625
|
+
|
|
626
|
+
case "get-reminders":
|
|
627
|
+
guard let data = jsonArg.data(using: .utf8),
|
|
628
|
+
let parsed = try? JSONDecoder().decode(GetRemindersArgs.self, from: data) else {
|
|
629
|
+
output(ResultResponse(success: false, data: nil, error: "Invalid JSON for get-reminders"))
|
|
630
|
+
break
|
|
631
|
+
}
|
|
632
|
+
getReminders(store, parsed)
|
|
633
|
+
|
|
634
|
+
case "search":
|
|
635
|
+
guard let data = jsonArg.data(using: .utf8),
|
|
636
|
+
let parsed = try? JSONDecoder().decode(SearchArgs.self, from: data) else {
|
|
637
|
+
output(ResultResponse(success: false, data: nil, error: "Invalid JSON for search"))
|
|
638
|
+
break
|
|
639
|
+
}
|
|
640
|
+
searchReminders(store, parsed)
|
|
641
|
+
|
|
642
|
+
case "create":
|
|
643
|
+
guard let data = jsonArg.data(using: .utf8),
|
|
644
|
+
let parsed = try? JSONDecoder().decode(CreateArgs.self, from: data) else {
|
|
645
|
+
output(ResultResponse(success: false, data: nil, error: "Invalid JSON for create"))
|
|
646
|
+
break
|
|
647
|
+
}
|
|
648
|
+
createReminder(store, parsed)
|
|
649
|
+
|
|
650
|
+
case "edit":
|
|
651
|
+
guard let data = jsonArg.data(using: .utf8),
|
|
652
|
+
let parsed = try? JSONDecoder().decode(EditArgs.self, from: data) else {
|
|
653
|
+
output(ResultResponse(success: false, data: nil, error: "Invalid JSON for edit"))
|
|
654
|
+
break
|
|
655
|
+
}
|
|
656
|
+
editReminder(store, parsed)
|
|
657
|
+
|
|
658
|
+
case "complete":
|
|
659
|
+
guard let data = jsonArg.data(using: .utf8),
|
|
660
|
+
let parsed = try? JSONDecoder().decode(IdArgs.self, from: data) else {
|
|
661
|
+
output(ResultResponse(success: false, data: nil, error: "Invalid JSON for complete"))
|
|
662
|
+
break
|
|
663
|
+
}
|
|
664
|
+
completeReminder(store, parsed)
|
|
665
|
+
|
|
666
|
+
case "delete":
|
|
667
|
+
guard let data = jsonArg.data(using: .utf8),
|
|
668
|
+
let parsed = try? JSONDecoder().decode(IdArgs.self, from: data) else {
|
|
669
|
+
output(ResultResponse(success: false, data: nil, error: "Invalid JSON for delete"))
|
|
670
|
+
break
|
|
671
|
+
}
|
|
672
|
+
deleteReminder(store, parsed)
|
|
673
|
+
|
|
674
|
+
case "create-list":
|
|
675
|
+
guard let data = jsonArg.data(using: .utf8),
|
|
676
|
+
let parsed = try? JSONDecoder().decode(CreateListArgs.self, from: data) else {
|
|
677
|
+
output(ResultResponse(success: false, data: nil, error: "Invalid JSON for create-list"))
|
|
678
|
+
break
|
|
679
|
+
}
|
|
680
|
+
createList(store, parsed)
|
|
681
|
+
|
|
682
|
+
case "delete-list":
|
|
683
|
+
guard let data = jsonArg.data(using: .utf8),
|
|
684
|
+
let parsed = try? JSONDecoder().decode(DeleteListArgs.self, from: data) else {
|
|
685
|
+
output(ResultResponse(success: false, data: nil, error: "Invalid JSON for delete-list"))
|
|
686
|
+
break
|
|
687
|
+
}
|
|
688
|
+
deleteList(store, parsed)
|
|
689
|
+
|
|
690
|
+
default:
|
|
691
|
+
output(ResultResponse(success: false, data: nil, error: "Unknown command: \(command). Available: list-lists, get-reminders, search, create, edit, complete, delete, create-list, delete-list"))
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
sem.signal()
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
sem.wait()
|