eventkit/
lib.rs

1//! # EventKit-RS
2//!
3//! A Rust library for interacting with macOS Calendar and Reminders via EventKit.
4//!
5//! This library provides safe wrappers around the Apple EventKit framework to:
6//! - Request and check authorization for calendar and reminders access
7//! - List, create, update, and delete calendar events
8//! - List, create, update, and delete reminders
9//! - Manage calendars and reminder lists
10//!
11//! ## Quick Start
12//!
13//! ```rust,no_run
14//! use eventkit::{RemindersManager, EventsManager, Result};
15//!
16//! fn main() -> Result<()> {
17//!     // Working with reminders
18//!     let reminders = RemindersManager::new();
19//!     reminders.request_access()?;
20//!
21//!     for reminder in reminders.fetch_incomplete_reminders()? {
22//!         println!("Todo: {}", reminder.title);
23//!     }
24//!
25//!     // Working with calendar events
26//!     let events = EventsManager::new();
27//!     events.request_access()?;
28//!
29//!     for event in events.fetch_today_events()? {
30//!         println!("Event: {}", event.title);
31//!     }
32//!
33//!     Ok(())
34//! }
35//! ```
36//!
37//! ## Platform Support
38//!
39//! This library only works on macOS. It requires macOS 10.14 or later for full functionality.
40//!
41//! ## Privacy Permissions
42//!
43//! Your application will need to request calendar and/or reminders permissions.
44//! Make sure to include the appropriate keys in your `Info.plist`:
45//!
46//! - `NSRemindersUsageDescription` - for reminders access
47//! - `NSCalendarsFullAccessUsageDescription` - for calendar access (macOS 14+)
48//! - `NSCalendarsUsageDescription` - for calendar access (older macOS)
49
50use block2::RcBlock;
51use chrono::{DateTime, Duration, Local, TimeZone};
52use objc2::Message;
53use objc2::rc::Retained;
54use objc2::runtime::Bool;
55use objc2_event_kit::{
56    EKAuthorizationStatus, EKCalendar, EKEntityType, EKEvent, EKEventStore, EKReminder, EKSpan,
57};
58use objc2_foundation::{NSArray, NSDate, NSError, NSString};
59use std::sync::{Arc, Condvar, Mutex};
60use thiserror::Error;
61
62/// Errors that can occur when working with EventKit
63#[derive(Error, Debug)]
64pub enum EventKitError {
65    #[error("Authorization denied")]
66    AuthorizationDenied,
67
68    #[error("Authorization restricted by system policy")]
69    AuthorizationRestricted,
70
71    #[error("Authorization not determined")]
72    AuthorizationNotDetermined,
73
74    #[error("Failed to request authorization: {0}")]
75    AuthorizationRequestFailed(String),
76
77    #[error("No default calendar")]
78    NoDefaultCalendar,
79
80    #[error("Calendar not found: {0}")]
81    CalendarNotFound(String),
82
83    #[error("Item not found: {0}")]
84    ItemNotFound(String),
85
86    #[error("Failed to save: {0}")]
87    SaveFailed(String),
88
89    #[error("Failed to delete: {0}")]
90    DeleteFailed(String),
91
92    #[error("Failed to fetch: {0}")]
93    FetchFailed(String),
94
95    #[error("EventKit error: {0}")]
96    EventKitError(String),
97
98    #[error("Invalid date range")]
99    InvalidDateRange,
100}
101
102/// Backward compatibility alias
103pub type RemindersError = EventKitError;
104
105/// Result type for EventKit operations
106pub type Result<T> = std::result::Result<T, EventKitError>;
107
108/// Represents a reminder item with its properties
109#[derive(Debug, Clone)]
110pub struct ReminderItem {
111    /// Unique identifier for the reminder
112    pub identifier: String,
113    /// Title of the reminder
114    pub title: String,
115    /// Optional notes/description
116    pub notes: Option<String>,
117    /// Whether the reminder is completed
118    pub completed: bool,
119    /// Priority (0 = none, 1-4 = high, 5 = medium, 6-9 = low)
120    pub priority: usize,
121    /// Calendar/list the reminder belongs to
122    pub calendar_title: Option<String>,
123}
124
125/// Represents a calendar (reminder list)
126#[derive(Debug, Clone)]
127pub struct CalendarInfo {
128    /// Unique identifier
129    pub identifier: String,
130    /// Title of the calendar
131    pub title: String,
132    /// Source name (e.g., iCloud, Local)
133    pub source: Option<String>,
134    /// Whether content can be modified
135    pub allows_modifications: bool,
136}
137
138/// The main reminders manager providing access to EventKit functionality
139pub struct RemindersManager {
140    store: Retained<EKEventStore>,
141}
142
143impl RemindersManager {
144    /// Creates a new RemindersManager instance
145    pub fn new() -> Self {
146        let store = unsafe { EKEventStore::new() };
147        Self { store }
148    }
149
150    /// Gets the current authorization status for reminders
151    pub fn authorization_status() -> AuthorizationStatus {
152        let status =
153            unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Reminder) };
154        status.into()
155    }
156
157    /// Requests full access to reminders (blocking)
158    ///
159    /// Returns Ok(true) if access was granted, Ok(false) if denied
160    pub fn request_access(&self) -> Result<bool> {
161        let result = Arc::new((Mutex::new(None::<(bool, Option<String>)>), Condvar::new()));
162        let result_clone = Arc::clone(&result);
163
164        let completion = RcBlock::new(move |granted: Bool, error: *mut NSError| {
165            let error_msg = if !error.is_null() {
166                let error_ref = unsafe { &*error };
167                Some(format!("{:?}", error_ref))
168            } else {
169                None
170            };
171
172            let (lock, cvar) = &*result_clone;
173            let mut res = lock.lock().unwrap();
174            *res = Some((granted.as_bool(), error_msg));
175            cvar.notify_one();
176        });
177
178        unsafe {
179            // Convert RcBlock to raw pointer for the API
180            let block_ptr = &*completion as *const _ as *mut _;
181            self.store
182                .requestFullAccessToRemindersWithCompletion(block_ptr);
183        }
184
185        let (lock, cvar) = &*result;
186        let mut res = lock.lock().unwrap();
187        while res.is_none() {
188            res = cvar.wait(res).unwrap();
189        }
190
191        match res.take() {
192            Some((granted, None)) => Ok(granted),
193            Some((_, Some(error))) => Err(RemindersError::AuthorizationRequestFailed(error)),
194            None => Err(RemindersError::AuthorizationRequestFailed(
195                "Unknown error".to_string(),
196            )),
197        }
198    }
199
200    /// Ensures we have authorization, requesting if needed
201    pub fn ensure_authorized(&self) -> Result<()> {
202        match Self::authorization_status() {
203            AuthorizationStatus::FullAccess => Ok(()),
204            AuthorizationStatus::NotDetermined => {
205                if self.request_access()? {
206                    Ok(())
207                } else {
208                    Err(RemindersError::AuthorizationDenied)
209                }
210            }
211            AuthorizationStatus::Denied => Err(RemindersError::AuthorizationDenied),
212            AuthorizationStatus::Restricted => Err(RemindersError::AuthorizationRestricted),
213            AuthorizationStatus::WriteOnly => Ok(()), // Can still read with write-only in some cases
214        }
215    }
216
217    /// Lists all reminder calendars (lists)
218    pub fn list_calendars(&self) -> Result<Vec<CalendarInfo>> {
219        self.ensure_authorized()?;
220
221        let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
222
223        let mut result = Vec::new();
224        for calendar in calendars.iter() {
225            result.push(calendar_to_info(&calendar));
226        }
227
228        Ok(result)
229    }
230
231    /// Gets the default calendar for new reminders
232    pub fn default_calendar(&self) -> Result<CalendarInfo> {
233        self.ensure_authorized()?;
234
235        let calendar = unsafe { self.store.defaultCalendarForNewReminders() };
236
237        match calendar {
238            Some(cal) => Ok(calendar_to_info(&cal)),
239            None => Err(RemindersError::NoDefaultCalendar),
240        }
241    }
242
243    /// Fetches all reminders (blocking)
244    pub fn fetch_all_reminders(&self) -> Result<Vec<ReminderItem>> {
245        self.fetch_reminders(None)
246    }
247
248    /// Fetches reminders from specific calendars (blocking)
249    pub fn fetch_reminders(&self, calendar_titles: Option<&[&str]>) -> Result<Vec<ReminderItem>> {
250        self.ensure_authorized()?;
251
252        let calendars: Option<Retained<NSArray<EKCalendar>>> = match calendar_titles {
253            Some(titles) => {
254                let all_calendars =
255                    unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
256                let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
257
258                for cal in all_calendars.iter() {
259                    let title = unsafe { cal.title() };
260                    let title_str = title.to_string();
261                    if titles.iter().any(|t| *t == title_str) {
262                        matching.push(cal.retain());
263                    }
264                }
265
266                if matching.is_empty() {
267                    return Err(RemindersError::CalendarNotFound(titles.join(", ")));
268                }
269
270                Some(NSArray::from_retained_slice(&matching))
271            }
272            None => None,
273        };
274
275        let predicate = unsafe {
276            self.store
277                .predicateForRemindersInCalendars(calendars.as_deref())
278        };
279
280        let result = Arc::new((Mutex::new(None::<Vec<ReminderItem>>), Condvar::new()));
281        let result_clone = Arc::clone(&result);
282
283        let completion = RcBlock::new(move |reminders: *mut NSArray<EKReminder>| {
284            let items = if reminders.is_null() {
285                Vec::new()
286            } else {
287                let reminders = unsafe { Retained::retain(reminders).unwrap() };
288                reminders.iter().map(|r| reminder_to_item(&r)).collect()
289            };
290            let (lock, cvar) = &*result_clone;
291            let mut guard = lock.lock().unwrap();
292            *guard = Some(items);
293            cvar.notify_one();
294        });
295
296        unsafe {
297            self.store
298                .fetchRemindersMatchingPredicate_completion(&predicate, &completion);
299        }
300
301        let (lock, cvar) = &*result;
302        let mut guard = lock.lock().unwrap();
303        while guard.is_none() {
304            guard = cvar.wait(guard).unwrap();
305        }
306
307        guard
308            .take()
309            .ok_or_else(|| RemindersError::FetchFailed("Unknown error".to_string()))
310    }
311
312    /// Fetches incomplete reminders
313    pub fn fetch_incomplete_reminders(&self) -> Result<Vec<ReminderItem>> {
314        self.ensure_authorized()?;
315
316        let predicate = unsafe {
317            self.store
318                .predicateForIncompleteRemindersWithDueDateStarting_ending_calendars(
319                    None, None, None,
320                )
321        };
322
323        let result = Arc::new((Mutex::new(None::<Vec<ReminderItem>>), Condvar::new()));
324        let result_clone = Arc::clone(&result);
325
326        let completion = RcBlock::new(move |reminders: *mut NSArray<EKReminder>| {
327            let items = if reminders.is_null() {
328                Vec::new()
329            } else {
330                let reminders = unsafe { Retained::retain(reminders).unwrap() };
331                reminders.iter().map(|r| reminder_to_item(&r)).collect()
332            };
333            let (lock, cvar) = &*result_clone;
334            let mut guard = lock.lock().unwrap();
335            *guard = Some(items);
336            cvar.notify_one();
337        });
338
339        unsafe {
340            self.store
341                .fetchRemindersMatchingPredicate_completion(&predicate, &completion);
342        }
343
344        let (lock, cvar) = &*result;
345        let mut guard = lock.lock().unwrap();
346        while guard.is_none() {
347            guard = cvar.wait(guard).unwrap();
348        }
349
350        guard
351            .take()
352            .ok_or_else(|| RemindersError::FetchFailed("Unknown error".to_string()))
353    }
354
355    /// Creates a new reminder
356    pub fn create_reminder(
357        &self,
358        title: &str,
359        notes: Option<&str>,
360        calendar_title: Option<&str>,
361        priority: Option<usize>,
362    ) -> Result<ReminderItem> {
363        self.ensure_authorized()?;
364
365        let reminder = unsafe { EKReminder::reminderWithEventStore(&self.store) };
366
367        // Set title
368        let ns_title = NSString::from_str(title);
369        unsafe { reminder.setTitle(Some(&ns_title)) };
370
371        // Set notes if provided
372        if let Some(notes_text) = notes {
373            let ns_notes = NSString::from_str(notes_text);
374            unsafe { reminder.setNotes(Some(&ns_notes)) };
375        }
376
377        // Set priority if provided
378        if let Some(p) = priority {
379            unsafe { reminder.setPriority(p) };
380        }
381
382        // Set calendar
383        let calendar = if let Some(cal_title) = calendar_title {
384            self.find_calendar_by_title(cal_title)?
385        } else {
386            unsafe { self.store.defaultCalendarForNewReminders() }
387                .ok_or(RemindersError::NoDefaultCalendar)?
388        };
389        unsafe { reminder.setCalendar(Some(&calendar)) };
390
391        // Save
392        unsafe {
393            self.store
394                .saveReminder_commit_error(&reminder, true)
395                .map_err(|e| RemindersError::SaveFailed(format!("{:?}", e)))?;
396        }
397
398        Ok(reminder_to_item(&reminder))
399    }
400
401    /// Updates an existing reminder
402    pub fn update_reminder(
403        &self,
404        identifier: &str,
405        title: Option<&str>,
406        notes: Option<&str>,
407        completed: Option<bool>,
408        priority: Option<usize>,
409    ) -> Result<ReminderItem> {
410        self.ensure_authorized()?;
411
412        let reminder = self.find_reminder_by_id(identifier)?;
413
414        if let Some(t) = title {
415            let ns_title = NSString::from_str(t);
416            unsafe { reminder.setTitle(Some(&ns_title)) };
417        }
418
419        if let Some(n) = notes {
420            let ns_notes = NSString::from_str(n);
421            unsafe { reminder.setNotes(Some(&ns_notes)) };
422        }
423
424        if let Some(c) = completed {
425            unsafe { reminder.setCompleted(c) };
426        }
427
428        if let Some(p) = priority {
429            unsafe { reminder.setPriority(p) };
430        }
431
432        unsafe {
433            self.store
434                .saveReminder_commit_error(&reminder, true)
435                .map_err(|e| RemindersError::SaveFailed(format!("{:?}", e)))?;
436        }
437
438        Ok(reminder_to_item(&reminder))
439    }
440
441    /// Marks a reminder as complete
442    pub fn complete_reminder(&self, identifier: &str) -> Result<ReminderItem> {
443        self.update_reminder(identifier, None, None, Some(true), None)
444    }
445
446    /// Marks a reminder as incomplete
447    pub fn uncomplete_reminder(&self, identifier: &str) -> Result<ReminderItem> {
448        self.update_reminder(identifier, None, None, Some(false), None)
449    }
450
451    /// Deletes a reminder
452    pub fn delete_reminder(&self, identifier: &str) -> Result<()> {
453        self.ensure_authorized()?;
454
455        let reminder = self.find_reminder_by_id(identifier)?;
456
457        unsafe {
458            self.store
459                .removeReminder_commit_error(&reminder, true)
460                .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
461        }
462
463        Ok(())
464    }
465
466    /// Gets a reminder by its identifier
467    pub fn get_reminder(&self, identifier: &str) -> Result<ReminderItem> {
468        self.ensure_authorized()?;
469        let reminder = self.find_reminder_by_id(identifier)?;
470        Ok(reminder_to_item(&reminder))
471    }
472
473    // Helper to find a calendar by title
474    fn find_calendar_by_title(&self, title: &str) -> Result<Retained<EKCalendar>> {
475        let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
476
477        for cal in calendars.iter() {
478            let cal_title = unsafe { cal.title() };
479            if cal_title.to_string() == title {
480                return Ok(cal.retain());
481            }
482        }
483
484        Err(RemindersError::CalendarNotFound(title.to_string()))
485    }
486
487    // Helper to find a reminder by identifier
488    fn find_reminder_by_id(&self, identifier: &str) -> Result<Retained<EKReminder>> {
489        let ns_id = NSString::from_str(identifier);
490        let item = unsafe { self.store.calendarItemWithIdentifier(&ns_id) };
491
492        match item {
493            Some(item) => {
494                // Try to downcast to EKReminder
495                if let Some(reminder) = item.downcast_ref::<EKReminder>() {
496                    Ok(reminder.retain())
497                } else {
498                    Err(EventKitError::ItemNotFound(identifier.to_string()))
499                }
500            }
501            None => Err(EventKitError::ItemNotFound(identifier.to_string())),
502        }
503    }
504}
505
506impl Default for RemindersManager {
507    fn default() -> Self {
508        Self::new()
509    }
510}
511
512/// Authorization status for reminders access
513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
514pub enum AuthorizationStatus {
515    /// User has not yet made a choice
516    NotDetermined,
517    /// Access restricted by system policy
518    Restricted,
519    /// User explicitly denied access
520    Denied,
521    /// Full access granted
522    FullAccess,
523    /// Write-only access granted
524    WriteOnly,
525}
526
527impl From<EKAuthorizationStatus> for AuthorizationStatus {
528    fn from(status: EKAuthorizationStatus) -> Self {
529        if status == EKAuthorizationStatus::NotDetermined {
530            AuthorizationStatus::NotDetermined
531        } else if status == EKAuthorizationStatus::Restricted {
532            AuthorizationStatus::Restricted
533        } else if status == EKAuthorizationStatus::Denied {
534            AuthorizationStatus::Denied
535        } else if status == EKAuthorizationStatus::FullAccess {
536            AuthorizationStatus::FullAccess
537        } else if status == EKAuthorizationStatus::WriteOnly {
538            AuthorizationStatus::WriteOnly
539        } else {
540            AuthorizationStatus::NotDetermined
541        }
542    }
543}
544
545impl std::fmt::Display for AuthorizationStatus {
546    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547        match self {
548            AuthorizationStatus::NotDetermined => write!(f, "Not Determined"),
549            AuthorizationStatus::Restricted => write!(f, "Restricted"),
550            AuthorizationStatus::Denied => write!(f, "Denied"),
551            AuthorizationStatus::FullAccess => write!(f, "Full Access"),
552            AuthorizationStatus::WriteOnly => write!(f, "Write Only"),
553        }
554    }
555}
556
557// Helper function to convert EKReminder to ReminderItem
558fn reminder_to_item(reminder: &EKReminder) -> ReminderItem {
559    let identifier = unsafe { reminder.calendarItemIdentifier() }.to_string();
560    let title = unsafe { reminder.title() }.to_string();
561    let notes = unsafe { reminder.notes() }.map(|n| n.to_string());
562    let completed = unsafe { reminder.isCompleted() };
563    let priority = unsafe { reminder.priority() };
564    let calendar_title = unsafe { reminder.calendar() }.map(|c| unsafe { c.title() }.to_string());
565
566    ReminderItem {
567        identifier,
568        title,
569        notes,
570        completed,
571        priority,
572        calendar_title,
573    }
574}
575
576// Helper function to convert EKCalendar to CalendarInfo
577fn calendar_to_info(calendar: &EKCalendar) -> CalendarInfo {
578    let identifier = unsafe { calendar.calendarIdentifier() }.to_string();
579    let title = unsafe { calendar.title() }.to_string();
580    let source = unsafe { calendar.source() }.map(|s| unsafe { s.title() }.to_string());
581    let allows_modifications = unsafe { calendar.allowsContentModifications() };
582
583    CalendarInfo {
584        identifier,
585        title,
586        source,
587        allows_modifications,
588    }
589}
590
591// ============================================================================
592// Calendar Events Support
593// ============================================================================
594
595/// Represents a calendar event with its properties
596#[derive(Debug, Clone)]
597pub struct EventItem {
598    /// Unique identifier for the event
599    pub identifier: String,
600    /// Title of the event
601    pub title: String,
602    /// Optional notes/description
603    pub notes: Option<String>,
604    /// Optional location
605    pub location: Option<String>,
606    /// Start date/time
607    pub start_date: DateTime<Local>,
608    /// End date/time
609    pub end_date: DateTime<Local>,
610    /// Whether this is an all-day event
611    pub all_day: bool,
612    /// Calendar the event belongs to
613    pub calendar_title: Option<String>,
614}
615
616/// The events manager providing access to Calendar events via EventKit
617pub struct EventsManager {
618    store: Retained<EKEventStore>,
619}
620
621impl EventsManager {
622    /// Creates a new EventsManager instance
623    pub fn new() -> Self {
624        let store = unsafe { EKEventStore::new() };
625        Self { store }
626    }
627
628    /// Gets the current authorization status for calendar events
629    pub fn authorization_status() -> AuthorizationStatus {
630        let status = unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Event) };
631        status.into()
632    }
633
634    /// Requests full access to calendar events (blocking)
635    ///
636    /// Returns Ok(true) if access was granted, Ok(false) if denied
637    pub fn request_access(&self) -> Result<bool> {
638        let result = Arc::new((Mutex::new(None::<(bool, Option<String>)>), Condvar::new()));
639        let result_clone = Arc::clone(&result);
640
641        let completion = RcBlock::new(move |granted: Bool, error: *mut NSError| {
642            let error_msg = if !error.is_null() {
643                let error_ref = unsafe { &*error };
644                Some(format!("{:?}", error_ref))
645            } else {
646                None
647            };
648
649            let (lock, cvar) = &*result_clone;
650            let mut res = lock.lock().unwrap();
651            *res = Some((granted.as_bool(), error_msg));
652            cvar.notify_one();
653        });
654
655        unsafe {
656            let block_ptr = &*completion as *const _ as *mut _;
657            self.store
658                .requestFullAccessToEventsWithCompletion(block_ptr);
659        }
660
661        let (lock, cvar) = &*result;
662        let mut res = lock.lock().unwrap();
663        while res.is_none() {
664            res = cvar.wait(res).unwrap();
665        }
666
667        match res.take() {
668            Some((granted, None)) => Ok(granted),
669            Some((_, Some(error))) => Err(EventKitError::AuthorizationRequestFailed(error)),
670            None => Err(EventKitError::AuthorizationRequestFailed(
671                "Unknown error".to_string(),
672            )),
673        }
674    }
675
676    /// Ensures we have authorization, requesting if needed
677    pub fn ensure_authorized(&self) -> Result<()> {
678        match Self::authorization_status() {
679            AuthorizationStatus::FullAccess => Ok(()),
680            AuthorizationStatus::NotDetermined => {
681                if self.request_access()? {
682                    Ok(())
683                } else {
684                    Err(EventKitError::AuthorizationDenied)
685                }
686            }
687            AuthorizationStatus::Denied => Err(EventKitError::AuthorizationDenied),
688            AuthorizationStatus::Restricted => Err(EventKitError::AuthorizationRestricted),
689            AuthorizationStatus::WriteOnly => Ok(()),
690        }
691    }
692
693    /// Lists all event calendars
694    pub fn list_calendars(&self) -> Result<Vec<CalendarInfo>> {
695        self.ensure_authorized()?;
696
697        let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
698
699        let mut result = Vec::new();
700        for calendar in calendars.iter() {
701            result.push(calendar_to_info(&calendar));
702        }
703
704        Ok(result)
705    }
706
707    /// Gets the default calendar for new events
708    pub fn default_calendar(&self) -> Result<CalendarInfo> {
709        self.ensure_authorized()?;
710
711        let calendar = unsafe { self.store.defaultCalendarForNewEvents() };
712
713        match calendar {
714            Some(cal) => Ok(calendar_to_info(&cal)),
715            None => Err(EventKitError::NoDefaultCalendar),
716        }
717    }
718
719    /// Fetches events for today
720    pub fn fetch_today_events(&self) -> Result<Vec<EventItem>> {
721        let now = Local::now();
722        let start = now.date_naive().and_hms_opt(0, 0, 0).unwrap();
723        let end = now.date_naive().and_hms_opt(23, 59, 59).unwrap();
724
725        self.fetch_events(
726            Local.from_local_datetime(&start).unwrap(),
727            Local.from_local_datetime(&end).unwrap(),
728            None,
729        )
730    }
731
732    /// Fetches events for the next N days
733    pub fn fetch_upcoming_events(&self, days: i64) -> Result<Vec<EventItem>> {
734        let now = Local::now();
735        let end = now + Duration::days(days);
736        self.fetch_events(now, end, None)
737    }
738
739    /// Fetches events in a date range
740    pub fn fetch_events(
741        &self,
742        start: DateTime<Local>,
743        end: DateTime<Local>,
744        calendar_titles: Option<&[&str]>,
745    ) -> Result<Vec<EventItem>> {
746        self.ensure_authorized()?;
747
748        if start >= end {
749            return Err(EventKitError::InvalidDateRange);
750        }
751
752        let calendars: Option<Retained<NSArray<EKCalendar>>> = match calendar_titles {
753            Some(titles) => {
754                let all_calendars =
755                    unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
756                let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
757
758                for cal in all_calendars.iter() {
759                    let title = unsafe { cal.title() };
760                    let title_str = title.to_string();
761                    if titles.iter().any(|t| *t == title_str) {
762                        matching.push(cal.retain());
763                    }
764                }
765
766                if matching.is_empty() {
767                    return Err(EventKitError::CalendarNotFound(titles.join(", ")));
768                }
769
770                Some(NSArray::from_retained_slice(&matching))
771            }
772            None => None,
773        };
774
775        let start_date = datetime_to_nsdate(start);
776        let end_date = datetime_to_nsdate(end);
777
778        let predicate = unsafe {
779            self.store
780                .predicateForEventsWithStartDate_endDate_calendars(
781                    &start_date,
782                    &end_date,
783                    calendars.as_deref(),
784                )
785        };
786
787        let events = unsafe { self.store.eventsMatchingPredicate(&predicate) };
788
789        let mut items = Vec::new();
790        for event in events.iter() {
791            items.push(event_to_item(&event));
792        }
793
794        // Sort by start date
795        items.sort_by(|a, b| a.start_date.cmp(&b.start_date));
796
797        Ok(items)
798    }
799
800    /// Creates a new event
801    #[allow(clippy::too_many_arguments)]
802    pub fn create_event(
803        &self,
804        title: &str,
805        start: DateTime<Local>,
806        end: DateTime<Local>,
807        notes: Option<&str>,
808        location: Option<&str>,
809        calendar_title: Option<&str>,
810        all_day: bool,
811    ) -> Result<EventItem> {
812        self.ensure_authorized()?;
813
814        let event = unsafe { EKEvent::eventWithEventStore(&self.store) };
815
816        // Set title
817        let ns_title = NSString::from_str(title);
818        unsafe { event.setTitle(Some(&ns_title)) };
819
820        // Set dates
821        let start_date = datetime_to_nsdate(start);
822        let end_date = datetime_to_nsdate(end);
823        unsafe {
824            event.setStartDate(Some(&start_date));
825            event.setEndDate(Some(&end_date));
826            event.setAllDay(all_day);
827        }
828
829        // Set notes if provided
830        if let Some(notes_text) = notes {
831            let ns_notes = NSString::from_str(notes_text);
832            unsafe { event.setNotes(Some(&ns_notes)) };
833        }
834
835        // Set location if provided
836        if let Some(loc) = location {
837            let ns_location = NSString::from_str(loc);
838            unsafe { event.setLocation(Some(&ns_location)) };
839        }
840
841        // Set calendar
842        let calendar = if let Some(cal_title) = calendar_title {
843            self.find_calendar_by_title(cal_title)?
844        } else {
845            unsafe { self.store.defaultCalendarForNewEvents() }
846                .ok_or(EventKitError::NoDefaultCalendar)?
847        };
848        unsafe { event.setCalendar(Some(&calendar)) };
849
850        // Save
851        unsafe {
852            self.store
853                .saveEvent_span_error(&event, EKSpan::ThisEvent)
854                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
855        }
856
857        Ok(event_to_item(&event))
858    }
859
860    /// Updates an existing event
861    pub fn update_event(
862        &self,
863        identifier: &str,
864        title: Option<&str>,
865        notes: Option<&str>,
866        location: Option<&str>,
867        start: Option<DateTime<Local>>,
868        end: Option<DateTime<Local>>,
869    ) -> Result<EventItem> {
870        self.ensure_authorized()?;
871
872        let event = self.find_event_by_id(identifier)?;
873
874        if let Some(t) = title {
875            let ns_title = NSString::from_str(t);
876            unsafe { event.setTitle(Some(&ns_title)) };
877        }
878
879        if let Some(n) = notes {
880            let ns_notes = NSString::from_str(n);
881            unsafe { event.setNotes(Some(&ns_notes)) };
882        }
883
884        if let Some(l) = location {
885            let ns_location = NSString::from_str(l);
886            unsafe { event.setLocation(Some(&ns_location)) };
887        }
888
889        if let Some(s) = start {
890            let start_date = datetime_to_nsdate(s);
891            unsafe { event.setStartDate(Some(&start_date)) };
892        }
893
894        if let Some(e) = end {
895            let end_date = datetime_to_nsdate(e);
896            unsafe { event.setEndDate(Some(&end_date)) };
897        }
898
899        unsafe {
900            self.store
901                .saveEvent_span_error(&event, EKSpan::ThisEvent)
902                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
903        }
904
905        Ok(event_to_item(&event))
906    }
907
908    /// Deletes an event
909    pub fn delete_event(&self, identifier: &str) -> Result<()> {
910        self.ensure_authorized()?;
911
912        let event = self.find_event_by_id(identifier)?;
913
914        unsafe {
915            self.store
916                .removeEvent_span_error(&event, EKSpan::ThisEvent)
917                .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
918        }
919
920        Ok(())
921    }
922
923    /// Gets an event by its identifier
924    pub fn get_event(&self, identifier: &str) -> Result<EventItem> {
925        self.ensure_authorized()?;
926        let event = self.find_event_by_id(identifier)?;
927        Ok(event_to_item(&event))
928    }
929
930    // Helper to find a calendar by title
931    fn find_calendar_by_title(&self, title: &str) -> Result<Retained<EKCalendar>> {
932        let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
933
934        for cal in calendars.iter() {
935            let cal_title = unsafe { cal.title() };
936            if cal_title.to_string() == title {
937                return Ok(cal.retain());
938            }
939        }
940
941        Err(EventKitError::CalendarNotFound(title.to_string()))
942    }
943
944    // Helper to find an event by identifier
945    fn find_event_by_id(&self, identifier: &str) -> Result<Retained<EKEvent>> {
946        let ns_id = NSString::from_str(identifier);
947        let event = unsafe { self.store.eventWithIdentifier(&ns_id) };
948
949        match event {
950            Some(e) => Ok(e),
951            None => Err(EventKitError::ItemNotFound(identifier.to_string())),
952        }
953    }
954}
955
956impl Default for EventsManager {
957    fn default() -> Self {
958        Self::new()
959    }
960}
961
962// Helper function to convert EKEvent to EventItem
963fn event_to_item(event: &EKEvent) -> EventItem {
964    let identifier = unsafe { event.eventIdentifier() }
965        .map(|s| s.to_string())
966        .unwrap_or_default();
967    let title = unsafe { event.title() }.to_string();
968    let notes = unsafe { event.notes() }.map(|n| n.to_string());
969    let location = unsafe { event.location() }.map(|l| l.to_string());
970    let all_day = unsafe { event.isAllDay() };
971    let calendar_title = unsafe { event.calendar() }.map(|c| unsafe { c.title() }.to_string());
972
973    let start_ns: Retained<NSDate> = unsafe { event.startDate() };
974    let end_ns: Retained<NSDate> = unsafe { event.endDate() };
975
976    let start_date = nsdate_to_datetime(&start_ns);
977    let end_date = nsdate_to_datetime(&end_ns);
978
979    EventItem {
980        identifier,
981        title,
982        notes,
983        location,
984        start_date,
985        end_date,
986        all_day,
987        calendar_title,
988    }
989}
990
991// Helper to convert chrono DateTime to NSDate
992fn datetime_to_nsdate(dt: DateTime<Local>) -> Retained<NSDate> {
993    let timestamp = dt.timestamp() as f64;
994    NSDate::dateWithTimeIntervalSince1970(timestamp)
995}
996
997// Helper to convert NSDate to chrono DateTime
998fn nsdate_to_datetime(date: &NSDate) -> DateTime<Local> {
999    let timestamp = date.timeIntervalSince1970();
1000    Local.timestamp_opt(timestamp as i64, 0).unwrap()
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006
1007    #[test]
1008    fn test_authorization_status_display() {
1009        assert_eq!(
1010            format!("{}", AuthorizationStatus::NotDetermined),
1011            "Not Determined"
1012        );
1013        assert_eq!(
1014            format!("{}", AuthorizationStatus::FullAccess),
1015            "Full Access"
1016        );
1017    }
1018
1019    #[test]
1020    fn test_event_item_debug() {
1021        let event = EventItem {
1022            identifier: "test".to_string(),
1023            title: "Test Event".to_string(),
1024            notes: None,
1025            location: None,
1026            start_date: Local::now(),
1027            end_date: Local::now(),
1028            all_day: false,
1029            calendar_title: None,
1030        };
1031        assert!(format!("{:?}", event).contains("Test Event"));
1032    }
1033}