1use block2::RcBlock;
51use chrono::{DateTime, Datelike, Duration, Local, TimeZone, Timelike};
52use objc2::Message;
53use objc2::rc::Retained;
54use objc2::runtime::Bool;
55use objc2_event_kit::{
56 EKAlarm, EKAlarmProximity, EKAuthorizationStatus, EKCalendar, EKCalendarItem, EKEntityType,
57 EKEvent, EKEventStore, EKRecurrenceDayOfWeek, EKRecurrenceEnd, EKRecurrenceFrequency,
58 EKRecurrenceRule, EKReminder, EKSource, EKSpan, EKStructuredLocation, EKWeekday,
59};
60use objc2_foundation::{
61 NSArray, NSCalendar, NSDate, NSDateComponents, NSError, NSNumber, NSString,
62};
63use std::sync::{Arc, Condvar, Mutex};
64use thiserror::Error;
65
66#[cfg(feature = "location")]
67pub mod location;
68
69#[cfg(feature = "mcp")]
70pub mod mcp;
71
72#[derive(Error, Debug)]
74pub enum EventKitError {
75 #[error("Authorization denied")]
76 AuthorizationDenied,
77
78 #[error("Authorization restricted by system policy")]
79 AuthorizationRestricted,
80
81 #[error("Authorization not determined")]
82 AuthorizationNotDetermined,
83
84 #[error("Failed to request authorization: {0}")]
85 AuthorizationRequestFailed(String),
86
87 #[error("No default calendar")]
88 NoDefaultCalendar,
89
90 #[error("Calendar not found: {0}")]
91 CalendarNotFound(String),
92
93 #[error("Item not found: {0}")]
94 ItemNotFound(String),
95
96 #[error("Failed to save: {0}")]
97 SaveFailed(String),
98
99 #[error("Failed to delete: {0}")]
100 DeleteFailed(String),
101
102 #[error("Failed to fetch: {0}")]
103 FetchFailed(String),
104
105 #[error("EventKit error: {0}")]
106 EventKitError(String),
107
108 #[error("Invalid date range")]
109 InvalidDateRange,
110}
111
112pub type RemindersError = EventKitError;
114
115pub type Result<T> = std::result::Result<T, EventKitError>;
117
118#[derive(Debug, Clone)]
120pub struct ReminderItem {
121 pub identifier: String,
123 pub title: String,
125 pub notes: Option<String>,
127 pub completed: bool,
129 pub priority: usize,
131 pub calendar_title: Option<String>,
133 pub calendar_id: Option<String>,
135 pub due_date: Option<DateTime<Local>>,
137 pub start_date: Option<DateTime<Local>>,
139 pub completion_date: Option<DateTime<Local>>,
141 pub external_identifier: Option<String>,
143 pub location: Option<String>,
145 pub url: Option<String>,
147 pub creation_date: Option<DateTime<Local>>,
149 pub last_modified_date: Option<DateTime<Local>>,
151 pub timezone: Option<String>,
153 pub has_alarms: bool,
155 pub has_recurrence_rules: bool,
157 pub has_attendees: bool,
159 pub has_notes: bool,
161 pub attendees: Vec<ParticipantInfo>,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum CalendarType {
168 Local,
169 CalDAV,
170 Exchange,
171 Subscription,
172 Birthday,
173 Unknown,
174}
175
176#[derive(Debug, Clone)]
178pub struct SourceInfo {
179 pub identifier: String,
180 pub title: String,
181 pub source_type: String,
182}
183
184#[derive(Debug, Clone)]
186pub struct CalendarInfo {
187 pub identifier: String,
189 pub title: String,
191 pub source: Option<String>,
193 pub source_id: Option<String>,
195 pub calendar_type: CalendarType,
197 pub allows_modifications: bool,
199 pub is_immutable: bool,
201 pub is_subscribed: bool,
203 pub color: Option<(f64, f64, f64, f64)>,
205 pub allowed_entity_types: Vec<String>,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub enum AlarmProximity {
212 None,
214 Enter,
216 Leave,
218}
219
220#[derive(Debug, Clone)]
222pub struct StructuredLocation {
223 pub title: String,
225 pub latitude: f64,
227 pub longitude: f64,
229 pub radius: f64,
231}
232
233#[derive(Debug, Clone)]
235pub struct AlarmInfo {
236 pub relative_offset: Option<f64>,
238 pub absolute_date: Option<DateTime<Local>>,
240 pub proximity: AlarmProximity,
242 pub location: Option<StructuredLocation>,
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum RecurrenceFrequency {
249 Daily,
250 Weekly,
251 Monthly,
252 Yearly,
253}
254
255#[derive(Debug, Clone)]
257pub enum RecurrenceEndCondition {
258 Never,
260 AfterCount(usize),
262 OnDate(DateTime<Local>),
264}
265
266#[derive(Debug, Clone)]
268pub struct RecurrenceRule {
269 pub frequency: RecurrenceFrequency,
271 pub interval: usize,
273 pub end: RecurrenceEndCondition,
275 pub days_of_week: Option<Vec<u8>>,
277 pub days_of_month: Option<Vec<i32>>,
279}
280
281pub struct RemindersManager {
283 store: Retained<EKEventStore>,
284}
285
286impl RemindersManager {
287 pub fn new() -> Self {
289 let store = unsafe { EKEventStore::new() };
290 Self { store }
291 }
292
293 pub fn authorization_status() -> AuthorizationStatus {
295 let status =
296 unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Reminder) };
297 status.into()
298 }
299
300 pub fn request_access(&self) -> Result<bool> {
304 let result = Arc::new((Mutex::new(None::<(bool, Option<String>)>), Condvar::new()));
305 let result_clone = Arc::clone(&result);
306
307 let completion = RcBlock::new(move |granted: Bool, error: *mut NSError| {
308 let error_msg = if !error.is_null() {
309 let error_ref = unsafe { &*error };
310 Some(format!("{:?}", error_ref))
311 } else {
312 None
313 };
314
315 let (lock, cvar) = &*result_clone;
316 let mut res = lock.lock().unwrap();
317 *res = Some((granted.as_bool(), error_msg));
318 cvar.notify_one();
319 });
320
321 unsafe {
322 let block_ptr = &*completion as *const _ as *mut _;
324 self.store
325 .requestFullAccessToRemindersWithCompletion(block_ptr);
326 }
327
328 let (lock, cvar) = &*result;
329 let mut res = lock.lock().unwrap();
330 while res.is_none() {
331 res = cvar.wait(res).unwrap();
332 }
333
334 match res.take() {
335 Some((granted, None)) => Ok(granted),
336 Some((_, Some(error))) => Err(RemindersError::AuthorizationRequestFailed(error)),
337 None => Err(RemindersError::AuthorizationRequestFailed(
338 "Unknown error".to_string(),
339 )),
340 }
341 }
342
343 pub fn ensure_authorized(&self) -> Result<()> {
345 match Self::authorization_status() {
346 AuthorizationStatus::FullAccess => Ok(()),
347 AuthorizationStatus::NotDetermined => {
348 if self.request_access()? {
349 Ok(())
350 } else {
351 Err(RemindersError::AuthorizationDenied)
352 }
353 }
354 AuthorizationStatus::Denied => Err(RemindersError::AuthorizationDenied),
355 AuthorizationStatus::Restricted => Err(RemindersError::AuthorizationRestricted),
356 AuthorizationStatus::WriteOnly => Ok(()), }
358 }
359
360 pub fn list_calendars(&self) -> Result<Vec<CalendarInfo>> {
362 self.ensure_authorized()?;
363
364 let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
365
366 let mut result = Vec::new();
367 for calendar in calendars.iter() {
368 result.push(calendar_to_info(&calendar));
369 }
370
371 Ok(result)
372 }
373
374 pub fn list_sources(&self) -> Result<Vec<SourceInfo>> {
376 self.ensure_authorized()?;
377 let sources = unsafe { self.store.sources() };
378 let mut result = Vec::new();
379 for source in sources.iter() {
380 result.push(source_to_info(&source));
381 }
382 Ok(result)
383 }
384
385 pub fn default_calendar(&self) -> Result<CalendarInfo> {
387 self.ensure_authorized()?;
388
389 let calendar = unsafe { self.store.defaultCalendarForNewReminders() };
390
391 match calendar {
392 Some(cal) => Ok(calendar_to_info(&cal)),
393 None => Err(RemindersError::NoDefaultCalendar),
394 }
395 }
396
397 pub fn fetch_all_reminders(&self) -> Result<Vec<ReminderItem>> {
399 self.fetch_reminders(None)
400 }
401
402 pub fn fetch_reminders(&self, calendar_titles: Option<&[&str]>) -> Result<Vec<ReminderItem>> {
404 self.ensure_authorized()?;
405
406 let calendars: Option<Retained<NSArray<EKCalendar>>> = match calendar_titles {
407 Some(titles) => {
408 let all_calendars =
409 unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
410 let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
411
412 for cal in all_calendars.iter() {
413 let title = unsafe { cal.title() };
414 let title_str = title.to_string();
415 if titles.iter().any(|t| *t == title_str) {
416 matching.push(cal.retain());
417 }
418 }
419
420 if matching.is_empty() {
421 return Err(RemindersError::CalendarNotFound(titles.join(", ")));
422 }
423
424 Some(NSArray::from_retained_slice(&matching))
425 }
426 None => None,
427 };
428
429 let predicate = unsafe {
430 self.store
431 .predicateForRemindersInCalendars(calendars.as_deref())
432 };
433
434 let result = Arc::new((Mutex::new(None::<Vec<ReminderItem>>), Condvar::new()));
435 let result_clone = Arc::clone(&result);
436
437 let completion = RcBlock::new(move |reminders: *mut NSArray<EKReminder>| {
438 let items = if reminders.is_null() {
439 Vec::new()
440 } else {
441 let reminders = unsafe { Retained::retain(reminders).unwrap() };
442 reminders.iter().map(|r| reminder_to_item(&r)).collect()
443 };
444 let (lock, cvar) = &*result_clone;
445 let mut guard = lock.lock().unwrap();
446 *guard = Some(items);
447 cvar.notify_one();
448 });
449
450 unsafe {
451 self.store
452 .fetchRemindersMatchingPredicate_completion(&predicate, &completion);
453 }
454
455 let (lock, cvar) = &*result;
456 let mut guard = lock.lock().unwrap();
457 while guard.is_none() {
458 guard = cvar.wait(guard).unwrap();
459 }
460
461 guard
462 .take()
463 .ok_or_else(|| RemindersError::FetchFailed("Unknown error".to_string()))
464 }
465
466 pub fn fetch_incomplete_reminders(&self) -> Result<Vec<ReminderItem>> {
468 self.ensure_authorized()?;
469
470 let predicate = unsafe {
471 self.store
472 .predicateForIncompleteRemindersWithDueDateStarting_ending_calendars(
473 None, None, None,
474 )
475 };
476
477 let result = Arc::new((Mutex::new(None::<Vec<ReminderItem>>), Condvar::new()));
478 let result_clone = Arc::clone(&result);
479
480 let completion = RcBlock::new(move |reminders: *mut NSArray<EKReminder>| {
481 let items = if reminders.is_null() {
482 Vec::new()
483 } else {
484 let reminders = unsafe { Retained::retain(reminders).unwrap() };
485 reminders.iter().map(|r| reminder_to_item(&r)).collect()
486 };
487 let (lock, cvar) = &*result_clone;
488 let mut guard = lock.lock().unwrap();
489 *guard = Some(items);
490 cvar.notify_one();
491 });
492
493 unsafe {
494 self.store
495 .fetchRemindersMatchingPredicate_completion(&predicate, &completion);
496 }
497
498 let (lock, cvar) = &*result;
499 let mut guard = lock.lock().unwrap();
500 while guard.is_none() {
501 guard = cvar.wait(guard).unwrap();
502 }
503
504 guard
505 .take()
506 .ok_or_else(|| RemindersError::FetchFailed("Unknown error".to_string()))
507 }
508
509 #[allow(clippy::too_many_arguments)]
519 pub fn create_reminder(
520 &self,
521 title: &str,
522 notes: Option<&str>,
523 calendar_title: Option<&str>,
524 priority: Option<usize>,
525 due_date: Option<DateTime<Local>>,
526 start_date: Option<DateTime<Local>>,
527 ) -> Result<ReminderItem> {
528 self.ensure_authorized()?;
529
530 let reminder = unsafe { EKReminder::reminderWithEventStore(&self.store) };
531
532 let ns_title = NSString::from_str(title);
534 unsafe { reminder.setTitle(Some(&ns_title)) };
535
536 if let Some(notes_text) = notes {
538 let ns_notes = NSString::from_str(notes_text);
539 unsafe { reminder.setNotes(Some(&ns_notes)) };
540 }
541
542 if let Some(p) = priority {
544 unsafe { reminder.setPriority(p) };
545 }
546
547 if let Some(due) = due_date {
549 let components = datetime_to_date_components(due);
550 unsafe { reminder.setDueDateComponents(Some(&components)) };
551 }
552
553 if let Some(start) = start_date {
555 let components = datetime_to_date_components(start);
556 unsafe { reminder.setStartDateComponents(Some(&components)) };
557 }
558
559 let calendar = if let Some(cal_title) = calendar_title {
561 self.find_calendar_by_title(cal_title)?
562 } else {
563 unsafe { self.store.defaultCalendarForNewReminders() }
564 .ok_or(RemindersError::NoDefaultCalendar)?
565 };
566 unsafe { reminder.setCalendar(Some(&calendar)) };
567
568 unsafe {
570 self.store
571 .saveReminder_commit_error(&reminder, true)
572 .map_err(|e| RemindersError::SaveFailed(format!("{:?}", e)))?;
573 }
574
575 Ok(reminder_to_item(&reminder))
576 }
577
578 #[allow(clippy::too_many_arguments)]
584 pub fn update_reminder(
585 &self,
586 identifier: &str,
587 title: Option<&str>,
588 notes: Option<&str>,
589 completed: Option<bool>,
590 priority: Option<usize>,
591 due_date: Option<Option<DateTime<Local>>>,
592 start_date: Option<Option<DateTime<Local>>>,
593 calendar_title: Option<&str>,
594 ) -> Result<ReminderItem> {
595 self.ensure_authorized()?;
596
597 let reminder = self.find_reminder_by_id(identifier)?;
598
599 if let Some(t) = title {
600 let ns_title = NSString::from_str(t);
601 unsafe { reminder.setTitle(Some(&ns_title)) };
602 }
603
604 if let Some(n) = notes {
605 let ns_notes = NSString::from_str(n);
606 unsafe { reminder.setNotes(Some(&ns_notes)) };
607 }
608
609 if let Some(c) = completed {
610 unsafe { reminder.setCompleted(c) };
611 }
612
613 if let Some(p) = priority {
614 unsafe { reminder.setPriority(p) };
615 }
616
617 if let Some(due_opt) = due_date {
619 match due_opt {
620 Some(due) => {
621 let components = datetime_to_date_components(due);
622 unsafe { reminder.setDueDateComponents(Some(&components)) };
623 }
624 None => {
625 unsafe { reminder.setDueDateComponents(None) };
626 }
627 }
628 }
629
630 if let Some(start_opt) = start_date {
632 match start_opt {
633 Some(start) => {
634 let components = datetime_to_date_components(start);
635 unsafe { reminder.setStartDateComponents(Some(&components)) };
636 }
637 None => {
638 unsafe { reminder.setStartDateComponents(None) };
639 }
640 }
641 }
642
643 if let Some(cal_title) = calendar_title {
645 let calendar = self.find_calendar_by_title(cal_title)?;
646 unsafe { reminder.setCalendar(Some(&calendar)) };
647 }
648
649 unsafe {
650 self.store
651 .saveReminder_commit_error(&reminder, true)
652 .map_err(|e| RemindersError::SaveFailed(format!("{:?}", e)))?;
653 }
654
655 Ok(reminder_to_item(&reminder))
656 }
657
658 pub fn complete_reminder(&self, identifier: &str) -> Result<ReminderItem> {
660 self.update_reminder(identifier, None, None, Some(true), None, None, None, None)
661 }
662
663 pub fn uncomplete_reminder(&self, identifier: &str) -> Result<ReminderItem> {
665 self.update_reminder(identifier, None, None, Some(false), None, None, None, None)
666 }
667
668 pub fn delete_reminder(&self, identifier: &str) -> Result<()> {
670 self.ensure_authorized()?;
671
672 let reminder = self.find_reminder_by_id(identifier)?;
673
674 unsafe {
675 self.store
676 .removeReminder_commit_error(&reminder, true)
677 .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
678 }
679
680 Ok(())
681 }
682
683 pub fn get_reminder(&self, identifier: &str) -> Result<ReminderItem> {
685 self.ensure_authorized()?;
686 let reminder = self.find_reminder_by_id(identifier)?;
687 Ok(reminder_to_item(&reminder))
688 }
689
690 pub fn get_alarms(&self, identifier: &str) -> Result<Vec<AlarmInfo>> {
696 self.ensure_authorized()?;
697 let reminder = self.find_reminder_by_id(identifier)?;
698 Ok(get_item_alarms(&reminder))
699 }
700
701 pub fn add_alarm(&self, identifier: &str, alarm: &AlarmInfo) -> Result<()> {
703 self.ensure_authorized()?;
704 let reminder = self.find_reminder_by_id(identifier)?;
705 add_item_alarm(&reminder, alarm);
706 unsafe {
707 self.store
708 .saveReminder_commit_error(&reminder, true)
709 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
710 }
711 Ok(())
712 }
713
714 pub fn remove_all_alarms(&self, identifier: &str) -> Result<()> {
716 self.ensure_authorized()?;
717 let reminder = self.find_reminder_by_id(identifier)?;
718 clear_item_alarms(&reminder);
719 unsafe {
720 self.store
721 .saveReminder_commit_error(&reminder, true)
722 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
723 }
724 Ok(())
725 }
726
727 pub fn remove_alarm(&self, identifier: &str, index: usize) -> Result<()> {
729 self.ensure_authorized()?;
730 let reminder = self.find_reminder_by_id(identifier)?;
731 remove_item_alarm(&reminder, index)?;
732 unsafe {
733 self.store
734 .saveReminder_commit_error(&reminder, true)
735 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
736 }
737 Ok(())
738 }
739
740 pub fn set_url(&self, identifier: &str, url: Option<&str>) -> Result<()> {
746 self.ensure_authorized()?;
747 let reminder = self.find_reminder_by_id(identifier)?;
748 set_item_url(&reminder, url);
749 unsafe {
750 self.store
751 .saveReminder_commit_error(&reminder, true)
752 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
753 }
754 Ok(())
755 }
756
757 pub fn get_recurrence_rules(&self, identifier: &str) -> Result<Vec<RecurrenceRule>> {
763 self.ensure_authorized()?;
764 let reminder = self.find_reminder_by_id(identifier)?;
765 Ok(get_item_recurrence_rules(&reminder))
766 }
767
768 pub fn set_recurrence_rule(&self, identifier: &str, rule: &RecurrenceRule) -> Result<()> {
770 self.ensure_authorized()?;
771 let reminder = self.find_reminder_by_id(identifier)?;
772 set_item_recurrence_rule(&reminder, rule);
773 unsafe {
774 self.store
775 .saveReminder_commit_error(&reminder, true)
776 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
777 }
778 Ok(())
779 }
780
781 pub fn remove_recurrence_rules(&self, identifier: &str) -> Result<()> {
783 self.ensure_authorized()?;
784 let reminder = self.find_reminder_by_id(identifier)?;
785 clear_item_recurrence_rules(&reminder);
786 unsafe {
787 self.store
788 .saveReminder_commit_error(&reminder, true)
789 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
790 }
791 Ok(())
792 }
793
794 pub fn create_calendar(&self, title: &str) -> Result<CalendarInfo> {
802 self.ensure_authorized()?;
803
804 let calendar = unsafe {
806 EKCalendar::calendarForEntityType_eventStore(EKEntityType::Reminder, &self.store)
807 };
808
809 let ns_title = NSString::from_str(title);
811 unsafe { calendar.setTitle(&ns_title) };
812
813 let source = self.find_best_source_for_reminders()?;
815 unsafe { calendar.setSource(Some(&source)) };
816
817 unsafe {
819 self.store
820 .saveCalendar_commit_error(&calendar, true)
821 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
822 }
823
824 Ok(calendar_to_info(&calendar))
825 }
826
827 pub fn rename_calendar(&self, identifier: &str, new_title: &str) -> Result<CalendarInfo> {
830 self.update_calendar(identifier, Some(new_title), None)
831 }
832
833 pub fn update_calendar(
835 &self,
836 identifier: &str,
837 new_title: Option<&str>,
838 color_rgba: Option<(f64, f64, f64, f64)>,
839 ) -> Result<CalendarInfo> {
840 self.ensure_authorized()?;
841 let calendar = self.find_calendar_by_id(identifier)?;
842
843 if !unsafe { calendar.allowsContentModifications() } {
844 return Err(EventKitError::SaveFailed(
845 "Calendar does not allow modifications".to_string(),
846 ));
847 }
848
849 if let Some(title) = new_title {
850 let ns_title = NSString::from_str(title);
851 unsafe { calendar.setTitle(&ns_title) };
852 }
853
854 if let Some((r, g, b, a)) = color_rgba {
855 let cg = objc2_core_graphics::CGColor::new_srgb(r, g, b, a);
856 unsafe { calendar.setCGColor(Some(&cg)) };
857 }
858
859 unsafe {
860 self.store
861 .saveCalendar_commit_error(&calendar, true)
862 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
863 }
864
865 Ok(calendar_to_info(&calendar))
866 }
867
868 pub fn delete_calendar(&self, identifier: &str) -> Result<()> {
872 self.ensure_authorized()?;
873
874 let calendar = self.find_calendar_by_id(identifier)?;
875
876 if !unsafe { calendar.allowsContentModifications() } {
878 return Err(EventKitError::DeleteFailed(
879 "Calendar does not allow modifications".to_string(),
880 ));
881 }
882
883 unsafe {
884 self.store
885 .removeCalendar_commit_error(&calendar, true)
886 .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
887 }
888
889 Ok(())
890 }
891
892 pub fn get_calendar(&self, identifier: &str) -> Result<CalendarInfo> {
894 self.ensure_authorized()?;
895 let calendar = self.find_calendar_by_id(identifier)?;
896 Ok(calendar_to_info(&calendar))
897 }
898
899 fn find_best_source_for_reminders(&self) -> Result<Retained<objc2_event_kit::EKSource>> {
901 if let Some(default_cal) = unsafe { self.store.defaultCalendarForNewReminders() }
903 && let Some(source) = unsafe { default_cal.source() }
904 {
905 return Ok(source);
906 }
907
908 let sources = unsafe { self.store.sources() };
910 for source in sources.iter() {
911 let calendars = unsafe { source.calendarsForEntityType(EKEntityType::Reminder) };
913 if !calendars.is_empty() {
914 return Ok(source.retain());
915 }
916 }
917
918 Err(EventKitError::SaveFailed(
919 "No suitable source found for creating reminder calendar".to_string(),
920 ))
921 }
922
923 fn find_calendar_by_id(&self, identifier: &str) -> Result<Retained<EKCalendar>> {
925 let ns_id = NSString::from_str(identifier);
926 let calendar = unsafe { self.store.calendarWithIdentifier(&ns_id) };
927
928 match calendar {
929 Some(cal) => Ok(cal),
930 None => Err(EventKitError::CalendarNotFound(identifier.to_string())),
931 }
932 }
933
934 fn find_calendar_by_title(&self, title: &str) -> Result<Retained<EKCalendar>> {
936 let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
937
938 for cal in calendars.iter() {
939 let cal_title = unsafe { cal.title() };
940 if cal_title.to_string() == title {
941 return Ok(cal.retain());
942 }
943 }
944
945 Err(RemindersError::CalendarNotFound(title.to_string()))
946 }
947
948 fn find_reminder_by_id(&self, identifier: &str) -> Result<Retained<EKReminder>> {
950 let ns_id = NSString::from_str(identifier);
951 let item = unsafe { self.store.calendarItemWithIdentifier(&ns_id) };
952
953 match item {
954 Some(item) => {
955 if let Some(reminder) = item.downcast_ref::<EKReminder>() {
957 Ok(reminder.retain())
958 } else {
959 Err(EventKitError::ItemNotFound(identifier.to_string()))
960 }
961 }
962 None => Err(EventKitError::ItemNotFound(identifier.to_string())),
963 }
964 }
965}
966
967impl Default for RemindersManager {
968 fn default() -> Self {
969 Self::new()
970 }
971}
972
973#[derive(Debug, Clone, Copy, PartialEq, Eq)]
975pub enum AuthorizationStatus {
976 NotDetermined,
978 Restricted,
980 Denied,
982 FullAccess,
984 WriteOnly,
986}
987
988impl From<EKAuthorizationStatus> for AuthorizationStatus {
989 fn from(status: EKAuthorizationStatus) -> Self {
990 if status == EKAuthorizationStatus::NotDetermined {
991 AuthorizationStatus::NotDetermined
992 } else if status == EKAuthorizationStatus::Restricted {
993 AuthorizationStatus::Restricted
994 } else if status == EKAuthorizationStatus::Denied {
995 AuthorizationStatus::Denied
996 } else if status == EKAuthorizationStatus::FullAccess {
997 AuthorizationStatus::FullAccess
998 } else if status == EKAuthorizationStatus::WriteOnly {
999 AuthorizationStatus::WriteOnly
1000 } else {
1001 AuthorizationStatus::NotDetermined
1002 }
1003 }
1004}
1005
1006impl std::fmt::Display for AuthorizationStatus {
1007 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1008 match self {
1009 AuthorizationStatus::NotDetermined => write!(f, "Not Determined"),
1010 AuthorizationStatus::Restricted => write!(f, "Restricted"),
1011 AuthorizationStatus::Denied => write!(f, "Denied"),
1012 AuthorizationStatus::FullAccess => write!(f, "Full Access"),
1013 AuthorizationStatus::WriteOnly => write!(f, "Write Only"),
1014 }
1015 }
1016}
1017
1018fn reminder_to_item(reminder: &EKReminder) -> ReminderItem {
1020 let identifier = unsafe { reminder.calendarItemIdentifier() }.to_string();
1021 let title = unsafe { reminder.title() }.to_string();
1022 let notes = unsafe { reminder.notes() }.map(|n| n.to_string());
1023 let completed = unsafe { reminder.isCompleted() };
1024 let priority = unsafe { reminder.priority() };
1025 let cal = unsafe { reminder.calendar() };
1026 let calendar_title = cal.as_ref().map(|c| unsafe { c.title() }.to_string());
1027 let calendar_id = cal
1028 .as_ref()
1029 .map(|c| unsafe { c.calendarIdentifier() }.to_string());
1030
1031 let due_date = unsafe { reminder.dueDateComponents() }
1033 .and_then(|components| date_components_to_datetime(&components));
1034
1035 let start_date = unsafe { reminder.startDateComponents() }
1037 .and_then(|components| date_components_to_datetime(&components));
1038
1039 let completion_date =
1041 unsafe { reminder.completionDate() }.map(|date| nsdate_to_datetime(&date));
1042
1043 let external_identifier =
1045 unsafe { reminder.calendarItemExternalIdentifier() }.map(|id| id.to_string());
1046 let location = unsafe { reminder.location() }.map(|loc| loc.to_string());
1047 let url = unsafe { reminder.URL() }
1048 .as_ref()
1049 .and_then(|url_ref| url_ref.absoluteString())
1050 .map(|abs_str| abs_str.to_string());
1051 let creation_date = unsafe { reminder.creationDate() }.map(|date| nsdate_to_datetime(&date));
1052 let last_modified_date =
1053 unsafe { reminder.lastModifiedDate() }.map(|date| nsdate_to_datetime(&date));
1054 let timezone = unsafe { reminder.timeZone() }.map(|tz| tz.name().to_string());
1055 let has_alarms = unsafe { reminder.hasAlarms() };
1056 let has_recurrence_rules = unsafe { reminder.hasRecurrenceRules() };
1057 let has_attendees = unsafe { reminder.hasAttendees() };
1058 let has_notes = unsafe { reminder.hasNotes() };
1059
1060 ReminderItem {
1061 identifier,
1062 title,
1063 notes,
1064 completed,
1065 priority,
1066 calendar_title,
1067 calendar_id,
1068 due_date,
1069 start_date,
1070 completion_date,
1071 external_identifier,
1072 location,
1073 url,
1074 creation_date,
1075 last_modified_date,
1076 timezone,
1077 has_alarms,
1078 has_recurrence_rules,
1079 has_attendees,
1080 has_notes,
1081 attendees: get_item_attendees(reminder),
1082 }
1083}
1084
1085fn source_to_info(source: &EKSource) -> SourceInfo {
1087 let identifier = unsafe { source.sourceIdentifier() }.to_string();
1088 let title = unsafe { source.title() }.to_string();
1089 let source_type = unsafe { source.sourceType() };
1091 let source_type = match source_type.0 {
1092 0 => "local",
1093 1 => "exchange",
1094 2 => "caldav",
1095 3 => "mobileme",
1096 4 => "subscribed",
1097 5 => "birthdays",
1098 _ => "unknown",
1099 }
1100 .to_string();
1101
1102 SourceInfo {
1103 identifier,
1104 title,
1105 source_type,
1106 }
1107}
1108
1109fn calendar_to_info(calendar: &EKCalendar) -> CalendarInfo {
1110 let identifier = unsafe { calendar.calendarIdentifier() }.to_string();
1111 let title = unsafe { calendar.title() }.to_string();
1112 let source = unsafe { calendar.source() }.map(|s| unsafe { s.title() }.to_string());
1113 let source_id =
1114 unsafe { calendar.source() }.map(|s| unsafe { s.sourceIdentifier() }.to_string());
1115 let allows_modifications = unsafe { calendar.allowsContentModifications() };
1116 let is_immutable = unsafe { calendar.isImmutable() };
1117 let is_subscribed = unsafe { calendar.isSubscribed() };
1118
1119 let cal_type = unsafe { calendar.r#type() };
1121 let calendar_type = match cal_type.0 {
1122 0 => CalendarType::Local,
1123 1 => CalendarType::CalDAV,
1124 2 => CalendarType::Exchange,
1125 3 => CalendarType::Subscription,
1126 4 => CalendarType::Birthday,
1127 _ => CalendarType::Unknown,
1128 };
1129
1130 let color: Option<(f64, f64, f64, f64)> = unsafe {
1132 calendar.CGColor().and_then(|cg| {
1133 use objc2_core_graphics::CGColor as CG;
1134 let n = CG::number_of_components(Some(&cg));
1135 if n >= 3 {
1136 let ptr = CG::components(Some(&cg));
1137 let r = *ptr;
1138 let g = *ptr.add(1);
1139 let b = *ptr.add(2);
1140 let a = if n >= 4 { *ptr.add(3) } else { 1.0 };
1141 Some((r, g, b, a))
1142 } else {
1143 None
1144 }
1145 })
1146 };
1147
1148 let entity_mask = unsafe { calendar.allowedEntityTypes() };
1150 let mut allowed_entity_types = Vec::new();
1151 if entity_mask.0 & 1 != 0 {
1152 allowed_entity_types.push("event".to_string());
1153 }
1154 if entity_mask.0 & 2 != 0 {
1155 allowed_entity_types.push("reminder".to_string());
1156 }
1157
1158 CalendarInfo {
1159 identifier,
1160 title,
1161 source,
1162 source_id,
1163 calendar_type,
1164 allows_modifications,
1165 is_immutable,
1166 is_subscribed,
1167 color,
1168 allowed_entity_types,
1169 }
1170}
1171
1172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1178pub enum EventAvailability {
1179 NotSupported,
1180 Busy,
1181 Free,
1182 Tentative,
1183 Unavailable,
1184}
1185
1186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1188pub enum EventStatus {
1189 None,
1190 Confirmed,
1191 Tentative,
1192 Canceled,
1193}
1194
1195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1197pub enum ParticipantRole {
1198 Unknown,
1199 Required,
1200 Optional,
1201 Chair,
1202 NonParticipant,
1203}
1204
1205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1207pub enum ParticipantStatus {
1208 Unknown,
1209 Pending,
1210 Accepted,
1211 Declined,
1212 Tentative,
1213 Delegated,
1214 Completed,
1215 InProcess,
1216}
1217
1218#[derive(Debug, Clone)]
1220pub struct ParticipantInfo {
1221 pub name: Option<String>,
1222 pub url: Option<String>,
1223 pub role: ParticipantRole,
1224 pub status: ParticipantStatus,
1225 pub is_current_user: bool,
1226}
1227
1228#[derive(Debug, Clone)]
1230pub struct EventItem {
1231 pub identifier: String,
1233 pub title: String,
1235 pub notes: Option<String>,
1237 pub location: Option<String>,
1239 pub start_date: DateTime<Local>,
1241 pub end_date: DateTime<Local>,
1243 pub all_day: bool,
1245 pub calendar_title: Option<String>,
1247 pub calendar_id: Option<String>,
1249 pub url: Option<String>,
1251 pub availability: EventAvailability,
1253 pub status: EventStatus,
1255 pub is_detached: bool,
1257 pub occurrence_date: Option<DateTime<Local>>,
1259 pub structured_location: Option<StructuredLocation>,
1261 pub attendees: Vec<ParticipantInfo>,
1263 pub organizer: Option<ParticipantInfo>,
1265}
1266
1267pub struct EventsManager {
1269 store: Retained<EKEventStore>,
1270}
1271
1272impl EventsManager {
1273 pub fn new() -> Self {
1275 let store = unsafe { EKEventStore::new() };
1276 Self { store }
1277 }
1278
1279 pub fn authorization_status() -> AuthorizationStatus {
1281 let status = unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Event) };
1282 status.into()
1283 }
1284
1285 pub fn request_access(&self) -> Result<bool> {
1289 let result = Arc::new((Mutex::new(None::<(bool, Option<String>)>), Condvar::new()));
1290 let result_clone = Arc::clone(&result);
1291
1292 let completion = RcBlock::new(move |granted: Bool, error: *mut NSError| {
1293 let error_msg = if !error.is_null() {
1294 let error_ref = unsafe { &*error };
1295 Some(format!("{:?}", error_ref))
1296 } else {
1297 None
1298 };
1299
1300 let (lock, cvar) = &*result_clone;
1301 let mut res = lock.lock().unwrap();
1302 *res = Some((granted.as_bool(), error_msg));
1303 cvar.notify_one();
1304 });
1305
1306 unsafe {
1307 let block_ptr = &*completion as *const _ as *mut _;
1308 self.store
1309 .requestFullAccessToEventsWithCompletion(block_ptr);
1310 }
1311
1312 let (lock, cvar) = &*result;
1313 let mut res = lock.lock().unwrap();
1314 while res.is_none() {
1315 res = cvar.wait(res).unwrap();
1316 }
1317
1318 match res.take() {
1319 Some((granted, None)) => Ok(granted),
1320 Some((_, Some(error))) => Err(EventKitError::AuthorizationRequestFailed(error)),
1321 None => Err(EventKitError::AuthorizationRequestFailed(
1322 "Unknown error".to_string(),
1323 )),
1324 }
1325 }
1326
1327 pub fn ensure_authorized(&self) -> Result<()> {
1329 match Self::authorization_status() {
1330 AuthorizationStatus::FullAccess => Ok(()),
1331 AuthorizationStatus::NotDetermined => {
1332 if self.request_access()? {
1333 Ok(())
1334 } else {
1335 Err(EventKitError::AuthorizationDenied)
1336 }
1337 }
1338 AuthorizationStatus::Denied => Err(EventKitError::AuthorizationDenied),
1339 AuthorizationStatus::Restricted => Err(EventKitError::AuthorizationRestricted),
1340 AuthorizationStatus::WriteOnly => Ok(()),
1341 }
1342 }
1343
1344 pub fn list_calendars(&self) -> Result<Vec<CalendarInfo>> {
1346 self.ensure_authorized()?;
1347
1348 let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
1349
1350 let mut result = Vec::new();
1351 for calendar in calendars.iter() {
1352 result.push(calendar_to_info(&calendar));
1353 }
1354
1355 Ok(result)
1356 }
1357
1358 pub fn default_calendar(&self) -> Result<CalendarInfo> {
1360 self.ensure_authorized()?;
1361
1362 let calendar = unsafe { self.store.defaultCalendarForNewEvents() };
1363
1364 match calendar {
1365 Some(cal) => Ok(calendar_to_info(&cal)),
1366 None => Err(EventKitError::NoDefaultCalendar),
1367 }
1368 }
1369
1370 pub fn fetch_today_events(&self) -> Result<Vec<EventItem>> {
1372 let now = Local::now();
1373 let start = now.date_naive().and_hms_opt(0, 0, 0).unwrap();
1374 let end = now.date_naive().and_hms_opt(23, 59, 59).unwrap();
1375
1376 self.fetch_events(
1377 Local.from_local_datetime(&start).unwrap(),
1378 Local.from_local_datetime(&end).unwrap(),
1379 None,
1380 )
1381 }
1382
1383 pub fn fetch_upcoming_events(&self, days: i64) -> Result<Vec<EventItem>> {
1385 let now = Local::now();
1386 let end = now + Duration::days(days);
1387 self.fetch_events(now, end, None)
1388 }
1389
1390 pub fn fetch_events(
1392 &self,
1393 start: DateTime<Local>,
1394 end: DateTime<Local>,
1395 calendar_titles: Option<&[&str]>,
1396 ) -> Result<Vec<EventItem>> {
1397 self.ensure_authorized()?;
1398
1399 if start >= end {
1400 return Err(EventKitError::InvalidDateRange);
1401 }
1402
1403 let calendars: Option<Retained<NSArray<EKCalendar>>> = match calendar_titles {
1404 Some(titles) => {
1405 let all_calendars =
1406 unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
1407 let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
1408
1409 for cal in all_calendars.iter() {
1410 let title = unsafe { cal.title() };
1411 let title_str = title.to_string();
1412 if titles.iter().any(|t| *t == title_str) {
1413 matching.push(cal.retain());
1414 }
1415 }
1416
1417 if matching.is_empty() {
1418 return Err(EventKitError::CalendarNotFound(titles.join(", ")));
1419 }
1420
1421 Some(NSArray::from_retained_slice(&matching))
1422 }
1423 None => None,
1424 };
1425
1426 let start_date = datetime_to_nsdate(start);
1427 let end_date = datetime_to_nsdate(end);
1428
1429 let predicate = unsafe {
1430 self.store
1431 .predicateForEventsWithStartDate_endDate_calendars(
1432 &start_date,
1433 &end_date,
1434 calendars.as_deref(),
1435 )
1436 };
1437
1438 let events = unsafe { self.store.eventsMatchingPredicate(&predicate) };
1439
1440 let mut items = Vec::new();
1441 for event in events.iter() {
1442 items.push(event_to_item(&event));
1443 }
1444
1445 items.sort_by(|a, b| a.start_date.cmp(&b.start_date));
1447
1448 Ok(items)
1449 }
1450
1451 #[allow(clippy::too_many_arguments)]
1453 pub fn create_event(
1454 &self,
1455 title: &str,
1456 start: DateTime<Local>,
1457 end: DateTime<Local>,
1458 notes: Option<&str>,
1459 location: Option<&str>,
1460 calendar_title: Option<&str>,
1461 all_day: bool,
1462 ) -> Result<EventItem> {
1463 self.ensure_authorized()?;
1464
1465 let event = unsafe { EKEvent::eventWithEventStore(&self.store) };
1466
1467 let ns_title = NSString::from_str(title);
1469 unsafe { event.setTitle(Some(&ns_title)) };
1470
1471 let start_date = datetime_to_nsdate(start);
1473 let end_date = datetime_to_nsdate(end);
1474 unsafe {
1475 event.setStartDate(Some(&start_date));
1476 event.setEndDate(Some(&end_date));
1477 event.setAllDay(all_day);
1478 }
1479
1480 if let Some(notes_text) = notes {
1482 let ns_notes = NSString::from_str(notes_text);
1483 unsafe { event.setNotes(Some(&ns_notes)) };
1484 }
1485
1486 if let Some(loc) = location {
1488 let ns_location = NSString::from_str(loc);
1489 unsafe { event.setLocation(Some(&ns_location)) };
1490 }
1491
1492 let calendar = if let Some(cal_title) = calendar_title {
1494 self.find_calendar_by_title(cal_title)?
1495 } else {
1496 unsafe { self.store.defaultCalendarForNewEvents() }
1497 .ok_or(EventKitError::NoDefaultCalendar)?
1498 };
1499 unsafe { event.setCalendar(Some(&calendar)) };
1500
1501 unsafe {
1503 self.store
1504 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1505 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1506 }
1507
1508 Ok(event_to_item(&event))
1509 }
1510
1511 pub fn update_event(
1513 &self,
1514 identifier: &str,
1515 title: Option<&str>,
1516 notes: Option<&str>,
1517 location: Option<&str>,
1518 start: Option<DateTime<Local>>,
1519 end: Option<DateTime<Local>>,
1520 ) -> Result<EventItem> {
1521 self.ensure_authorized()?;
1522
1523 let event = self.find_event_by_id(identifier)?;
1524
1525 if let Some(t) = title {
1526 let ns_title = NSString::from_str(t);
1527 unsafe { event.setTitle(Some(&ns_title)) };
1528 }
1529
1530 if let Some(n) = notes {
1531 let ns_notes = NSString::from_str(n);
1532 unsafe { event.setNotes(Some(&ns_notes)) };
1533 }
1534
1535 if let Some(l) = location {
1536 let ns_location = NSString::from_str(l);
1537 unsafe { event.setLocation(Some(&ns_location)) };
1538 }
1539
1540 if let Some(s) = start {
1541 let start_date = datetime_to_nsdate(s);
1542 unsafe { event.setStartDate(Some(&start_date)) };
1543 }
1544
1545 if let Some(e) = end {
1546 let end_date = datetime_to_nsdate(e);
1547 unsafe { event.setEndDate(Some(&end_date)) };
1548 }
1549
1550 unsafe {
1551 self.store
1552 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1553 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1554 }
1555
1556 Ok(event_to_item(&event))
1557 }
1558
1559 pub fn delete_event(&self, identifier: &str, affect_future: bool) -> Result<()> {
1561 self.ensure_authorized()?;
1562
1563 let event = self.find_event_by_id(identifier)?;
1564 let span = if affect_future {
1565 EKSpan::FutureEvents
1566 } else {
1567 EKSpan::ThisEvent
1568 };
1569
1570 unsafe {
1571 self.store
1572 .removeEvent_span_commit_error(&event, span, true)
1573 .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
1574 }
1575
1576 Ok(())
1577 }
1578
1579 pub fn get_event(&self, identifier: &str) -> Result<EventItem> {
1581 self.ensure_authorized()?;
1582 let event = self.find_event_by_id(identifier)?;
1583 Ok(event_to_item(&event))
1584 }
1585
1586 pub fn create_event_calendar(&self, title: &str) -> Result<CalendarInfo> {
1592 self.ensure_authorized()?;
1593 let calendar = unsafe {
1594 EKCalendar::calendarForEntityType_eventStore(EKEntityType::Event, &self.store)
1595 };
1596 let ns_title = NSString::from_str(title);
1597 unsafe { calendar.setTitle(&ns_title) };
1598
1599 if let Some(default_cal) = unsafe { self.store.defaultCalendarForNewEvents() }
1601 && let Some(source) = unsafe { default_cal.source() }
1602 {
1603 unsafe { calendar.setSource(Some(&source)) };
1604 }
1605
1606 unsafe {
1607 self.store
1608 .saveCalendar_commit_error(&calendar, true)
1609 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1610 }
1611 Ok(calendar_to_info(&calendar))
1612 }
1613
1614 pub fn rename_event_calendar(&self, identifier: &str, new_title: &str) -> Result<CalendarInfo> {
1617 self.update_event_calendar(identifier, Some(new_title), None)
1618 }
1619
1620 pub fn update_event_calendar(
1622 &self,
1623 identifier: &str,
1624 new_title: Option<&str>,
1625 color_rgba: Option<(f64, f64, f64, f64)>,
1626 ) -> Result<CalendarInfo> {
1627 self.ensure_authorized()?;
1628 let calendar = unsafe {
1629 self.store
1630 .calendarWithIdentifier(&NSString::from_str(identifier))
1631 }
1632 .ok_or_else(|| EventKitError::CalendarNotFound(identifier.to_string()))?;
1633
1634 if let Some(title) = new_title {
1635 let ns_title = NSString::from_str(title);
1636 unsafe { calendar.setTitle(&ns_title) };
1637 }
1638
1639 if let Some((r, g, b, a)) = color_rgba {
1640 let cg = objc2_core_graphics::CGColor::new_srgb(r, g, b, a);
1641 unsafe { calendar.setCGColor(Some(&cg)) };
1642 }
1643
1644 unsafe {
1645 self.store
1646 .saveCalendar_commit_error(&calendar, true)
1647 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1648 }
1649 Ok(calendar_to_info(&calendar))
1650 }
1651
1652 pub fn delete_event_calendar(&self, identifier: &str) -> Result<()> {
1654 self.ensure_authorized()?;
1655 let calendar = unsafe {
1656 self.store
1657 .calendarWithIdentifier(&NSString::from_str(identifier))
1658 }
1659 .ok_or_else(|| EventKitError::CalendarNotFound(identifier.to_string()))?;
1660
1661 unsafe {
1662 self.store
1663 .removeCalendar_commit_error(&calendar, true)
1664 .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
1665 }
1666 Ok(())
1667 }
1668
1669 pub fn get_event_alarms(&self, identifier: &str) -> Result<Vec<AlarmInfo>> {
1675 self.ensure_authorized()?;
1676 let event = self.find_event_by_id(identifier)?;
1677 Ok(get_item_alarms(&event))
1678 }
1679
1680 pub fn add_event_alarm(&self, identifier: &str, alarm: &AlarmInfo) -> Result<()> {
1682 self.ensure_authorized()?;
1683 let event = self.find_event_by_id(identifier)?;
1684 add_item_alarm(&event, alarm);
1685 unsafe {
1686 self.store
1687 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1688 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1689 }
1690 Ok(())
1691 }
1692
1693 pub fn get_event_recurrence_rules(&self, identifier: &str) -> Result<Vec<RecurrenceRule>> {
1699 self.ensure_authorized()?;
1700 let event = self.find_event_by_id(identifier)?;
1701 Ok(get_item_recurrence_rules(&event))
1702 }
1703
1704 pub fn set_event_recurrence_rule(&self, identifier: &str, rule: &RecurrenceRule) -> Result<()> {
1706 self.ensure_authorized()?;
1707 let event = self.find_event_by_id(identifier)?;
1708 set_item_recurrence_rule(&event, rule);
1709 unsafe {
1710 self.store
1711 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1712 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1713 }
1714 Ok(())
1715 }
1716
1717 pub fn remove_event_recurrence_rules(&self, identifier: &str) -> Result<()> {
1719 self.ensure_authorized()?;
1720 let event = self.find_event_by_id(identifier)?;
1721 clear_item_recurrence_rules(&event);
1722 unsafe {
1723 self.store
1724 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1725 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1726 }
1727 Ok(())
1728 }
1729
1730 pub fn remove_event_alarm(&self, identifier: &str, index: usize) -> Result<()> {
1732 self.ensure_authorized()?;
1733 let event = self.find_event_by_id(identifier)?;
1734 remove_item_alarm(&event, index)?;
1735 unsafe {
1736 self.store
1737 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1738 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1739 }
1740 Ok(())
1741 }
1742
1743 pub fn set_event_url(&self, identifier: &str, url: Option<&str>) -> Result<()> {
1745 self.ensure_authorized()?;
1746 let event = self.find_event_by_id(identifier)?;
1747 set_item_url(&event, url);
1748 unsafe {
1749 self.store
1750 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1751 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1752 }
1753 Ok(())
1754 }
1755
1756 fn find_calendar_by_title(&self, title: &str) -> Result<Retained<EKCalendar>> {
1758 let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
1759
1760 for cal in calendars.iter() {
1761 let cal_title = unsafe { cal.title() };
1762 if cal_title.to_string() == title {
1763 return Ok(cal.retain());
1764 }
1765 }
1766
1767 Err(EventKitError::CalendarNotFound(title.to_string()))
1768 }
1769
1770 fn find_event_by_id(&self, identifier: &str) -> Result<Retained<EKEvent>> {
1772 let ns_id = NSString::from_str(identifier);
1773 let event = unsafe { self.store.eventWithIdentifier(&ns_id) };
1774
1775 match event {
1776 Some(e) => Ok(e),
1777 None => Err(EventKitError::ItemNotFound(identifier.to_string())),
1778 }
1779 }
1780}
1781
1782impl Default for EventsManager {
1783 fn default() -> Self {
1784 Self::new()
1785 }
1786}
1787
1788fn event_to_item(event: &EKEvent) -> EventItem {
1790 let identifier = unsafe { event.eventIdentifier() }
1791 .map(|s| s.to_string())
1792 .unwrap_or_default();
1793 let title = unsafe { event.title() }.to_string();
1794 let notes = unsafe { event.notes() }.map(|n| n.to_string());
1795 let location = unsafe { event.location() }.map(|l| l.to_string());
1796 let all_day = unsafe { event.isAllDay() };
1797 let cal = unsafe { event.calendar() };
1798 let calendar_title = cal.as_ref().map(|c| unsafe { c.title() }.to_string());
1799 let calendar_id = cal
1800 .as_ref()
1801 .map(|c| unsafe { c.calendarIdentifier() }.to_string());
1802
1803 let start_ns: Retained<NSDate> = unsafe { event.startDate() };
1804 let end_ns: Retained<NSDate> = unsafe { event.endDate() };
1805 let start_date = nsdate_to_datetime(&start_ns);
1806 let end_date = nsdate_to_datetime(&end_ns);
1807
1808 let url = get_item_url(event);
1809
1810 let avail = unsafe { event.availability() };
1812 let availability = match avail.0 {
1813 0 => EventAvailability::Busy,
1814 1 => EventAvailability::Free,
1815 2 => EventAvailability::Tentative,
1816 3 => EventAvailability::Unavailable,
1817 _ => EventAvailability::NotSupported,
1818 };
1819
1820 let stat = unsafe { event.status() };
1822 let status = match stat.0 {
1823 1 => EventStatus::Confirmed,
1824 2 => EventStatus::Tentative,
1825 3 => EventStatus::Canceled,
1826 _ => EventStatus::None,
1827 };
1828
1829 let is_detached = unsafe { event.isDetached() };
1830 let occurrence_date = unsafe { event.occurrenceDate() }.map(|d| nsdate_to_datetime(&d));
1831
1832 let structured_location = unsafe { event.structuredLocation() }.map(|loc| {
1834 let title = unsafe { loc.title() }
1835 .map(|t| t.to_string())
1836 .unwrap_or_default();
1837 let radius = unsafe { loc.radius() };
1838 let (latitude, longitude) = unsafe { loc.geoLocation() }
1839 .map(|geo| {
1840 let coord = unsafe { geo.coordinate() };
1841 (coord.latitude, coord.longitude)
1842 })
1843 .unwrap_or((0.0, 0.0));
1844 StructuredLocation {
1845 title,
1846 latitude,
1847 longitude,
1848 radius,
1849 }
1850 });
1851
1852 let attendees = get_item_attendees(event);
1854
1855 let organizer = unsafe { event.organizer() }.map(|p| participant_to_info(&p));
1857
1858 EventItem {
1859 identifier,
1860 title,
1861 notes,
1862 location,
1863 start_date,
1864 end_date,
1865 all_day,
1866 calendar_title,
1867 calendar_id,
1868 url,
1869 availability,
1870 status,
1871 is_detached,
1872 occurrence_date,
1873 structured_location,
1874 attendees,
1875 organizer,
1876 }
1877}
1878
1879fn get_item_attendees(item: &EKCalendarItem) -> Vec<ParticipantInfo> {
1881 let attendees = unsafe { item.attendees() };
1882 let Some(attendees) = attendees else {
1883 return Vec::new();
1884 };
1885 let mut result = Vec::new();
1886 for i in 0..attendees.len() {
1887 let p = attendees.objectAtIndex(i);
1888 result.push(participant_to_info(&p));
1889 }
1890 result
1891}
1892
1893fn participant_to_info(p: &objc2_event_kit::EKParticipant) -> ParticipantInfo {
1895 let name = unsafe { p.name() }.map(|n| n.to_string());
1896 let url = unsafe { p.URL() }.absoluteString().map(|s| s.to_string());
1897
1898 let role = unsafe { p.participantRole() };
1900 let role = match role.0 {
1901 1 => ParticipantRole::Required,
1902 2 => ParticipantRole::Optional,
1903 3 => ParticipantRole::Chair,
1904 4 => ParticipantRole::NonParticipant,
1905 _ => ParticipantRole::Unknown,
1906 };
1907
1908 let status = unsafe { p.participantStatus() };
1911 let status = match status.0 {
1912 1 => ParticipantStatus::Pending,
1913 2 => ParticipantStatus::Accepted,
1914 3 => ParticipantStatus::Declined,
1915 4 => ParticipantStatus::Tentative,
1916 5 => ParticipantStatus::Delegated,
1917 6 => ParticipantStatus::Completed,
1918 7 => ParticipantStatus::InProcess,
1919 _ => ParticipantStatus::Unknown,
1920 };
1921
1922 let is_current_user = unsafe { p.isCurrentUser() };
1923
1924 ParticipantInfo {
1925 name,
1926 url,
1927 role,
1928 status,
1929 is_current_user,
1930 }
1931}
1932
1933fn datetime_to_nsdate(dt: DateTime<Local>) -> Retained<NSDate> {
1935 let timestamp = dt.timestamp() as f64;
1936 NSDate::dateWithTimeIntervalSince1970(timestamp)
1937}
1938
1939fn nsdate_to_datetime(date: &NSDate) -> DateTime<Local> {
1941 let timestamp = date.timeIntervalSince1970();
1942 Local.timestamp_opt(timestamp as i64, 0).unwrap()
1943}
1944
1945fn date_components_to_datetime(components: &NSDateComponents) -> Option<DateTime<Local>> {
1947 let calendar = NSCalendar::currentCalendar();
1949
1950 let date = calendar.dateFromComponents(components)?;
1952
1953 Some(nsdate_to_datetime(&date))
1954}
1955
1956fn datetime_to_date_components(dt: DateTime<Local>) -> Retained<NSDateComponents> {
1958 let components = NSDateComponents::new();
1959
1960 components.setYear(dt.year() as isize);
1961 components.setMonth(dt.month() as isize);
1962 components.setDay(dt.day() as isize);
1963 components.setHour(dt.hour() as isize);
1964 components.setMinute(dt.minute() as isize);
1965 components.setSecond(dt.second() as isize);
1966
1967 components
1968}
1969
1970fn get_item_alarms(item: &EKCalendarItem) -> Vec<AlarmInfo> {
1978 let alarms = unsafe { item.alarms() };
1979 let Some(alarms) = alarms else {
1980 return Vec::new();
1981 };
1982 let mut result = Vec::new();
1983 for i in 0..alarms.len() {
1984 let alarm = alarms.objectAtIndex(i);
1985 result.push(alarm_to_info(&alarm));
1986 }
1987 result
1988}
1989
1990fn add_item_alarm(item: &EKCalendarItem, alarm: &AlarmInfo) {
1992 let ek_alarm = create_ek_alarm(alarm);
1993 unsafe { item.addAlarm(&ek_alarm) };
1994}
1995
1996fn remove_item_alarm(item: &EKCalendarItem, index: usize) -> Result<()> {
1998 let alarms = unsafe { item.alarms() };
1999 let Some(alarms) = alarms else {
2000 return Err(EventKitError::ItemNotFound("No alarms on this item".into()));
2001 };
2002 if index >= alarms.len() {
2003 return Err(EventKitError::ItemNotFound(format!(
2004 "Alarm index {} out of range ({})",
2005 index,
2006 alarms.len()
2007 )));
2008 }
2009 let alarm = alarms.objectAtIndex(index);
2010 unsafe { item.removeAlarm(&alarm) };
2011 Ok(())
2012}
2013
2014fn clear_item_alarms(item: &EKCalendarItem) {
2016 unsafe { item.setAlarms(None) };
2017}
2018
2019fn get_item_recurrence_rules(item: &EKCalendarItem) -> Vec<RecurrenceRule> {
2021 let rules = unsafe { item.recurrenceRules() };
2022 let Some(rules) = rules else {
2023 return Vec::new();
2024 };
2025 let mut result = Vec::new();
2026 for i in 0..rules.len() {
2027 let rule = rules.objectAtIndex(i);
2028 result.push(recurrence_rule_to_info(&rule));
2029 }
2030 result
2031}
2032
2033fn set_item_recurrence_rule(item: &EKCalendarItem, rule: &RecurrenceRule) {
2035 let ek_rule = create_ek_recurrence_rule(rule);
2036 unsafe {
2037 let rules = NSArray::from_retained_slice(&[ek_rule]);
2038 item.setRecurrenceRules(Some(&rules));
2039 }
2040}
2041
2042fn clear_item_recurrence_rules(item: &EKCalendarItem) {
2044 unsafe { item.setRecurrenceRules(None) };
2045}
2046
2047fn set_item_url(item: &EKCalendarItem, url: Option<&str>) {
2049 unsafe {
2050 let ns_url = url.map(|u| {
2051 let ns_str = NSString::from_str(u);
2052 objc2_foundation::NSURL::URLWithString(&ns_str).unwrap()
2053 });
2054 item.setURL(ns_url.as_deref());
2055 }
2056}
2057
2058fn get_item_url(item: &EKCalendarItem) -> Option<String> {
2060 unsafe { item.URL() }.map(|u| u.absoluteString().unwrap().to_string())
2061}
2062
2063fn recurrence_rule_to_info(rule: &EKRecurrenceRule) -> RecurrenceRule {
2069 let frequency = unsafe { rule.frequency() };
2070 let frequency = match frequency {
2071 EKRecurrenceFrequency::Daily => RecurrenceFrequency::Daily,
2072 EKRecurrenceFrequency::Weekly => RecurrenceFrequency::Weekly,
2073 EKRecurrenceFrequency::Monthly => RecurrenceFrequency::Monthly,
2074 EKRecurrenceFrequency::Yearly => RecurrenceFrequency::Yearly,
2075 _ => RecurrenceFrequency::Daily,
2076 };
2077
2078 let interval = unsafe { rule.interval() } as usize;
2079
2080 let end = unsafe { rule.recurrenceEnd() }
2081 .map(|end| {
2082 let count = unsafe { end.occurrenceCount() };
2083 if count > 0 {
2084 RecurrenceEndCondition::AfterCount(count)
2085 } else if let Some(date) = unsafe { end.endDate() } {
2086 RecurrenceEndCondition::OnDate(nsdate_to_datetime(&date))
2087 } else {
2088 RecurrenceEndCondition::Never
2089 }
2090 })
2091 .unwrap_or(RecurrenceEndCondition::Never);
2092
2093 let days_of_week = unsafe { rule.daysOfTheWeek() }.map(|days| {
2094 let mut result = Vec::new();
2095 for i in 0..days.len() {
2096 let day = days.objectAtIndex(i);
2097 let weekday = unsafe { day.dayOfTheWeek() };
2098 result.push(weekday.0 as u8);
2099 }
2100 result
2101 });
2102
2103 let days_of_month = unsafe { rule.daysOfTheMonth() }.map(|days| {
2104 let mut result = Vec::new();
2105 for i in 0..days.len() {
2106 let num = days.objectAtIndex(i);
2107 result.push(num.intValue());
2108 }
2109 result
2110 });
2111
2112 RecurrenceRule {
2113 frequency,
2114 interval,
2115 end,
2116 days_of_week,
2117 days_of_month,
2118 }
2119}
2120
2121fn create_ek_recurrence_rule(rule: &RecurrenceRule) -> Retained<EKRecurrenceRule> {
2123 let frequency = match rule.frequency {
2124 RecurrenceFrequency::Daily => EKRecurrenceFrequency::Daily,
2125 RecurrenceFrequency::Weekly => EKRecurrenceFrequency::Weekly,
2126 RecurrenceFrequency::Monthly => EKRecurrenceFrequency::Monthly,
2127 RecurrenceFrequency::Yearly => EKRecurrenceFrequency::Yearly,
2128 };
2129
2130 let end = match &rule.end {
2131 RecurrenceEndCondition::Never => None,
2132 RecurrenceEndCondition::AfterCount(count) => {
2133 Some(unsafe { EKRecurrenceEnd::recurrenceEndWithOccurrenceCount(*count) })
2134 }
2135 RecurrenceEndCondition::OnDate(date) => {
2136 let nsdate = datetime_to_nsdate(*date);
2137 Some(unsafe { EKRecurrenceEnd::recurrenceEndWithEndDate(&nsdate) })
2138 }
2139 };
2140
2141 let days_of_week: Option<Vec<Retained<EKRecurrenceDayOfWeek>>> =
2142 rule.days_of_week.as_ref().map(|days| {
2143 days.iter()
2144 .map(|&d| {
2145 let weekday = EKWeekday(d as isize);
2146 unsafe { EKRecurrenceDayOfWeek::dayOfWeek(weekday) }
2147 })
2148 .collect()
2149 });
2150
2151 let days_of_month: Option<Vec<Retained<NSNumber>>> = rule
2152 .days_of_month
2153 .as_ref()
2154 .map(|days| days.iter().map(|&d| NSNumber::new_i32(d)).collect());
2155
2156 let days_of_week_arr = days_of_week
2157 .as_ref()
2158 .map(|v| NSArray::from_retained_slice(v));
2159 let days_of_month_arr = days_of_month
2160 .as_ref()
2161 .map(|v| NSArray::from_retained_slice(v));
2162
2163 unsafe {
2164 use objc2::AnyThread;
2165 EKRecurrenceRule::initRecurrenceWithFrequency_interval_daysOfTheWeek_daysOfTheMonth_monthsOfTheYear_weeksOfTheYear_daysOfTheYear_setPositions_end(
2166 EKRecurrenceRule::alloc(),
2167 frequency,
2168 rule.interval as isize,
2169 days_of_week_arr.as_deref(),
2170 days_of_month_arr.as_deref(),
2171 None, None, None, None, end.as_deref(),
2176 )
2177 }
2178}
2179
2180fn alarm_to_info(alarm: &EKAlarm) -> AlarmInfo {
2182 let relative_offset = unsafe { alarm.relativeOffset() };
2183 let absolute_date = unsafe { alarm.absoluteDate() }.map(|d| nsdate_to_datetime(&d));
2184
2185 let proximity = unsafe { alarm.proximity() };
2186 let proximity = match proximity {
2187 EKAlarmProximity::Enter => AlarmProximity::Enter,
2188 EKAlarmProximity::Leave => AlarmProximity::Leave,
2189 _ => AlarmProximity::None,
2190 };
2191
2192 let location = unsafe { alarm.structuredLocation() }.map(|loc| {
2193 let title = unsafe { loc.title() }
2194 .map(|t| t.to_string())
2195 .unwrap_or_default();
2196 let radius = unsafe { loc.radius() };
2197 let (latitude, longitude) = unsafe { loc.geoLocation() }
2198 .map(|geo| {
2199 let coord = unsafe { geo.coordinate() };
2200 (coord.latitude, coord.longitude)
2201 })
2202 .unwrap_or((0.0, 0.0));
2203
2204 StructuredLocation {
2205 title,
2206 latitude,
2207 longitude,
2208 radius,
2209 }
2210 });
2211
2212 AlarmInfo {
2213 relative_offset: Some(relative_offset),
2215 absolute_date,
2216 proximity,
2217 location,
2218 }
2219}
2220
2221fn create_ek_alarm(info: &AlarmInfo) -> Retained<EKAlarm> {
2223 let alarm = if let Some(date) = &info.absolute_date {
2224 let nsdate = datetime_to_nsdate(*date);
2225 unsafe { EKAlarm::alarmWithAbsoluteDate(&nsdate) }
2226 } else {
2227 let offset = info.relative_offset.unwrap_or(0.0);
2228 unsafe { EKAlarm::alarmWithRelativeOffset(offset) }
2229 };
2230
2231 let prox = match info.proximity {
2233 AlarmProximity::Enter => EKAlarmProximity::Enter,
2234 AlarmProximity::Leave => EKAlarmProximity::Leave,
2235 AlarmProximity::None => EKAlarmProximity::None,
2236 };
2237 unsafe { alarm.setProximity(prox) };
2238
2239 if let Some(loc) = &info.location {
2241 let title = NSString::from_str(&loc.title);
2242 let structured = unsafe { EKStructuredLocation::locationWithTitle(&title) };
2243 unsafe { structured.setRadius(loc.radius) };
2244
2245 #[cfg(feature = "location")]
2247 {
2248 use objc2::AnyThread;
2249 use objc2_core_location::CLLocation;
2250 let cl_location = unsafe {
2251 CLLocation::initWithLatitude_longitude(
2252 CLLocation::alloc(),
2253 loc.latitude,
2254 loc.longitude,
2255 )
2256 };
2257 unsafe { structured.setGeoLocation(Some(&cl_location)) };
2258 }
2259
2260 unsafe { alarm.setStructuredLocation(Some(&structured)) };
2261 }
2262
2263 alarm
2264}
2265
2266#[cfg(test)]
2267mod tests {
2268 use super::*;
2269
2270 #[test]
2271 fn test_authorization_status_display() {
2272 assert_eq!(
2273 format!("{}", AuthorizationStatus::NotDetermined),
2274 "Not Determined"
2275 );
2276 assert_eq!(
2277 format!("{}", AuthorizationStatus::FullAccess),
2278 "Full Access"
2279 );
2280 }
2281
2282 #[test]
2283 fn test_event_item_debug() {
2284 let event = EventItem {
2285 identifier: "test".to_string(),
2286 title: "Test Event".to_string(),
2287 notes: None,
2288 location: None,
2289 start_date: Local::now(),
2290 end_date: Local::now(),
2291 all_day: false,
2292 calendar_title: None,
2293 calendar_id: None,
2294 url: None,
2295 availability: EventAvailability::Busy,
2296 status: EventStatus::None,
2297 is_detached: false,
2298 occurrence_date: None,
2299 structured_location: None,
2300 attendees: Vec::new(),
2301 organizer: None,
2302 };
2303 assert!(format!("{:?}", event).contains("Test Event"));
2304 }
2305}