From 7d44235d606762c5798fd786ee459271a0548030 Mon Sep 17 00:00:00 2001 From: Mike Gunville Date: Thu, 5 Feb 2026 12:15:22 -0600 Subject: [PATCH 1/4] Migrate from CalendarStore to EventKit framework CalendarStore was deprecated in macOS 10.8 and doesn't integrate with modern macOS privacy controls (TCC), causing "No calendars" errors. This migration to EventKit enables proper permission prompts and calendar/reminders access on modern macOS. Key changes: - Add EventKitStore.m for EKEventStore singleton and permission handling - Update calendarStoreImport.h with EventKit imports and compatibility typedefs - Update Makefile to link EventKit framework - Update icalBuddyFunctions.m for EventKit calendar/event/task queries - Update icalBuddyPrettyPrint.m for EKEvent/EKReminder property access - Update icalBuddyFormatting.m for CGColor to NSColor conversion - Handle EventKit API differences (URL vs url, calendarIdentifier vs uid, etc.) - Add @available checks for macOS 10.15+ CGColor API Tested: calendars, eventsToday, calendar filters, formatted output Co-Authored-By: Claude Sonnet 4.5 --- EventKitStore.m | 119 +++++++++++++++++ Makefile | 6 +- calendarStoreImport.h | 36 ++++- icalBuddy.m | 10 ++ icalBuddyFormatting.h | 2 +- icalBuddyFormatting.m | 25 ++++ icalBuddyFunctions.m | 290 ++++++++++++++++++++++++++++++++++++++++- icalBuddyPrettyPrint.m | 219 ++++++++++++++++++++++++++----- 8 files changed, 666 insertions(+), 41 deletions(-) create mode 100644 EventKitStore.m diff --git a/EventKitStore.m b/EventKitStore.m new file mode 100644 index 0000000..071228d --- /dev/null +++ b/EventKitStore.m @@ -0,0 +1,119 @@ +// icalBuddy EventKit Store +// +// http://hasseg.org/icalBuddy +// + +/* +The MIT License + +Copyright (c) 2008-2012 Ali Rantakari +Copyright (c) 2024 Community Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#import +#import + +// Global event store instance +EKEventStore *eventStore = nil; + +BOOL initEventStore(void) +{ + if (eventStore != nil) + return YES; + + eventStore = [[EKEventStore alloc] init]; + + // Request access synchronously using a semaphore + __block BOOL accessGranted = NO; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + // Check macOS version for appropriate API + if (@available(macOS 14.0, *)) { + // macOS 14+ uses requestFullAccessToEventsWithCompletion + [eventStore requestFullAccessToEventsWithCompletion:^(BOOL granted, NSError *error) { + accessGranted = granted; + if (error) { + NSLog(@"Calendar access error: %@", error.localizedDescription); + } + dispatch_semaphore_signal(semaphore); + }]; + } else if (@available(macOS 10.14, *)) { + // macOS 10.14-13.x uses requestAccessToEntityType + [eventStore requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError *error) { + accessGranted = granted; + if (error) { + NSLog(@"Calendar access error: %@", error.localizedDescription); + } + dispatch_semaphore_signal(semaphore); + }]; + } else { + // Older macOS - access is implicit + accessGranted = YES; + dispatch_semaphore_signal(semaphore); + } + + // Wait for completion (with timeout) + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)); + + if (!accessGranted) { + fprintf(stderr, "error: Calendar access denied. Please grant calendar access in System Settings > Privacy & Security > Calendars.\n"); + return NO; + } + + return YES; +} + +// Request access to reminders (for tasks) +BOOL initReminderAccess(void) +{ + if (eventStore == nil) { + if (!initEventStore()) + return NO; + } + + __block BOOL accessGranted = NO; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + if (@available(macOS 14.0, *)) { + [eventStore requestFullAccessToRemindersWithCompletion:^(BOOL granted, NSError *error) { + accessGranted = granted; + if (error) { + NSLog(@"Reminders access error: %@", error.localizedDescription); + } + dispatch_semaphore_signal(semaphore); + }]; + } else if (@available(macOS 10.14, *)) { + [eventStore requestAccessToEntityType:EKEntityTypeReminder completion:^(BOOL granted, NSError *error) { + accessGranted = granted; + if (error) { + NSLog(@"Reminders access error: %@", error.localizedDescription); + } + dispatch_semaphore_signal(semaphore); + }]; + } else { + accessGranted = YES; + dispatch_semaphore_signal(semaphore); + } + + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)); + + return accessGranted; +} diff --git a/Makefile b/Makefile index e7dc6e3..7d47c20 100644 --- a/Makefile +++ b/Makefile @@ -24,9 +24,9 @@ COMPILER_GCC=gcc COMPILER_CLANG=clang COMPILER=$(COMPILER_CLANG) -CC_WARN_OPTS=-Wall -Wextra -Wno-unused-parameter -Werror +CC_WARN_OPTS=-Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations -SOURCE_FILES=icalBuddy[ABCDEFGHIJKLMNOPQRSTUVWXYZ]*.m ANSIEscapeHelper.m HG*.m IcalBuddy*.m *+HGAdditions.m +SOURCE_FILES=icalBuddy[ABCDEFGHIJKLMNOPQRSTUVWXYZ]*.m ANSIEscapeHelper.m HG*.m IcalBuddy*.m *+HGAdditions.m EventKitStore.m @@ -42,7 +42,7 @@ icalBuddy: $(SOURCE_FILES) icalBuddy.m @echo @echo ---- Compiling main app: @echo ====================================== - $(COMPILER) $(ARG_DEBUG) -O3 $(CC_WARN_OPTS) -std=c99 -force_cpusubtype_ALL -mmacosx-version-min=10.5 -arch x86_64 -framework Cocoa -framework CalendarStore -framework AppKit -framework AddressBook -o $@ icalBuddy.m $(SOURCE_FILES) + $(COMPILER) $(ARG_DEBUG) -O3 $(CC_WARN_OPTS) -std=c99 -mmacosx-version-min=10.13 -framework Cocoa -framework EventKit -framework AppKit -framework AddressBook -o $@ icalBuddy.m $(SOURCE_FILES) diff --git a/calendarStoreImport.h b/calendarStoreImport.h index c841034..da68252 100644 --- a/calendarStoreImport.h +++ b/calendarStoreImport.h @@ -30,9 +30,41 @@ THE SOFTWARE. #ifdef USE_MOCKED_CALENDARSTORE #import "calendarStoreMock/MockCalCalendarStore.h" #define CALENDAR_STORE MockCalCalendarStore + // Keep original CalendarStore types for mock #else - #import - #define CALENDAR_STORE CalCalendarStore + // Use modern EventKit framework instead of deprecated CalendarStore + #import + + // Global event store - initialized once with permission request + extern EKEventStore *eventStore; + + // Initialize the event store and request calendar access + // Returns YES if access granted, NO otherwise + BOOL initEventStore(void); + + // Compatibility typedefs for easier migration + typedef EKCalendar CalCalendar; + typedef EKEvent CalEvent; + typedef EKReminder CalTask; + typedef EKCalendarItem CalCalendarItem; + + // Priority constants - map CalendarStore priorities to EventKit + // CalendarStore: CalPriorityNone=0, CalPriorityHigh=1, CalPriorityMedium=5, CalPriorityLow=9 + // EventKit uses same values for EKReminder priority property + typedef NS_ENUM(NSUInteger, CalPriority) { + CalPriorityNone = 0, + CalPriorityHigh = 1, + CalPriorityMedium = 5, + CalPriorityLow = 9 + }; + + // Calendar type constants for filtering + #define CalCalendarTypeBirthday @"Birthday" + #define CalCalendarTypeCalDAV @"CalDAV" + #define CalCalendarTypeExchange @"Exchange" + #define CalCalendarTypeIMAP @"IMAP" + #define CalCalendarTypeLocal @"Local" + #define CalCalendarTypeSubscription @"Subscription" #endif diff --git a/icalBuddy.m b/icalBuddy.m index a908d61..dee0f82 100644 --- a/icalBuddy.m +++ b/icalBuddy.m @@ -185,6 +185,7 @@ int main(int argc, char *argv[]) } else { + #ifdef USE_MOCKED_CALENDARSTORE for (CalCalendarItem *item in calItems) { if ([item isKindOfClass:[CalEvent class]]) @@ -192,6 +193,15 @@ int main(int argc, char *argv[]) else printCalTask((CalTask *)item, printOptions); } +#else + for (EKCalendarItem *item in calItems) + { + if ([item isKindOfClass:[EKEvent class]]) + printCalEvent((EKEvent *)item, printOptions, now); + else + printCalTask((EKReminder *)item, printOptions); + } +#endif } } // ------------------------------------------------------------------ diff --git a/icalBuddyFormatting.h b/icalBuddyFormatting.h index e557736..59d3114 100644 --- a/icalBuddyFormatting.h +++ b/icalBuddyFormatting.h @@ -29,7 +29,7 @@ THE SOFTWARE. #import #import -#import +#import "calendarStoreImport.h" void initFormatting(NSDictionary *aFormattingConfigDict, NSArray *aPropertySeparators); diff --git a/icalBuddyFormatting.m b/icalBuddyFormatting.m index 9817fc2..889a8e1 100644 --- a/icalBuddyFormatting.m +++ b/icalBuddyFormatting.m @@ -34,6 +34,23 @@ of this software and associated documentation files (the "Software"), to deal #import "icalBuddyL10N.h" #import "ANSIEscapeHelper.h" +#ifndef USE_MOCKED_CALENDARSTORE +// Helper to get calendar color as NSColor (EventKit returns CGColor) +static NSColor* getCalendarColor(EKCalendar *calendar) +{ + if (calendar == nil) + return nil; + if (@available(macOS 10.15, *)) { + CGColorRef cgColor = [calendar CGColor]; + if (cgColor == NULL) + return nil; + return [NSColor colorWithCGColor:cgColor]; + } + // Fallback for older macOS: no color support + return nil; +} +#endif + // default version of the formatting styles dictionary // that normally is under the "formatting" key in @@ -223,7 +240,15 @@ void initFormatting(NSDictionary *aFormattingConfigDict, NSArray *aPropertySepar else if ([part hasSuffix:kFormatColorCyan]) thisColorSGRCode = SGRCodeFgCyan; else if ([part hasSuffix:kFormatColorCalendarColor] && calItem != nil) + { +#ifdef USE_MOCKED_CALENDARSTORE thisColorSGRCode = [ansiEscapeHelper closestSGRCodeForColor:[[calItem calendar] color] isForegroundColor:YES]; +#else + NSColor *calColor = getCalendarColor([calItem calendar]); + if (calColor != nil) + thisColorSGRCode = [ansiEscapeHelper closestSGRCodeForColor:calColor isForegroundColor:YES]; +#endif + } if (thisColorSGRCode != SGRCodeNoneOrInvalid) { diff --git a/icalBuddyFunctions.m b/icalBuddyFunctions.m index 27f140e..61c6b30 100644 --- a/icalBuddyFunctions.m +++ b/icalBuddyFunctions.m @@ -43,6 +43,10 @@ of this software and associated documentation files (the "Software"), to deal NSDate *now; NSDate *today; +#ifndef USE_MOCKED_CALENDARSTORE +// Declare external reminders access function +extern BOOL initReminderAccess(void); +#endif @@ -122,13 +126,23 @@ BOOL areWePrintingAlsoPastEvents(AppOptions *opts) DebugPrintf(@"effective query start date: %@\n", opts->startDate); DebugPrintf(@"effective query end date: %@\n", opts->endDate); - // make predicate for getting all events between start and end dates + use it to get the events +#ifdef USE_MOCKED_CALENDARSTORE + // Use original CalendarStore API for mock NSPredicate *eventsPredicate = [CALENDAR_STORE eventPredicateWithStartDate:opts->startDate endDate:opts->endDate calendars:calendars ]; NSArray *ret = [[CALENDAR_STORE defaultCalendarStore] eventsWithPredicate:eventsPredicate]; +#else + // Use EventKit API + NSPredicate *eventsPredicate = [eventStore + predicateForEventsWithStartDate:opts->startDate + endDate:opts->endDate + calendars:calendars + ]; + NSArray *ret = [eventStore eventsMatchingPredicate:eventsPredicate]; +#endif // filter results if (opts->excludeAllDayEvents) @@ -140,6 +154,7 @@ BOOL areWePrintingAlsoPastEvents(AppOptions *opts) NSArray *getTasks(AppOptions *opts, NSArray *calendars) { +#ifdef USE_MOCKED_CALENDARSTORE NSPredicate *tasksPredicate = nil; if (opts->output_is_tasksDueBefore) @@ -176,6 +191,94 @@ BOOL areWePrintingAlsoPastEvents(AppOptions *opts) return nil; return [[CALENDAR_STORE defaultCalendarStore] tasksWithPredicate:tasksPredicate]; +#else + // EventKit uses EKReminder for tasks + // First ensure we have reminders access + if (!initReminderAccess()) { + PrintfErr(@"error: Reminders access denied.\n"); + return nil; + } + + // Get reminder calendars + NSArray *reminderCalendars = [eventStore calendarsForEntityType:EKEntityTypeReminder]; + + // Filter to match requested calendars by title + if (calendars != nil && [calendars count] > 0) { + NSMutableArray *calendarTitles = [NSMutableArray array]; + for (EKCalendar *cal in calendars) { + [calendarTitles addObject:[cal title]]; + } + NSMutableArray *filteredReminderCals = [NSMutableArray array]; + for (EKCalendar *reminderCal in reminderCalendars) { + if ([calendarTitles containsObject:[reminderCal title]]) { + [filteredReminderCals addObject:reminderCal]; + } + } + if ([filteredReminderCals count] > 0) { + reminderCalendars = filteredReminderCals; + } + } + + NSPredicate *remindersPredicate = nil; + + if (opts->output_is_tasksDueBefore) + { + NSDate *dueBeforeDate = nil; + + NSString *dueBeforeDateStr = [opts->output substringFromIndex:15]; + dueBeforeDate = dateFromUserInput(dueBeforeDateStr, @"due date", NO); + + if (dueBeforeDate == nil) + { + PrintfErr(@"\n"); + printDateFormatInfo(); + return nil; + } + + opts->dueBeforeDate = dueBeforeDate; + DebugPrintf(@"effective query 'due before' date: %@\n", dueBeforeDate); + + // Get incomplete reminders with due date before specified date + remindersPredicate = [eventStore predicateForIncompleteRemindersWithDueDateStarting:nil + ending:dueBeforeDate + calendars:reminderCalendars]; + } + else if (opts->output_is_uncompletedTasks) + { + // Get all incomplete reminders + remindersPredicate = [eventStore predicateForIncompleteRemindersWithDueDateStarting:nil + ending:nil + calendars:reminderCalendars]; + } + else if (opts->output_is_undatedUncompletedTasks) + { + // Get incomplete reminders and filter for those without due date + remindersPredicate = [eventStore predicateForIncompleteRemindersWithDueDateStarting:nil + ending:nil + calendars:reminderCalendars]; + } + else + return nil; + + // Fetch reminders synchronously + __block NSArray *reminders = nil; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + [eventStore fetchRemindersMatchingPredicate:remindersPredicate completion:^(NSArray *fetchedReminders) { + reminders = fetchedReminders; + dispatch_semaphore_signal(semaphore); + }]; + + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)); + + // For undated tasks, filter out those with due dates + if (opts->output_is_undatedUncompletedTasks && reminders != nil) { + reminders = [reminders filteredArrayUsingPredicate: + [NSPredicate predicateWithFormat:@"dueDateComponents == nil"]]; + } + + return reminders; +#endif } @@ -206,6 +309,7 @@ BOOL areWePrintingAlsoPastEvents(AppOptions *opts) // - sort numerically by priority except treat CalPriorityNone (0) as a special case // - if priorities match, sort tasks that are late from their due date to be first and then // order alphabetically by title +#ifdef USE_MOCKED_CALENDARSTORE NSInteger prioritySort(CalTask *task1, CalTask *task2, void *context) { if ([task1 priority] < [task2 priority]) @@ -243,6 +347,55 @@ NSInteger prioritySort(CalTask *task1, CalTask *task2, void *context) return [[task1 title] compare:[task2 title]]; } } +#else +NSInteger prioritySort(EKReminder *task1, EKReminder *task2, void *context) +{ + NSUInteger priority1 = [task1 priority]; + NSUInteger priority2 = [task2 priority]; + + if (priority1 < priority2) + { + if (priority1 == CalPriorityNone) + return NSOrderedDescending; + else + return NSOrderedAscending; + } + else if (priority1 > priority2) + if (priority2 == CalPriorityNone) + return NSOrderedAscending; + else + return NSOrderedDescending; + else + { + // check if one task is late and the other is not + BOOL task1late = NO; + BOOL task2late = NO; + + NSDate *dueDate1 = nil; + NSDate *dueDate2 = nil; + + if ([task1 dueDateComponents] != nil) { + dueDate1 = [[NSCalendar currentCalendar] dateFromComponents:[task1 dueDateComponents]]; + } + if ([task2 dueDateComponents] != nil) { + dueDate2 = [[NSCalendar currentCalendar] dateFromComponents:[task2 dueDateComponents]]; + } + + if (dueDate1 != nil && [now compare:dueDate1] == NSOrderedDescending) + task1late = YES; + if (dueDate2 != nil && [now compare:dueDate2] == NSOrderedDescending) + task2late = YES; + + if (task1late && !task2late) + return NSOrderedAscending; + else if (task2late && !task1late) + return NSOrderedDescending; + + // neither task is, or both tasks are late -> order alphabetically by title + return [[task1 title] compare:[task2 title]]; + } +} +#endif @@ -256,6 +409,7 @@ NSInteger prioritySort(CalTask *task1, CalTask *task2, void *context) { if (opts->sortTasksByDueDate || opts->sortTasksByDueDateAscending) { +#ifdef USE_MOCKED_CALENDARSTORE retCalItems = [calItems sortedArrayUsingDescriptors:[NSArray arrayWithObjects: @@ -279,6 +433,31 @@ NSInteger prioritySort(CalTask *task1, CalTask *task2, void *context) ]; retCalItems = [retCalItems arrayByAddingObjectsFromArray:tasksWithNoDueDate]; } +#else + // EventKit uses dueDateComponents instead of dueDate + retCalItems = [calItems + sortedArrayUsingDescriptors:[NSArray + arrayWithObjects: + [[[NSSortDescriptor alloc] initWithKey:@"dueDateComponents" ascending:opts->sortTasksByDueDateAscending] autorelease], + nil + ] + ]; + + if (opts->sortTasksByDueDateAscending) + { + NSArray *tasksWithNoDueDate = [retCalItems + filteredArrayUsingPredicate:[NSPredicate + predicateWithFormat:@"dueDateComponents == nil" + ] + ]; + retCalItems = [retCalItems + filteredArrayUsingPredicate:[NSPredicate + predicateWithFormat:@"dueDateComponents != nil" + ] + ]; + retCalItems = [retCalItems arrayByAddingObjectsFromArray:tasksWithNoDueDate]; + } +#endif } else retCalItems = [calItems sortedArrayUsingFunction:prioritySort context:NULL]; @@ -351,6 +530,7 @@ CalItemPrintOption getPrintOptions(AppOptions *opts) NSArray *calendars = getCalendars(opts); sections = [NSMutableArray arrayWithCapacity:[calendars count]]; +#ifdef USE_MOCKED_CALENDARSTORE for (CalCalendar *cal in calendars) { NSMutableArray *thisCalendarItems = [NSMutableArray arrayWithCapacity:((printingEvents)?[calItems count]:[calItems count])]; @@ -368,6 +548,26 @@ CalItemPrintOption getPrintOptions(AppOptions *opts) [sections addObject:SECTION_TO_NSVALUE(section)]; } } +#else + for (EKCalendar *cal in calendars) + { + NSMutableArray *thisCalendarItems = [NSMutableArray arrayWithCapacity:[calItems count]]; + + if (printingEvents) + [thisCalendarItems addObjectsFromArray:calItems]; + else if (printingTasks) + [thisCalendarItems addObjectsFromArray:calItems]; + + // EventKit uses calendarIdentifier instead of uid + [thisCalendarItems filterUsingPredicate:[NSPredicate predicateWithFormat:@"calendar.calendarIdentifier == %@", [cal calendarIdentifier]]]; + + if (thisCalendarItems != nil && [thisCalendarItems count] > 0) + { + PrintSection section = {[cal title], thisCalendarItems, nil}; + [sections addObject:SECTION_TO_NSVALUE(section)]; + } + } +#endif } else if (opts->separateByDate) { @@ -379,7 +579,11 @@ CalItemPrintOption getPrintOptions(AppOptions *opts) { // fill allDays using event start dates' days and all spanned days thereafter // if the event spans multiple days +#ifdef USE_MOCKED_CALENDARSTORE for (CalEvent *anEvent in calItems) +#else + for (EKEvent *anEvent in calItems) +#endif { // calculate anEvent's days span and limit it to the range of days we // want displayed @@ -436,6 +640,7 @@ CalItemPrintOption getPrintOptions(AppOptions *opts) else if (printingTasks) { // fill allDays using task due dates' days +#ifdef USE_MOCKED_CALENDARSTORE for (CalTask *aTask in calItems) { id thisDayKey = nil; @@ -455,7 +660,27 @@ CalItemPrintOption getPrintOptions(AppOptions *opts) NSCAssert((thisDayTasks != nil), @"thisDayTasks is nil"); [thisDayTasks addObject:aTask]; } +#else + for (EKReminder *aTask in calItems) + { + id thisDayKey = nil; + if ([aTask dueDateComponents] != nil) + { + NSDate *thisTaskDueDate = [[NSCalendar currentCalendar] dateFromComponents:[aTask dueDateComponents]]; + NSDate *thisDueDay = dateForStartOfDay(thisTaskDueDate); + thisDayKey = thisDueDay; + } + else + thisDayKey = [NSNull null]; + if (![[allDays allKeys] containsObject:thisDayKey]) + [allDays setObject:[NSMutableArray arrayWithCapacity:20] forKey:thisDayKey]; + + NSMutableArray *thisDayTasks = [allDays objectForKey:thisDayKey]; + NSCAssert((thisDayTasks != nil), @"thisDayTasks is nil"); + [thisDayTasks addObject:aTask]; + } +#endif } sections = [NSMutableArray arrayWithCapacity:[calItems count]]; @@ -544,11 +769,19 @@ CalItemPrintOption getPrintOptions(AppOptions *opts) { CalPriority priority = priorities[i]; NSMutableArray *thisCalendarItems = [NSMutableArray arrayWithCapacity:[calItems count]]; +#ifdef USE_MOCKED_CALENDARSTORE for (CalTask *aTask in calItems) { if ([aTask priority] == priority) [thisCalendarItems addObject:aTask]; } +#else + for (EKReminder *aTask in calItems) + { + if ([aTask priority] == priority) + [thisCalendarItems addObject:aTask]; + } +#endif if (0 < [thisCalendarItems count]) { PrintSection section = {localizedPriorityTitle(priority), thisCalendarItems, nil}; @@ -564,12 +797,21 @@ CalItemPrintOption getPrintOptions(AppOptions *opts) void filterCalendarsByNameOrUID(NSMutableArray *cals, AppOptions *opts) { +#ifdef USE_MOCKED_CALENDARSTORE if (opts->includeCals != nil) [cals filterUsingPredicate:[NSPredicate predicateWithFormat:@"(uid IN %@) OR (title IN %@)", opts->includeCals, opts->includeCals]]; if (opts->excludeCals != nil) [cals filterUsingPredicate:[NSPredicate predicateWithFormat:@"(NOT(uid IN %@)) AND (NOT(title IN %@))", opts->excludeCals, opts->excludeCals]]; +#else + // EventKit uses calendarIdentifier instead of uid + if (opts->includeCals != nil) + [cals filterUsingPredicate:[NSPredicate predicateWithFormat:@"(calendarIdentifier IN %@) OR (title IN %@)", opts->includeCals, opts->includeCals]]; + if (opts->excludeCals != nil) + [cals filterUsingPredicate:[NSPredicate predicateWithFormat:@"(NOT(calendarIdentifier IN %@)) AND (NOT(title IN %@))", opts->excludeCals, opts->excludeCals]]; +#endif } +#ifdef USE_MOCKED_CALENDARSTORE NSArray *getCalendarStoreCalTypeValuesForUserProvidedValues(NSArray *userProvidedCalTypes) { NSMutableArray *ret = [NSMutableArray arrayWithCapacity:[userProvidedCalTypes count]]; @@ -606,6 +848,43 @@ void filterCalendarsByType(NSMutableArray *cals, AppOptions *opts) [cals filterUsingPredicate:[NSPredicate predicateWithFormat:@"NOT(type IN %@)", excludeActualCalTypes]]; } } +#else +// EventKit calendar type filtering +NSArray *getEventKitCalTypeValuesForUserProvidedValues(NSArray *userProvidedCalTypes) +{ + NSMutableArray *ret = [NSMutableArray arrayWithCapacity:[userProvidedCalTypes count]]; + for (NSString *userProvidedType in userProvidedCalTypes) + { + if ([userProvidedType caseInsensitiveCompare:kCalendarTypeBirthday] == NSOrderedSame) + [ret addObject:@(EKCalendarTypeBirthday)]; + else if ([userProvidedType caseInsensitiveCompare:kCalendarTypeCalDAV] == NSOrderedSame) + [ret addObject:@(EKCalendarTypeCalDAV)]; + else if ([userProvidedType caseInsensitiveCompare:kCalendarTypeiCloud] == NSOrderedSame) + [ret addObject:@(EKCalendarTypeCalDAV)]; + else if ([userProvidedType caseInsensitiveCompare:kCalendarTypeExchange] == NSOrderedSame) + [ret addObject:@(EKCalendarTypeExchange)]; + else if ([userProvidedType caseInsensitiveCompare:kCalendarTypeLocal] == NSOrderedSame) + [ret addObject:@(EKCalendarTypeLocal)]; + else if ([userProvidedType caseInsensitiveCompare:kCalendarTypeSubscription] == NSOrderedSame) + [ret addObject:@(EKCalendarTypeSubscription)]; + } + return ret; +} + +void filterCalendarsByType(NSMutableArray *cals, AppOptions *opts) +{ + if (opts->includeCalTypes != nil) + { + NSArray *includeActualCalTypes = getEventKitCalTypeValuesForUserProvidedValues(opts->includeCalTypes); + [cals filterUsingPredicate:[NSPredicate predicateWithFormat:@"type IN %@", includeActualCalTypes]]; + } + if (opts->excludeCalTypes != nil) + { + NSArray *excludeActualCalTypes = getEventKitCalTypeValuesForUserProvidedValues(opts->excludeCalTypes); + [cals filterUsingPredicate:[NSPredicate predicateWithFormat:@"NOT(type IN %@)", excludeActualCalTypes]]; + } +} +#endif void filterCalendars(NSMutableArray *cals, AppOptions *opts) { @@ -624,7 +903,15 @@ void filterCalendars(NSMutableArray *cals, AppOptions *opts) NSArray *getCalendars(AppOptions *opts) { +#ifdef USE_MOCKED_CALENDARSTORE NSMutableArray *calendars = [[[[[[CALENDAR_STORE alloc] init] autorelease] calendars] mutableCopy] autorelease]; +#else + // Initialize event store if needed + if (!initEventStore()) { + return [NSArray array]; + } + NSMutableArray *calendars = [[[eventStore calendarsForEntityType:EKEntityTypeEvent] mutableCopy] autorelease]; +#endif filterCalendars(calendars, opts); return calendars; } @@ -740,4 +1027,3 @@ void openConfigFileInEditor(NSString *configFilePath, BOOL openInCLIEditor) } } - diff --git a/icalBuddyPrettyPrint.m b/icalBuddyPrettyPrint.m index aedba73..03cb147 100644 --- a/icalBuddyPrettyPrint.m +++ b/icalBuddyPrettyPrint.m @@ -40,6 +40,51 @@ of this software and associated documentation files (the "Software"), to deal #import "icalBuddyFunctions.h" // today, now #import "ABRecord+HGAdditions.h" +#ifndef USE_MOCKED_CALENDARSTORE +// Helper to get calendar color as NSColor (EventKit returns CGColor) +static NSColor* getCalendarColor(EKCalendar *calendar) +{ + if (calendar == nil) + return nil; + if (@available(macOS 10.15, *)) { + CGColorRef cgColor = [calendar CGColor]; + if (cgColor == NULL) + return nil; + return [NSColor colorWithCGColor:cgColor]; + } + // Fallback for older macOS: no color support + return nil; +} + +// Helper to check if calendar is a birthday calendar +static BOOL isBirthdayCalendar(EKCalendar *calendar) +{ + return (calendar != nil && [calendar type] == EKCalendarTypeBirthday); +} + +// Helper to get NSDate from EKReminder's dueDateComponents +static NSDate* getTaskDueDate(EKReminder *reminder) +{ + if (reminder == nil || [reminder dueDateComponents] == nil) + return nil; + return [[NSCalendar currentCalendar] dateFromComponents:[reminder dueDateComponents]]; +} + +// Helper to get calendar type as string for display +static NSString* getCalendarTypeString(EKCalendar *calendar) +{ + if (calendar == nil) + return @"Unknown"; + switch ([calendar type]) { + case EKCalendarTypeLocal: return @"Local"; + case EKCalendarTypeCalDAV: return @"CalDAV"; + case EKCalendarTypeExchange: return @"Exchange"; + case EKCalendarTypeSubscription: return @"Subscription"; + case EKCalendarTypeBirthday: return @"Birthday"; + default: return @"Unknown"; + } +} +#endif PrettyPrintOptions prettyPrintOptions; @@ -292,7 +337,12 @@ @implementation PropertyPresentationElements NSString *thisPropTempValue = nil; - if ([[[event calendar] type] isEqualToString:CalCalendarTypeBirthday]) +#ifdef USE_MOCKED_CALENDARSTORE + BOOL isBirthday = [[[event calendar] type] isEqualToString:CalCalendarTypeBirthday]; +#else + BOOL isBirthday = isBirthdayCalendar([event calendar]); +#endif + if (isBirthday) { ABAddressBook *addressBook = [ABAddressBook sharedAddressBook]; @@ -308,7 +358,12 @@ @implementation PropertyPresentationElements // so we have to use the URI to find the ABPerson from the Address Book // and print their name from there) - NSString *personId = [[NSString stringWithFormat:@"%@", [event url]] +#ifdef USE_MOCKED_CALENDARSTORE + NSURL *eventURL = [event url]; +#else + NSURL *eventURL = [event URL]; +#endif + NSString *personId = [[NSString stringWithFormat:@"%@", eventURL] stringByReplacingOccurrencesOfString:@"addressbook://" withString:@"" ]; @@ -412,10 +467,15 @@ @implementation PropertyPresentationElements elements.name = M_ATTR_STR(strConcat(localizedStr(kL10nKeyPropNameUrl), @":", nil)); - if ([event url] != nil && - ![[[event calendar] type] isEqualToString:CalCalendarTypeBirthday] - ) - elements.value = M_ATTR_STR(([NSString stringWithFormat: @"%@", [event url]])); +#ifdef USE_MOCKED_CALENDARSTORE + BOOL isBirthdayUrl = [[[event calendar] type] isEqualToString:CalCalendarTypeBirthday]; + NSURL *eventURLValue = [event url]; +#else + BOOL isBirthdayUrl = isBirthdayCalendar([event calendar]); + NSURL *eventURLValue = [event URL]; +#endif + if (eventURLValue != nil && !isBirthdayUrl) + elements.value = M_ATTR_STR(([NSString stringWithFormat: @"%@", eventURLValue])); return elements; } @@ -425,7 +485,11 @@ @implementation PropertyPresentationElements PropertyPresentationElements *elements = [PropertyPresentationElements new]; elements.name = M_ATTR_STR(strConcat(localizedStr(kL10nKeyPropNameUID), @":", nil)); +#ifdef USE_MOCKED_CALENDARSTORE elements.value = M_ATTR_STR([event uid]); +#else + elements.value = M_ATTR_STR([event calendarItemIdentifier]); +#endif return elements; } @@ -436,14 +500,27 @@ @implementation PropertyPresentationElements elements.name = M_ATTR_STR(strConcat(localizedStr(kL10nKeyPropNameAttendees), @":", nil)); - if ([event attendees] != nil && ![[[event calendar] type] isEqualToString:CalCalendarTypeBirthday]) +#ifdef USE_MOCKED_CALENDARSTORE + BOOL isBirthdayAttendees = [[[event calendar] type] isEqualToString:CalCalendarTypeBirthday]; +#else + BOOL isBirthdayAttendees = isBirthdayCalendar([event calendar]); +#endif + if ([event attendees] != nil && !isBirthdayAttendees) { NSMutableArray *attendeeNames = [NSMutableArray array]; +#ifdef USE_MOCKED_CALENDARSTORE for (CalAttendee *attendee in [event attendees]) { NSString *attendeeDisplayName = [attendee commonName] ?: [NSString stringWithFormat:@"%@", [attendee address]]; [attendeeNames addObject:attendeeDisplayName]; } +#else + for (EKParticipant *attendee in [event attendees]) + { + NSString *attendeeDisplayName = [attendee name] ?: [NSString stringWithFormat:@"%@", [attendee URL]]; + [attendeeNames addObject:attendeeDisplayName]; + } +#endif if (0 < printOptions.maxNumPrintedAttendees && printOptions.maxNumPrintedAttendees < attendeeNames.count) { attendeeNames = [[attendeeNames subarrayWithRange:NSMakeRange(0, printOptions.maxNumPrintedAttendees)] @@ -458,7 +535,12 @@ @implementation PropertyPresentationElements { PropertyPresentationElements *elements = [PropertyPresentationElements new]; - if ([[[event calendar] type] isEqualToString:CalCalendarTypeBirthday]) +#ifdef USE_MOCKED_CALENDARSTORE + BOOL isBirthdayEvent = [[[event calendar] type] isEqualToString:CalCalendarTypeBirthday]; +#else + BOOL isBirthdayEvent = isBirthdayCalendar([event calendar]); +#endif + if (isBirthdayEvent) { if (!printOptions.singleDay) elements.value = M_ATTR_STR(dateStr([event startDate], ONLY_DATE)); @@ -626,13 +708,20 @@ @implementation PropertyPresentationElements if ([propName isEqualToString:kPropName_title] && prettyPrintOptions.useCalendarColorsForTitles && ![[[elements.value attributesAtIndex:0 effectiveRange:NULL] allKeys] containsObject:NSForegroundColorAttributeName] - && [[event calendar] color] != nil ) - [elements.value - addAttribute:NSForegroundColorAttributeName - value:getClosestAnsiColorForColor([[event calendar] color], YES) - range:NSMakeRange(0, [elements.value length]) - ]; + { +#ifdef USE_MOCKED_CALENDARSTORE + NSColor *calColor = [[event calendar] color]; +#else + NSColor *calColor = getCalendarColor([event calendar]); +#endif + if (calColor != nil) + [elements.value + addAttribute:NSForegroundColorAttributeName + value:getClosestAnsiColorForColor(calColor, YES) + range:NSMakeRange(0, [elements.value length]) + ]; + } if (elements.valueSuffix != nil) [elements.value appendAttributedString:elements.valueSuffix]; @@ -786,8 +875,13 @@ void printCalEvent(CalEvent *event, CalItemPrintOption printOptions, NSDate *con elements.name = M_ATTR_STR(strConcat(localizedStr(kL10nKeyPropNameUrl), @":", nil)); - if ([task url] != nil) - elements.value = M_ATTR_STR(([NSString stringWithFormat:@"%@", [task url]])); +#ifdef USE_MOCKED_CALENDARSTORE + NSURL *taskURL = [task url]; +#else + NSURL *taskURL = [task URL]; +#endif + if (taskURL != nil) + elements.value = M_ATTR_STR(([NSString stringWithFormat:@"%@", taskURL])); return elements; } @@ -797,7 +891,11 @@ void printCalEvent(CalEvent *event, CalItemPrintOption printOptions, NSDate *con PropertyPresentationElements *elements = [PropertyPresentationElements new]; elements.name = M_ATTR_STR(strConcat(localizedStr(kL10nKeyPropNameUID), @":", nil)); +#ifdef USE_MOCKED_CALENDARSTORE elements.value = M_ATTR_STR([task uid]); +#else + elements.value = M_ATTR_STR([task calendarItemIdentifier]); +#endif return elements; } @@ -808,8 +906,13 @@ void printCalEvent(CalEvent *event, CalItemPrintOption printOptions, NSDate *con elements.name = M_ATTR_STR(strConcat(localizedStr(kL10nKeyPropNameDueDate), @":", nil)); - if ([task dueDate] != nil && !printOptions.singleDay) - elements.value = M_ATTR_STR(dateStr([task dueDate], DATE_AND_TIME)); +#ifdef USE_MOCKED_CALENDARSTORE + NSDate *dueDate = [task dueDate]; +#else + NSDate *dueDate = getTaskDueDate(task); +#endif + if (dueDate != nil && !printOptions.singleDay) + elements.value = M_ATTR_STR(dateStr(dueDate, DATE_AND_TIME)); return elements; } @@ -925,11 +1028,19 @@ void printCalEvent(CalEvent *event, CalItemPrintOption printOptions, NSDate *con && prettyPrintOptions.useCalendarColorsForTitles && ![[[elements.value attributesAtIndex:0 effectiveRange:NULL] allKeys] containsObject:NSForegroundColorAttributeName] ) - [elements.value - addAttribute:NSForegroundColorAttributeName - value:getClosestAnsiColorForColor([[task calendar] color], YES) - range:NSMakeRange(0, [elements.value length]) - ]; + { +#ifdef USE_MOCKED_CALENDARSTORE + NSColor *taskCalColor = [[task calendar] color]; +#else + NSColor *taskCalColor = getCalendarColor([task calendar]); +#endif + if (taskCalColor != nil) + [elements.value + addAttribute:NSForegroundColorAttributeName + value:getClosestAnsiColorForColor(taskCalColor, YES) + range:NSMakeRange(0, [elements.value length]) + ]; + } if (elements.valueSuffix != nil) [elements.value appendAttributedString:elements.valueSuffix]; @@ -973,8 +1084,13 @@ void printCalTask(CalTask *task, CalItemPrintOption printOptions) NSMutableAttributedString *prefixStr; if (numPrintedProps == 0) { - BOOL useAlertBullet = ([task dueDate] != nil && - [now compare:[task dueDate]] == NSOrderedDescending); +#ifdef USE_MOCKED_CALENDARSTORE + NSDate *taskDueDate = [task dueDate]; +#else + NSDate *taskDueDate = getTaskDueDate(task); +#endif + BOOL useAlertBullet = (taskDueDate != nil && + [now compare:taskDueDate] == NSOrderedDescending); prefixStr = mutableAttrStrWithAttrs( ((useAlertBullet)?prettyPrintOptions.prefixStrBulletAlert:prettyPrintOptions.prefixStrBullet), getBulletStringAttributes(useAlertBullet, task) @@ -1060,14 +1176,21 @@ void printItemSections(NSArray *sections, CalItemPrintOption printOptions) && prettyPrintOptions.useCalendarColorsForTitles && ![[[thisOutput attributesAtIndex:0 effectiveRange:NULL] allKeys] containsObject:NSForegroundColorAttributeName] && section.items != nil && [section.items count] > 0 - && [[((CalCalendarItem *)[section.items objectAtIndex:0]) calendar] color] != nil ) { - [thisOutput - addAttribute:NSForegroundColorAttributeName - value:getClosestAnsiColorForColor([[((CalCalendarItem *)[section.items objectAtIndex:0]) calendar] color], YES) - range:NSMakeRange(0, [thisOutput length]) - ]; +#ifdef USE_MOCKED_CALENDARSTORE + NSColor *sectionColor = [[((CalCalendarItem *)[section.items objectAtIndex:0]) calendar] color]; +#else + NSColor *sectionColor = getCalendarColor([((EKCalendarItem *)[section.items objectAtIndex:0]) calendar]); +#endif + if (sectionColor != nil) + { + [thisOutput + addAttribute:NSForegroundColorAttributeName + value:getClosestAnsiColorForColor(sectionColor, YES) + range:NSMakeRange(0, [thisOutput length]) + ]; + } } ADD_TO_OUTPUT_BUFFER(thisOutput); @@ -1089,6 +1212,7 @@ void printItemSections(NSArray *sections, CalItemPrintOption printOptions) } // print items in section +#ifdef USE_MOCKED_CALENDARSTORE for (CalCalendarItem *item in section.items) { if ([item isKindOfClass:[CalEvent class]]) @@ -1101,6 +1225,20 @@ void printItemSections(NSArray *sections, CalItemPrintOption printOptions) else if ([item isKindOfClass:[CalTask class]]) printCalTask((CalTask*)item, printOptions); } +#else + for (EKCalendarItem *item in section.items) + { + if ([item isKindOfClass:[EKEvent class]]) + { + NSDate *contextDay = section.eventsContextDay; + if (contextDay == nil) + contextDay = now; + printCalEvent((EKEvent*)item, printOptions, contextDay); + } + else if ([item isKindOfClass:[EKReminder class]]) + printCalTask((EKReminder*)item, printOptions); + } +#endif } } @@ -1110,17 +1248,32 @@ void printAllCalendars(AppOptions *opts) { NSArray *calendars = getCalendars(opts); +#ifdef USE_MOCKED_CALENDARSTORE for (CalCalendar *cal in calendars) { ADD_TO_OUTPUT_BUFFER(ATTR_STR(@"• ")); NSMutableAttributedString *calendarName = M_ATTR_STR([cal title]); - if([cal color] != nil) - [calendarName addAttribute:NSForegroundColorAttributeName value:[cal color] range:NSMakeRange(0, [calendarName length])]; + if([cal color] != nil) + [calendarName addAttribute:NSForegroundColorAttributeName value:[cal color] range:NSMakeRange(0, [calendarName length])]; ADD_TO_OUTPUT_BUFFER(calendarName); ADD_TO_OUTPUT_BUFFER(ATTR_STR(@"\n")); ADD_TO_OUTPUT_BUFFER(ATTR_STR(([NSString stringWithFormat:@" type: %@\n", [cal type]]))); ADD_TO_OUTPUT_BUFFER(ATTR_STR(([NSString stringWithFormat:@" UID: %@\n", [cal uid]]))); } +#else + for (EKCalendar *cal in calendars) + { + ADD_TO_OUTPUT_BUFFER(ATTR_STR(@"• ")); + NSMutableAttributedString *calendarName = M_ATTR_STR([cal title]); + NSColor *calColor = getCalendarColor(cal); + if(calColor != nil) + [calendarName addAttribute:NSForegroundColorAttributeName value:calColor range:NSMakeRange(0, [calendarName length])]; + ADD_TO_OUTPUT_BUFFER(calendarName); + ADD_TO_OUTPUT_BUFFER(ATTR_STR(@"\n")); + ADD_TO_OUTPUT_BUFFER(ATTR_STR(([NSString stringWithFormat:@" type: %@\n", getCalendarTypeString(cal)]))); + ADD_TO_OUTPUT_BUFFER(ATTR_STR(([NSString stringWithFormat:@" UID: %@\n", [cal calendarIdentifier]]))); + } +#endif } void flushOutputBuffer(NSMutableAttributedString *buffer, AppOptions *opts, NSDictionary *formattedKeywords) From d61ae7395dfe05374a12268391a8472f8f3c3246 Mon Sep 17 00:00:00 2001 From: Mike Gunville Date: Thu, 5 Feb 2026 12:22:57 -0600 Subject: [PATCH 2/4] Update README with fork documentation and add LICENSE file - Document the EventKit migration and why it's needed - Add installation and usage instructions - Include troubleshooting section for common issues - Credit all authors in fork lineage table - Add separate LICENSE file with full attribution Co-Authored-By: Claude Sonnet 4.5 --- LICENSE | 23 +++++++++++ readme.md | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ccbb67b --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2008-2012 Ali Rantakari +64-bit updates (c) dkaluta +EventKit migration (c) 2025 mgunville + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/readme.md b/readme.md index ee176fc..1722a06 100644 --- a/readme.md +++ b/readme.md @@ -1,14 +1,118 @@ -# icalBuddy +# icalBuddy (EventKit Fork) -This is a command-line utility that can be used to get lists of events and tasks/to-do's from the macOS calendar database (the same one Calendar.app uses). +A command-line utility for macOS that displays events and tasks from Calendar and Reminders apps. -Read more at . +**This fork migrates from the deprecated CalendarStore framework to EventKit**, fixing the "No calendars" issue on modern macOS (10.14+). -Compiles and runs successfully on macOS 10.15.0 Catalina +## Why This Fork? -## The MIT License +The original icalBuddy uses Apple's CalendarStore framework, which was deprecated in macOS 10.8 (2012). On modern macOS, CalendarStore doesn't integrate with the privacy system (TCC - Transparency, Consent, and Control), causing icalBuddy to report "No calendars" even when calendars exist. -Copyright (c) Ali Rantakari +This fork uses EventKit, which: +- Works with modern macOS privacy controls (10.14+) +- Triggers proper permission prompts +- Supports both Calendar and Reminders access +- Is actively maintained by Apple + +## Installation + +### From Source + +```bash +git clone https://github.com/mgunville/icalBuddy64.git +cd icalBuddy64 +make clean && make +sudo cp icalBuddy /usr/local/bin/ +# Or for user-local install: +cp icalBuddy ~/bin/ +``` + +### First Run + +On first run, icalBuddy will request calendar access. Click "OK" when prompted, or grant access manually in: +**System Settings → Privacy & Security → Calendars** + +For tasks/reminders, also grant access in: +**System Settings → Privacy & Security → Reminders** + +## Usage + +```bash +# List all calendars +icalBuddy calendars + +# Today's events +icalBuddy eventsToday + +# Events for next 7 days +icalBuddy eventsToday+7 + +# Events from a specific calendar +icalBuddy -ic "Work" eventsToday + +# Uncompleted tasks +icalBuddy uncompletedTasks + +# Formatted output (e.g., for Obsidian Templater) +icalBuddy -npn -nc -ps "/ - /" -iep "datetime,title" \ + -po "datetime,title" -b "###### " -tf "%H%M" \ + -ic "Work" eventsToday +``` + +See the [man page](http://hasseg.org/icalBuddy/man.html) for full documentation. + +## Troubleshooting + +### "No calendars" Error + +1. Check System Settings → Privacy & Security → Calendars +2. Ensure Terminal (or your app) has access enabled +3. If needed, reset and retry: + ```bash + tccutil reset Calendar + ./icalBuddy calendars # Will prompt again + ``` + +### "Reminders access denied" + +Grant access in System Settings → Privacy & Security → Reminders + +## Fork Lineage + +This project has the following history: + +| Version | Author | Repository | +|---------|--------|------------| +| Original | Ali Rantakari | [hasseg.org/icalBuddy](http://hasseg.org/icalBuddy) | +| 64-bit fork | dkaluta | [github.com/dkaluta/icalBuddy64](https://github.com/dkaluta/icalBuddy64) | +| EventKit fork | mgunville | [github.com/mgunville/icalBuddy64](https://github.com/mgunville/icalBuddy64) | + +## Changes from Upstream + +- **Framework**: CalendarStore → EventKit +- **Permissions**: Proper TCC integration with permission prompts +- **Compatibility**: macOS 10.13+ (colors require 10.15+) +- **New file**: `EventKitStore.m` for permission handling + +All original functionality is preserved. The codebase maintains `#ifdef USE_MOCKED_CALENDARSTORE` guards for test compatibility. + +## Building + +Requirements: +- macOS 10.13+ +- Xcode Command Line Tools + +```bash +make clean && make +``` + +## License + +The MIT License + +Copyright (c) 2008-2012 Ali Rantakari +64-bit updates (c) dkaluta +EventKit migration (c) 2025 mgunville Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 0b16d6fb02847f72375a8a53ff3bd9554c16f282 Mon Sep 17 00:00:00 2001 From: Mike Gunville Date: Tue, 17 Feb 2026 16:36:31 -0600 Subject: [PATCH 3/4] Add EventKit migration planning docs Co-Authored-By: Claude Sonnet 4.5 --- ARCHITECTURE.md | 371 +++++++++++++++++++++++++++++++++++++++++++++ EVENTKIT_README.md | 154 +++++++++++++++++++ EVENTKIT_TODO.md | 104 +++++++++++++ MIGRATION_PLAN.md | 212 ++++++++++++++++++++++++++ 4 files changed, 841 insertions(+) create mode 100644 ARCHITECTURE.md create mode 100644 EVENTKIT_README.md create mode 100644 EVENTKIT_TODO.md create mode 100644 MIGRATION_PLAN.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..ff8e3be --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,371 @@ +# icalBuddy Architecture + +## System Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ icalBuddy CLI │ +├─────────────────────────────────────────────────────────────────┤ +│ main() │ +│ ├── Argument Parsing (icalBuddyArgs.m) │ +│ ├── Config Loading │ +│ ├── Localization (icalBuddyL10N.m) │ +│ └── Command Dispatch │ +│ ├── calendars → printAllCalendars() │ +│ ├── eventsToday/eventsNow/eventsFrom → getEvents() │ +│ ├── uncompletedTasks/tasksDueBefore → getTasks() │ +│ └── editConfig → openConfigFileInEditor() │ +├─────────────────────────────────────────────────────────────────┤ +│ Data Layer (icalBuddyFunctions.m) │ +│ ├── getCalendars() ─────────┐ │ +│ ├── getEvents() │ │ +│ ├── getTasks() ├──→ EventKitStore.m │ +│ ├── sortCalItems() │ ├── EKEventStore (singleton) │ +│ └── putItemsUnderSections() │ ├── initEventStore() │ +│ │ └── initReminderAccess() │ +├──────────────────────────────┴──────────────────────────────────┤ +│ Output Layer │ +│ ├── icalBuddyPrettyPrint.m (formatting, output buffer) │ +│ ├── icalBuddyFormatting.m (ANSI colors, styles) │ +│ └── ANSIEscapeHelper.m (terminal escape codes) │ +├─────────────────────────────────────────────────────────────────┤ +│ Utilities │ +│ ├── HGDateFunctions.m (date manipulation) │ +│ ├── HGCLIUtils.m (CLI helpers) │ +│ └── HGUtils.m (general utilities) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ macOS Frameworks │ +├─────────────────────────────────────────────────────────────────┤ +│ EventKit.framework (NEW) │ +│ ├── EKEventStore - calendar database access │ +│ ├── EKCalendar - calendar containers │ +│ ├── EKEvent - calendar events │ +│ └── EKReminder - tasks/reminders │ +├─────────────────────────────────────────────────────────────────┤ +│ Other Frameworks │ +│ ├── Cocoa.framework - Foundation + AppKit │ +│ ├── AddressBook.framework - contact lookup for attendees │ +│ └── AppKit.framework - NSWorkspace, NSColor │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Module Descriptions + +### Core Modules + +#### icalBuddy.m +**Purpose:** Main entry point and command dispatcher +**Key Functions:** +- `main()` - argument processing, command routing +- `versionNumberStr()` - version string formatting + +#### icalBuddyFunctions.m +**Purpose:** Calendar data access and manipulation +**Key Functions:** +- `getCalendars(opts)` - retrieve filtered calendar list +- `getEvents(opts, calendars)` - query events by date range +- `getTasks(opts, calendars)` - query incomplete reminders +- `getCalItems(opts)` - unified entry point for events/tasks +- `sortCalItems(opts, calItems)` - sort by date/priority +- `putItemsUnderSections(opts, calItems)` - group for output +- `filterCalendars(cals, opts)` - apply include/exclude filters + +#### EventKitStore.m (NEW) +**Purpose:** EventKit initialization and permission handling +**Key Functions:** +- `initEventStore()` - create EKEventStore, request calendar access +- `initReminderAccess()` - request reminders access +**Global Variables:** +- `EKEventStore *eventStore` - singleton store instance + +### Output Modules + +#### icalBuddyPrettyPrint.m +**Purpose:** Format and buffer output for display +**Key Functions:** +- `initPrettyPrint(buffer, opts)` - initialize output system +- `printCalEvent(event, opts, now)` - format single event +- `printCalTask(task, opts)` - format single task +- `printItemSections(sections, opts)` - format grouped items +- `dateStr(date, printOption)` - format dates with relative names + +#### icalBuddyFormatting.m +**Purpose:** ANSI terminal formatting and colors +**Key Functions:** +- `initFormatting(configDict, separators)` - load formatting config +- `ansiEscapedStringWithAttributedString(str)` - convert to ANSI +- Color and style application functions + +### Configuration Modules + +#### icalBuddyArgs.m +**Purpose:** Command-line argument parsing +**Key Functions:** +- `readProgramArgs(opts, prettyPrintOpts, argc, argv)` +- `readArgsFromConfigFile(opts, prettyPrintOpts, path, configDict)` +- `processAppOptions(opts, prettyPrintOpts, separators)` + +#### icalBuddyL10N.m +**Purpose:** Localization support +**Key Functions:** +- `initL10N(filePath)` - load localization file +- `localizedStr(key)` - get localized string +- `localizedPriorityTitle(priority)` - priority names + +### Utility Modules + +#### HGDateFunctions.m +- `dateForStartOfDay(date)`, `dateForEndOfDay(date)` +- `dateByAddingDays(date, days)` +- `getDayDiff(date1, date2)` +- `datesRepresentSameDay(date1, date2)` +- `dateFromUserInput(str, description, isEndDate)` + +#### HGCLIUtils.m +- `Printf(format, ...)`, `PrintfErr(format, ...)` +- `DebugPrintf(format, ...)` +- `flushOutputBuffer(buffer, opts, keywords)` + +## Data Structures + +### AppOptions +```objc +typedef struct { + // Output type flags + BOOL output_is_eventsToday; + BOOL output_is_eventsNow; + BOOL output_is_eventsFromTo; + BOOL output_is_uncompletedTasks; + BOOL output_is_undatedUncompletedTasks; + BOOL output_is_tasksDueBefore; + + // Filter options + NSArray *includeCals; + NSArray *excludeCals; + NSArray *includeCalTypes; + NSArray *excludeCalTypes; + + // Display options + BOOL noCalendarNames; + BOOL noPropNames; + BOOL separateByCalendar; + BOOL separateByDate; + BOOL separateByPriority; + BOOL excludeAllDayEvents; + + // Date range (computed) + NSDate *startDate; + NSDate *endDate; + NSDate *dueBeforeDate; + + // ... more options +} AppOptions; +``` + +### PrettyPrintOptions +```objc +typedef struct { + NSString *prefixStrBullet; + NSString *prefixStrBulletAlert; + NSString *sectionSeparatorStr; + NSString *timeFormatStr; + NSString *dateFormatStr; + + BOOL displayRelativeDates; + BOOL excludeEndDates; + BOOL useCalendarColorsForTitles; + BOOL showUIDs; + + NSUInteger maxNumPrintedItems; + NSArray *propertyOrder; + // ... more options +} PrettyPrintOptions; +``` + +### CalItemPrintOption +```objc +typedef struct { + BOOL singleDay; + BOOL calendarAgnostic; + BOOL priorityAgnostic; + BOOL withoutPropNames; + BOOL calendarColorsForSectionTitles; + NSUInteger maxNumPrintedAttendees; + NSUInteger maxNumNoteCharacters; +} CalItemPrintOption; +``` + +### PrintSection +```objc +typedef struct { + NSString *title; + NSArray *items; + NSDate *contextDay; // For date-based sections +} PrintSection; +``` + +## EventKit Migration Details + +### Permission Flow + +``` +┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐ +│ User runs │────▶│ initEventStore()│────▶│ Check macOS │ +│ icalBuddy │ │ │ │ version │ +└──────────────┘ └─────────────────┘ └────────┬─────────┘ + │ + ┌────────────────────────────────┼────────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ + │ macOS 14+ │ │ macOS 10.14-13 │ │ macOS < 10.14 │ + │ requestFull │ │ requestAccess │ │ (implicit │ + │ AccessToEvents │ │ ToEntityType │ │ access) │ + └───────┬────────┘ └───────┬────────┘ └───────┬────────┘ + │ │ │ + ▼ ▼ ▼ + ┌────────────────────────────────────────────────────────────────────────────┐ + │ Wait on semaphore (sync) │ + └────────────────────────────────────────────────────────────────────────────┘ + │ + ┌─────────────────────────┴─────────────────────────┐ + │ │ + ▼ ▼ + ┌────────────────┐ ┌────────────────┐ + │ Access Granted │ │ Access Denied │ + │ Continue... │ │ Print error │ + └────────────────┘ │ Exit │ + └────────────────┘ +``` + +### Calendar Query Flow + +``` +getCalendars(opts) + │ + ▼ +initEventStore() ──────▶ EKEventStore created + │ + ▼ +calendarsForEntityType:EKEntityTypeEvent + │ + ▼ +filterCalendars() + ├── filterCalendarsByType() [by EKCalendarType] + └── filterCalendarsByNameOrUID() [by title or calendarIdentifier] + │ + ▼ +Return filtered NSArray +``` + +### Event Query Flow + +``` +getEvents(opts, calendars) + │ + ▼ +Calculate date range (startDate, endDate) + │ + ▼ +predicateForEventsWithStartDate:endDate:calendars: + │ + ▼ +eventsMatchingPredicate: + │ + ▼ +Filter all-day events (if excludeAllDayEvents) + │ + ▼ +Return NSArray +``` + +### Reminder Query Flow + +``` +getTasks(opts, calendars) + │ + ▼ +initReminderAccess() ──────▶ Request reminders permission + │ + ▼ +calendarsForEntityType:EKEntityTypeReminder + │ + ▼ +Filter to match requested calendar titles + │ + ▼ +predicateForIncompleteRemindersWithDueDateStarting:ending:calendars: + │ + ▼ +fetchRemindersMatchingPredicate:completion: (async) + │ + ▼ +Wait on semaphore (sync) + │ + ▼ +Filter undated (if output_is_undatedUncompletedTasks) + │ + ▼ +Return NSArray +``` + +## Build System + +### Makefile Targets + +| Target | Description | +|--------|-------------| +| `all` / `icalBuddy` | Build main binary | +| `testIcalBuddy` | Build with mock calendar store | +| `testRunner` | Build unit test runner | +| `clean` | Remove build artifacts | +| `docs` | Generate man pages | +| `package` | Create release zip | + +### Compiler Flags + +```makefile +COMPILER=clang +CC_WARN_OPTS=-Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations +FRAMEWORKS=-framework Cocoa -framework EventKit -framework AppKit -framework AddressBook +MIN_VERSION=-mmacosx-version-min=10.13 +``` + +## Configuration + +### Config File Location +`~/.icalBuddyConfig.plist` + +### Config Structure +```xml + + + + + formatting + + titleValue + bold + datetimeValue + yellow + + formattedKeywords + + today + green,bold + + + +``` + +## Error Handling + +| Error | Cause | User Message | +|-------|-------|--------------| +| No calendars | Permission denied or no calendars configured | "error: No calendars." | +| Calendar access denied | User denied permission in System Settings | "error: Calendar access denied. Please grant calendar access in System Settings > Privacy & Security > Calendars." | +| Reminders access denied | User denied reminders permission | "error: Reminders access denied." | +| Invalid date format | User provided unparseable date string | Date format help printed | diff --git a/EVENTKIT_README.md b/EVENTKIT_README.md new file mode 100644 index 0000000..2c6ea26 --- /dev/null +++ b/EVENTKIT_README.md @@ -0,0 +1,154 @@ +# icalBuddy64 - EventKit Migration + +A modernized fork of icalBuddy that uses Apple's EventKit framework instead of the deprecated CalendarStore framework. + +## Why This Fork? + +The original icalBuddy uses CalendarStore, which was deprecated in macOS 10.8 (2012). On modern macOS (10.14+), CalendarStore doesn't integrate with the privacy system (TCC), causing icalBuddy to report "No calendars" even when calendars exist and permissions are granted. + +This fork migrates to EventKit, which: +- Works with modern macOS privacy controls +- Triggers proper permission prompts +- Supports both Calendar and Reminders access +- Is actively maintained by Apple + +## Repository + +- **Fork:** https://github.com/mgunville/icalBuddy64 +- **Upstream:** https://github.com/dkaluta/icalBuddy64 +- **Branch:** `eventkit-migration` + +## Quick Start + +```bash +# Clone the fork +git clone https://github.com/mgunville/icalBuddy64.git +cd icalBuddy64 +git checkout eventkit-migration + +# Build +make clean && make + +# Test (will prompt for calendar access on first run) +./icalBuddy calendars +./icalBuddy eventsToday + +# Install +sudo cp icalBuddy /usr/local/bin/ +# OR +cp icalBuddy ~/bin/ +``` + +## Migration Status + +| Component | Status | +|-----------|--------| +| Core framework switch | Done | +| Calendar queries | Done | +| Event queries | Done | +| Task/Reminder queries | Done | +| Permission handling | Done | +| Pretty print output | In Progress | +| Calendar colors | In Progress | +| Testing | Pending | +| Documentation | Pending | + +## Documentation + +- [MIGRATION_PLAN.md](./MIGRATION_PLAN.md) - Detailed migration plan and timeline +- [ARCHITECTURE.md](./ARCHITECTURE.md) - System architecture and module descriptions + +## Key Changes from Upstream + +### 1. Framework Change +```diff +- #import ++ #import +``` + +### 2. Permission Handling +New `EventKitStore.m` handles permission requests synchronously: +```objc +BOOL initEventStore(void); // Request calendar access +BOOL initReminderAccess(void); // Request reminders access +``` + +### 3. API Updates +| Old (CalendarStore) | New (EventKit) | +|---------------------|----------------| +| `[CalCalendarStore defaultCalendarStore]` | `eventStore` (global) | +| `[cal uid]` | `[cal calendarIdentifier]` | +| `[task dueDate]` | `[reminder dueDateComponents]` | +| Sync task queries | Async with semaphore | + +## Usage Examples + +```bash +# List all calendars +icalBuddy calendars + +# Today's events +icalBuddy eventsToday + +# Events for next 7 days +icalBuddy eventsToday+7 + +# Events from specific calendar +icalBuddy -ic "Work" eventsToday + +# Formatted output (for Obsidian Templater) +icalBuddy -npn -nc -ps "/ - /" -iep "datetime,title" \ + -po "datetime,title" -b "###### " -tf "%H%M" \ + -ic "Work" eventsToday + +# Uncompleted tasks +icalBuddy uncompletedTasks + +# Tasks due before a date +icalBuddy tasksDueBefore:today+7 +``` + +## Troubleshooting + +### "No calendars" Error + +1. **Check System Settings:** + - System Settings → Privacy & Security → Calendars + - Ensure Terminal (or your app) has access + +2. **Reset permissions and retry:** + ```bash + tccutil reset Calendar + ./icalBuddy calendars # Will prompt again + ``` + +3. **Check calendar exists:** + ```bash + # Via AppleScript (bypasses TCC issues) + osascript -e 'tell application "Calendar" to get name of calendars' + ``` + +### Permission Not Prompting + +The permission prompt only appears once. If denied: +1. Open System Settings → Privacy & Security → Calendars +2. Manually add Terminal/iTerm/your app +3. Toggle access on + +## Contributing + +1. Fork the repository +2. Create a feature branch from `eventkit-migration` +3. Make changes with appropriate `#ifdef USE_MOCKED_CALENDARSTORE` guards +4. Test with `make && ./icalBuddy eventsToday` +5. Submit a pull request + +## License + +MIT License - see original icalBuddy license. + +## Credits + +- **Original Author:** Ali Rantakari (http://hasseg.org/icalBuddy) +- **64-bit Fork:** dkaluta +- **EventKit Migration:** mgunville diff --git a/EVENTKIT_TODO.md b/EVENTKIT_TODO.md new file mode 100644 index 0000000..6da0a15 --- /dev/null +++ b/EVENTKIT_TODO.md @@ -0,0 +1,104 @@ +# icalBuddy EventKit Migration - TODO + +## Remaining Work + +### High Priority (Required for MVP) + +- [x] **icalBuddyPrettyPrint.m** - Update for EventKit types + - [x] `printCalEvent()` - EKEvent property access + - [x] `printCalTask()` - EKReminder property access (dueDateComponents → NSDate) + - [x] Calendar color access via CGColor instead of NSColor + +- [x] **icalBuddyFormatting.m** - Update color handling + - [x] Convert CGColorRef to NSColor for ANSI color mapping + - [x] Update `closestSGRCodeForColor:` calls + +- [x] **Build & Test** + - [x] Successful compilation with no errors + - [x] Test `calendars` command + - [x] Test `eventsToday` command + - [ ] Test `uncompletedTasks` command (requires Reminders permission) + - [x] Test with calendar filters (-ic, -ec) + +### Medium Priority (Full Compatibility) + +- [x] **icalBuddyPrettyPrint.h** - No changes needed (uses typedefs) + +- [ ] **Testing** + - [x] Test `eventsToday+N` (multi-day) - works + - [ ] Test `eventsFrom:X to:Y` (date range) + - [ ] Test `eventsNow` (current events) + - [ ] Test `tasksDueBefore:DATE` + - [ ] Test `undatedUncompletedTasks` + - [x] Test `-sc` (separate by calendar) - works + - [ ] Test `-sd` (separate by date) + - [ ] Test `-sp` (separate by priority) + - [x] Test formatting options (-tf, -df, -b, -ps, etc.) - works + +- [ ] **Edge Cases** + - [x] All-day events + - [ ] Multi-day events + - [ ] Recurring events + - [ ] Events with no end time + - [ ] Tasks with no due date + - [ ] Tasks with priority + +### Low Priority (Polish) + +- [ ] **Documentation** + - [ ] Update man page (icalBuddy.pod) + - [ ] Update FAQ + - [ ] Add EventKit-specific notes + +- [ ] **Code Quality** + - [ ] Remove debug logging + - [ ] Clean up #ifdef blocks where possible + - [ ] Add comments for EventKit-specific code + +- [ ] **Release** + - [ ] Bump version number + - [ ] Create GitHub release + - [ ] Update upstream README with fork info + +## Files Changed Summary + +``` +Modified: + calendarStoreImport.h - EventKit imports and typedefs + Makefile - EventKit framework, compiler flags + icalBuddyFunctions.m - Core calendar/event/task queries + icalBuddy.m - Type casts in print loop + icalBuddyPrettyPrint.m - Output formatting (EventKit types) + icalBuddyFormatting.m - ANSI colors (CGColor handling) + icalBuddyFormatting.h - Import header update + +New: + EventKitStore.m - EKEventStore singleton, permissions +``` + +## Current Status + +**BUILD: PASSING** ✓ +**CALENDARS: WORKING** ✓ +**EVENTS: WORKING** ✓ +**REMINDERS: Requires permission grant** + +## Notes + +- Keep `#ifdef USE_MOCKED_CALENDARSTORE` guards for test compatibility +- EventKit reminders API is async - using semaphores for sync behavior +- Permission requests happen on first access, not app launch +- CGColor API requires macOS 10.15+ (fallback to nil for older versions) +- EventKit uses `URL` property (capitalized), not `url` +- EventKit uses `calendarItemIdentifier` instead of `uid` + +## Installation + +Binary installed to: `~/bin/icalBuddy` +Symlink at: `/usr/local/bin/icalBuddy` → `~/bin/icalBuddy` + +## Repository + +- Fork: https://github.com/mgunville/icalBuddy64 +- Branch: `eventkit-migration` +- Commit: 7d44235 diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md new file mode 100644 index 0000000..148bf29 --- /dev/null +++ b/MIGRATION_PLAN.md @@ -0,0 +1,212 @@ +# icalBuddy EventKit Migration Plan + +## Overview + +This document details the migration of icalBuddy from the deprecated CalendarStore framework to the modern EventKit framework. This migration is necessary because CalendarStore was deprecated in macOS 10.8 and does not integrate with modern macOS privacy controls (TCC - Transparency, Consent, and Control). + +**Repository:** https://github.com/mgunville/icalBuddy64 (forked from dkaluta/icalBuddy64) +**Branch:** `eventkit-migration` + +## Problem Statement + +### Current Issues +1. **Privacy Integration Failure**: CalendarStore doesn't trigger macOS privacy prompts, resulting in "No calendars" errors even when calendars exist +2. **Deprecated APIs**: CalendarStore has been deprecated since macOS 10.8 (2012) +3. **No Reminders Support**: Modern Reminders are managed through EventKit, not CalendarStore +4. **Future Compatibility**: CalendarStore may be removed entirely in future macOS versions + +### Impact +- Users cannot use icalBuddy with modern macOS (10.14+) without workarounds +- Obsidian Templater plugin integration broken for calendar queries +- CLI automation scripts fail silently + +## Solution Architecture + +### Framework Migration Map + +| CalendarStore | EventKit | Notes | +|--------------|----------|-------| +| `CalCalendarStore` | `EKEventStore` | Singleton with permission handling | +| `CalCalendar` | `EKCalendar` | Property differences (uid → calendarIdentifier) | +| `CalEvent` | `EKEvent` | Mostly compatible | +| `CalTask` | `EKReminder` | Significant API differences | +| `CalCalendarItem` | `EKCalendarItem` | Base class for events/reminders | +| `CalPriority` | `NSUInteger` | Same values (0, 1, 5, 9) | + +### Key API Changes + +#### Calendar Access +```objc +// OLD (CalendarStore) +[[CalCalendarStore defaultCalendarStore] calendars] + +// NEW (EventKit) +[eventStore calendarsForEntityType:EKEntityTypeEvent] +``` + +#### Event Predicates +```objc +// OLD +[CalCalendarStore eventPredicateWithStartDate:endDate:calendars:] +[[CalCalendarStore defaultCalendarStore] eventsWithPredicate:] + +// NEW +[eventStore predicateForEventsWithStartDate:endDate:calendars:] +[eventStore eventsMatchingPredicate:] +``` + +#### Task/Reminder Predicates +```objc +// OLD +[CalCalendarStore taskPredicateWithUncompletedTasks:] +[[CalCalendarStore defaultCalendarStore] tasksWithPredicate:] + +// NEW (async!) +[eventStore predicateForIncompleteRemindersWithDueDateStarting:ending:calendars:] +[eventStore fetchRemindersMatchingPredicate:completion:] +``` + +#### Property Access +```objc +// Calendar UID +// OLD: [calendar uid] +// NEW: [calendar calendarIdentifier] + +// Task Due Date +// OLD: [task dueDate] +// NEW: [[NSCalendar currentCalendar] dateFromComponents:[reminder dueDateComponents]] + +// Task Completion +// OLD: [task isCompleted] +// NEW: [reminder isCompleted] +``` + +### Permission Handling + +EventKit requires explicit permission requests. The migration adds: + +```objc +// macOS 14+ +[eventStore requestFullAccessToEventsWithCompletion:^(BOOL granted, NSError *error) { ... }]; + +// macOS 10.14-13.x +[eventStore requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError *error) { ... }]; +``` + +Permissions are requested synchronously using dispatch semaphores to maintain CLI compatibility. + +## File-by-File Migration Status + +| File | Status | Changes Required | +|------|--------|------------------| +| `calendarStoreImport.h` | **DONE** | Add EventKit import, typedefs, extern declarations | +| `Makefile` | **DONE** | Replace CalendarStore with EventKit framework, fix flags | +| `EventKitStore.m` | **NEW** | Global EKEventStore, permission handling | +| `icalBuddyFunctions.m` | **DONE** | Core calendar/event/task queries | +| `icalBuddy.m` | **DONE** | Type casts in print loop | +| `icalBuddyPrettyPrint.m` | **TODO** | Calendar color access, task properties | +| `icalBuddyPrettyPrint.h` | **TODO** | Function signatures | +| `icalBuddyFormatting.m` | **TODO** | Calendar color for ANSI output | +| `icalBuddyFormatting.h` | **TODO** | Type updates if needed | + +## Detailed Changes by File + +### 1. calendarStoreImport.h +- Conditionally import EventKit vs CalendarStore based on `USE_MOCKED_CALENDARSTORE` +- Define compatibility typedefs (`CalCalendar` → `EKCalendar`, etc.) +- Declare global `eventStore` and `initEventStore()` function +- Define `CalPriority` enum matching CalendarStore values + +### 2. EventKitStore.m (NEW) +- Global `EKEventStore *eventStore` instance +- `initEventStore()` - request calendar access synchronously +- `initReminderAccess()` - request reminders access for tasks +- Version-aware API selection (@available checks) + +### 3. icalBuddyFunctions.m +- `getCalendars()` - use `calendarsForEntityType:` +- `getEvents()` - use `predicateForEventsWithStartDate:` and `eventsMatchingPredicate:` +- `getTasks()` - use `predicateForIncompleteRemindersWithDueDateStarting:` and async `fetchRemindersMatchingPredicate:` +- `filterCalendarsByNameOrUID()` - use `calendarIdentifier` instead of `uid` +- `filterCalendarsByType()` - use `EKCalendarType` enum values +- `prioritySort()` - handle `dueDateComponents` instead of `dueDate` +- `putItemsUnderSections()` - update for EKReminder's `dueDateComponents` + +### 4. icalBuddy.m +- Update print loop to use `EKEvent`/`EKReminder` types + +### 5. icalBuddyPrettyPrint.m (TODO) +- `printCalEvent()` - update for EKEvent properties +- `printCalTask()` - update for EKReminder properties (dueDateComponents, priority) +- Calendar color access via `[calendar CGColor]` instead of `[calendar color]` + +### 6. icalBuddyFormatting.m (TODO) +- Update `closestSGRCodeForColor:` calls to use CGColor + +## Testing Plan + +### Unit Tests +1. Calendar listing with various filter options +2. Event queries (today, date range, now) +3. Task queries (uncompleted, due before, undated) +4. Priority sorting +5. Date-based sectioning + +### Integration Tests +1. Basic command: `icalBuddy eventsToday` +2. Calendar filter: `icalBuddy -ic "Work" eventsToday` +3. Date range: `icalBuddy eventsFrom:today to:today+7` +4. Tasks: `icalBuddy uncompletedTasks` +5. Formatting options: `-tf`, `-df`, `-b`, `-nc`, etc. + +### Manual Testing +1. First run permission prompt +2. Permission denied handling +3. Multiple calendar sources (iCloud, Exchange, Local) +4. Obsidian Templater integration + +## Build Instructions + +```bash +cd /tmp/icalBuddy64 +git checkout eventkit-migration +make clean +make +./icalBuddy calendars # Test calendar access +./icalBuddy eventsToday +``` + +## Installation + +```bash +# After successful build +sudo cp icalBuddy /usr/local/bin/ +# OR for user-local install +cp icalBuddy ~/bin/ +``` + +## Rollback Strategy + +The migration preserves the original CalendarStore code path via `#ifdef USE_MOCKED_CALENDARSTORE`. This allows: +1. Testing with mock calendar store +2. Easy comparison of old vs new behavior +3. Potential rollback if issues discovered + +## Timeline + +| Phase | Tasks | Status | +|-------|-------|--------| +| 1. Setup | Fork repo, create branch | DONE | +| 2. Core Migration | calendarStoreImport.h, EventKitStore.m, icalBuddyFunctions.m | DONE | +| 3. Main App | icalBuddy.m updates | DONE | +| 4. Pretty Print | icalBuddyPrettyPrint.m, icalBuddyFormatting.m | IN PROGRESS | +| 5. Testing | Build, test all commands | PENDING | +| 6. Documentation | Update README, man pages | PENDING | +| 7. Release | PR, merge, tag release | PENDING | + +## References + +- [EventKit Framework Reference](https://developer.apple.com/documentation/eventkit) +- [CalendarStore Framework (Deprecated)](https://developer.apple.com/documentation/calendarstore) +- [TCC Privacy Controls](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_personal-information_calendars) +- [Original icalBuddy](http://hasseg.org/icalBuddy) From 1ae38a5825943a57233d6a3ac1cb4ada608a3305 Mon Sep 17 00:00:00 2001 From: Mike Gunville Date: Sat, 28 Feb 2026 10:55:53 -0600 Subject: [PATCH 4/4] chore: harden secret handling and bootstrap docs --- .gitignore | 13 +++++++++++++ .pre-commit-config.yaml | 5 +++++ SECURITY_BOOTSTRAP.md | 31 +++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 SECURITY_BOOTSTRAP.md diff --git a/.gitignore b/.gitignore index 507000b..0985304 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,16 @@ tests/regression/*.log # other deploymentScpTarget .hg* + +# BEGIN CREDENTIAL HARDENING (managed) +.env +.env.* +!.env.example +.credentials/ +credentials.json +client_secret*.json +token*.json +*.pem +*.p12 +*.key +# END CREDENTIAL HARDENING (managed) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b8a004b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.24.2 + hooks: + - id: gitleaks diff --git a/SECURITY_BOOTSTRAP.md b/SECURITY_BOOTSTRAP.md new file mode 100644 index 0000000..76d69e1 --- /dev/null +++ b/SECURITY_BOOTSTRAP.md @@ -0,0 +1,31 @@ +# Security Bootstrap + +This repo is configured with a pre-commit gitleaks hook and hardened secret ignores. + +## One-Time Setup + +1. Install pre-commit (if not already installed): + +```bash +python3 -m pip install --user pre-commit +``` + +2. Install hooks in this repo: + +```bash +cd /Users/mike/Documents/Dev/agentic_Projects/projects/icalBuddy +pre-commit install --install-hooks +``` + +3. Validate current tree: + +```bash +cd /Users/mike/Documents/Dev/agentic_Projects/projects/icalBuddy +pre-commit run --all-files +``` + +## Notes + +- Real secrets must not be committed. +- Use `.env.example` for placeholders only. +- `.gitignore` includes a managed credentials hardening block.