1use block2::RcBlock;
2use chrono::{DateTime, Datelike, Duration, Local, TimeZone, Timelike};
3use objc2::Message;
4use objc2::rc::Retained;
5use objc2::runtime::Bool;
6use objc2_event_kit::{
7 EKAlarm, EKAlarmProximity, EKAlarmType, EKAuthorizationStatus, EKCalendar,
8 EKCalendarEventAvailabilityMask, EKCalendarItem, EKEntityType, EKEvent, EKEventAvailability,
9 EKEventStatus, EKEventStore, EKRecurrenceDayOfWeek, EKRecurrenceEnd, EKRecurrenceFrequency,
10 EKRecurrenceRule, EKReminder, EKSource, EKSpan, EKStructuredLocation, EKWeekday,
11};
12use objc2_foundation::{
13 NSArray, NSCalendar, NSDate, NSDateComponents, NSError, NSNumber, NSString,
14};
15use std::sync::{Arc, Condvar, Mutex};
16use thiserror::Error;
17
18#[cfg(feature = "location")]
19#[path = "location.rs"]
20pub mod location;
21
22#[cfg(feature = "mcp")]
23#[path = "mcp.rs"]
24pub mod mcp;
25
26#[derive(Error, Debug)]
28pub enum EventKitError {
29 #[error("Authorization denied")]
30 AuthorizationDenied,
31
32 #[error("Authorization restricted by system policy")]
33 AuthorizationRestricted,
34
35 #[error("Authorization not determined")]
36 AuthorizationNotDetermined,
37
38 #[error("Failed to request authorization: {0}")]
39 AuthorizationRequestFailed(String),
40
41 #[error("No default calendar")]
42 NoDefaultCalendar,
43
44 #[error("Calendar not found: {0}")]
45 CalendarNotFound(String),
46
47 #[error("Item not found: {0}")]
48 ItemNotFound(String),
49
50 #[error("Failed to save: {0}")]
51 SaveFailed(String),
52
53 #[error("Failed to delete: {0}")]
54 DeleteFailed(String),
55
56 #[error("Failed to fetch: {0}")]
57 FetchFailed(String),
58
59 #[error("EventKit error: {0}")]
60 EventKitError(String),
61
62 #[error("Invalid date range")]
63 InvalidDateRange,
64
65 #[error("Invalid URL: {0}")]
69 InvalidURL(String),
70}
71
72pub type RemindersError = EventKitError;
74
75pub type Result<T> = std::result::Result<T, EventKitError>;
77
78#[derive(Debug, Clone)]
80#[allow(non_snake_case)]
81pub struct ReminderItem {
82 pub identifier: String,
84 pub title: String,
86 pub notes: Option<String>,
88 pub completed: bool,
90 pub priority: usize,
92 pub calendar_title: Option<String>,
94 pub calendar_id: Option<String>,
96 pub due_date: Option<DateTime<Local>>,
98 pub start_date: Option<DateTime<Local>>,
100 pub completion_date: Option<DateTime<Local>>,
102 pub external_identifier: Option<String>,
104 pub location: Option<String>,
106 #[allow(non_snake_case)]
108 pub URL: Option<String>,
109 pub creation_date: Option<DateTime<Local>>,
111 pub last_modified_date: Option<DateTime<Local>>,
113 pub timezone: Option<String>,
115 pub due_date_timezone: Option<String>,
120 pub structured_location: Option<StructuredLocation>,
125 pub parent_id: Option<String>,
128 pub attachments_count: usize,
130 pub has_alarms: bool,
132 pub has_recurrence_rules: bool,
134 pub has_attendees: bool,
136 pub has_notes: bool,
138 pub attendees: Vec<ParticipantInfo>,
140}
141
142#[derive(Debug, Clone, Default)]
145#[allow(non_snake_case)]
146pub struct ReminderDraft<'a> {
147 pub title: &'a str,
148 pub notes: Option<&'a str>,
149 pub calendar_title: Option<&'a str>,
150 pub priority: Option<usize>,
151 pub due_date: Option<DateTime<Local>>,
152 pub start_date: Option<DateTime<Local>>,
153 #[allow(non_snake_case)]
154 pub URL: Option<&'a str>,
155 pub location: Option<&'a str>,
156 pub structured_location: Option<&'a StructuredLocation>,
161 pub due_date_timezone: Option<&'a str>,
163}
164
165#[derive(Debug, Clone, Default)]
169#[allow(non_snake_case)]
170pub struct ReminderPatch<'a> {
171 pub title: Option<&'a str>,
172 pub notes: Option<&'a str>,
173 pub completed: Option<bool>,
174 pub priority: Option<usize>,
175 pub due_date: Option<Option<DateTime<Local>>>,
176 pub start_date: Option<Option<DateTime<Local>>>,
177 pub calendar_title: Option<&'a str>,
178 #[allow(non_snake_case)]
179 pub URL: Option<Option<&'a str>>,
180 pub location: Option<Option<&'a str>>,
181 pub structured_location: Option<Option<&'a StructuredLocation>>,
184 pub due_date_timezone: Option<Option<&'a str>>,
187 pub completion_date: Option<Option<DateTime<Local>>>,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum CalendarType {
198 Local,
199 CalDAV,
200 Exchange,
201 Subscription,
202 Birthday,
203 Unknown,
204}
205
206#[derive(Debug, Clone)]
208pub struct SourceInfo {
209 pub identifier: String,
210 pub title: String,
211 pub source_type: String,
212}
213
214#[derive(Debug, Clone)]
216pub struct CalendarInfo {
217 pub identifier: String,
219 pub title: String,
221 pub source: Option<String>,
223 pub source_id: Option<String>,
225 pub calendar_type: CalendarType,
227 pub allows_modifications: bool,
229 pub is_immutable: bool,
231 pub is_subscribed: bool,
233 pub color: Option<(f64, f64, f64, f64)>,
235 pub allowed_entity_types: Vec<String>,
237 pub supported_event_availabilities: Vec<String>,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
247pub enum AlarmProximity {
248 #[default]
250 None,
251 Enter,
253 Leave,
255}
256
257#[derive(Debug, Clone)]
259pub struct StructuredLocation {
260 pub title: String,
262 pub latitude: f64,
264 pub longitude: f64,
266 pub radius: f64,
268}
269
270#[derive(Debug, Clone, Default)]
272pub struct AlarmInfo {
273 pub relative_offset: Option<f64>,
275 pub absolute_date: Option<DateTime<Local>>,
277 pub proximity: AlarmProximity,
279 pub location: Option<StructuredLocation>,
281 pub email_address: Option<String>,
283 pub sound_name: Option<String>,
285 pub url: Option<String>,
290 pub alarm_type: AlarmType,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
299pub enum AlarmType {
300 #[default]
301 Display,
302 Audio,
303 Procedure,
304 Email,
305 Unknown,
306}
307
308#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
310pub enum RecurrenceFrequency {
311 #[default]
312 Daily,
313 Weekly,
314 Monthly,
315 Yearly,
316}
317
318#[derive(Debug, Clone, Default)]
320pub enum RecurrenceEndCondition {
321 #[default]
323 Never,
324 AfterCount(usize),
326 OnDate(DateTime<Local>),
328}
329
330#[derive(Debug, Clone, Default)]
332pub struct RecurrenceRule {
333 pub frequency: RecurrenceFrequency,
335 pub interval: usize,
337 pub end: RecurrenceEndCondition,
339 pub days_of_week: Option<Vec<u8>>,
341 pub days_of_month: Option<Vec<i32>>,
343 pub months_of_year: Option<Vec<i32>>,
345 pub weeks_of_year: Option<Vec<i32>>,
347 pub days_of_year: Option<Vec<i32>>,
349 pub set_positions: Option<Vec<i32>>,
354}
355
356pub struct RemindersManager {
358 store: Retained<EKEventStore>,
359}
360
361impl RemindersManager {
362 pub fn new() -> Self {
364 let store = unsafe { EKEventStore::new() };
365 Self { store }
366 }
367
368 pub fn authorization_status() -> AuthorizationStatus {
370 let status =
371 unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Reminder) };
372 status.into()
373 }
374
375 pub fn request_access(&self) -> Result<bool> {
379 let result = Arc::new((Mutex::new(None::<(bool, Option<String>)>), Condvar::new()));
380 let result_clone = Arc::clone(&result);
381
382 let completion = RcBlock::new(move |granted: Bool, error: *mut NSError| {
383 let error_msg = if !error.is_null() {
384 let error_ref = unsafe { &*error };
385 Some(format!("{:?}", error_ref))
386 } else {
387 None
388 };
389
390 let (lock, cvar) = &*result_clone;
391 let mut res = lock.lock().unwrap();
392 *res = Some((granted.as_bool(), error_msg));
393 cvar.notify_one();
394 });
395
396 unsafe {
397 let block_ptr = &*completion as *const _ as *mut _;
399 self.store
400 .requestFullAccessToRemindersWithCompletion(block_ptr);
401 }
402
403 let (lock, cvar) = &*result;
404 let mut res = lock.lock().unwrap();
405 while res.is_none() {
406 res = cvar.wait(res).unwrap();
407 }
408
409 match res.take() {
410 Some((granted, None)) => Ok(granted),
411 Some((_, Some(error))) => Err(RemindersError::AuthorizationRequestFailed(error)),
412 None => Err(RemindersError::AuthorizationRequestFailed(
413 "Unknown error".to_string(),
414 )),
415 }
416 }
417
418 pub fn ensure_authorized(&self) -> Result<()> {
420 match Self::authorization_status() {
421 AuthorizationStatus::FullAccess => Ok(()),
422 AuthorizationStatus::NotDetermined => {
423 if self.request_access()? {
424 Ok(())
425 } else {
426 Err(RemindersError::AuthorizationDenied)
427 }
428 }
429 AuthorizationStatus::Denied => Err(RemindersError::AuthorizationDenied),
430 AuthorizationStatus::Restricted => Err(RemindersError::AuthorizationRestricted),
431 AuthorizationStatus::WriteOnly => Ok(()), }
433 }
434
435 pub fn list_calendars(&self) -> Result<Vec<CalendarInfo>> {
437 self.ensure_authorized()?;
438
439 let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
440
441 let mut result = Vec::new();
442 for calendar in calendars.iter() {
443 result.push(calendar_to_info(&calendar));
444 }
445
446 Ok(result)
447 }
448
449 pub fn list_sources(&self) -> Result<Vec<SourceInfo>> {
451 self.ensure_authorized()?;
452 let sources = unsafe { self.store.sources() };
453 let mut result = Vec::new();
454 for source in sources.iter() {
455 result.push(source_to_info(&source));
456 }
457 Ok(result)
458 }
459
460 pub fn default_calendar(&self) -> Result<CalendarInfo> {
462 self.ensure_authorized()?;
463
464 let calendar = unsafe { self.store.defaultCalendarForNewReminders() };
465
466 match calendar {
467 Some(cal) => Ok(calendar_to_info(&cal)),
468 None => Err(RemindersError::NoDefaultCalendar),
469 }
470 }
471
472 pub fn fetch_all_reminders(&self) -> Result<Vec<ReminderItem>> {
474 self.fetch_reminders(None)
475 }
476
477 pub fn fetch_reminders(&self, calendar_titles: Option<&[&str]>) -> Result<Vec<ReminderItem>> {
479 self.ensure_authorized()?;
480
481 let calendars: Option<Retained<NSArray<EKCalendar>>> = match calendar_titles {
482 Some(titles) => {
483 let all_calendars =
484 unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
485 let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
486
487 for cal in all_calendars.iter() {
488 let title = unsafe { cal.title() };
489 let title_str = title.to_string();
490 if titles.iter().any(|t| *t == title_str) {
491 matching.push(cal.retain());
492 }
493 }
494
495 if matching.is_empty() {
496 return Err(RemindersError::CalendarNotFound(titles.join(", ")));
497 }
498
499 Some(NSArray::from_retained_slice(&matching))
500 }
501 None => None,
502 };
503
504 let predicate = unsafe {
505 self.store
506 .predicateForRemindersInCalendars(calendars.as_deref())
507 };
508
509 let result = Arc::new((Mutex::new(None::<Vec<ReminderItem>>), Condvar::new()));
510 let result_clone = Arc::clone(&result);
511
512 let completion = RcBlock::new(move |reminders: *mut NSArray<EKReminder>| {
513 let items = if reminders.is_null() {
514 Vec::new()
515 } else {
516 let reminders = unsafe { Retained::retain(reminders).unwrap() };
517 reminders.iter().map(|r| reminder_to_item(&r)).collect()
518 };
519 let (lock, cvar) = &*result_clone;
520 let mut guard = lock.lock().unwrap();
521 *guard = Some(items);
522 cvar.notify_one();
523 });
524
525 unsafe {
526 self.store
527 .fetchRemindersMatchingPredicate_completion(&predicate, &completion);
528 }
529
530 let (lock, cvar) = &*result;
531 let mut guard = lock.lock().unwrap();
532 while guard.is_none() {
533 guard = cvar.wait(guard).unwrap();
534 }
535
536 guard
537 .take()
538 .ok_or_else(|| RemindersError::FetchFailed("Unknown error".to_string()))
539 }
540
541 pub fn fetch_incomplete_reminders(&self) -> Result<Vec<ReminderItem>> {
543 self.fetch_incomplete_reminders_in_due_range(None, None, None)
544 }
545
546 pub fn fetch_incomplete_reminders_in_due_range(
551 &self,
552 starting: Option<DateTime<Local>>,
553 ending: Option<DateTime<Local>>,
554 calendar_titles: Option<&[&str]>,
555 ) -> Result<Vec<ReminderItem>> {
556 self.ensure_authorized()?;
557 let calendars = self.resolve_reminder_calendars(calendar_titles)?;
558 let start_ns = starting.map(datetime_to_nsdate);
559 let end_ns = ending.map(datetime_to_nsdate);
560 let predicate = unsafe {
561 self.store
562 .predicateForIncompleteRemindersWithDueDateStarting_ending_calendars(
563 start_ns.as_deref(),
564 end_ns.as_deref(),
565 calendars.as_deref(),
566 )
567 };
568 self.fetch_by_predicate(&predicate)
569 }
570
571 pub fn fetch_completed_reminders_in_range(
575 &self,
576 starting: Option<DateTime<Local>>,
577 ending: Option<DateTime<Local>>,
578 calendar_titles: Option<&[&str]>,
579 ) -> Result<Vec<ReminderItem>> {
580 self.ensure_authorized()?;
581 let calendars = self.resolve_reminder_calendars(calendar_titles)?;
582 let start_ns = starting.map(datetime_to_nsdate);
583 let end_ns = ending.map(datetime_to_nsdate);
584 let predicate = unsafe {
585 self.store
586 .predicateForCompletedRemindersWithCompletionDateStarting_ending_calendars(
587 start_ns.as_deref(),
588 end_ns.as_deref(),
589 calendars.as_deref(),
590 )
591 };
592 self.fetch_by_predicate(&predicate)
593 }
594
595 fn resolve_reminder_calendars(
599 &self,
600 calendar_titles: Option<&[&str]>,
601 ) -> Result<Option<Retained<NSArray<EKCalendar>>>> {
602 let Some(titles) = calendar_titles else {
603 return Ok(None);
604 };
605 let all_calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
606 let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
607 for cal in all_calendars.iter() {
608 let title_str = unsafe { cal.title() }.to_string();
609 if titles.iter().any(|t| *t == title_str) {
610 matching.push(cal.retain());
611 }
612 }
613 if matching.is_empty() {
614 return Err(RemindersError::CalendarNotFound(titles.join(", ")));
615 }
616 Ok(Some(NSArray::from_retained_slice(&matching)))
617 }
618
619 fn fetch_by_predicate(
623 &self,
624 predicate: &objc2_foundation::NSPredicate,
625 ) -> Result<Vec<ReminderItem>> {
626 let result = Arc::new((Mutex::new(None::<Vec<ReminderItem>>), Condvar::new()));
627 let result_clone = Arc::clone(&result);
628
629 let completion = RcBlock::new(move |reminders: *mut NSArray<EKReminder>| {
630 let items = if reminders.is_null() {
631 Vec::new()
632 } else {
633 let reminders = unsafe { Retained::retain(reminders).unwrap() };
634 reminders.iter().map(|r| reminder_to_item(&r)).collect()
635 };
636 let (lock, cvar) = &*result_clone;
637 let mut guard = lock.lock().unwrap();
638 *guard = Some(items);
639 cvar.notify_one();
640 });
641
642 unsafe {
643 self.store
644 .fetchRemindersMatchingPredicate_completion(predicate, &completion);
645 }
646
647 let (lock, cvar) = &*result;
648 let mut guard = lock.lock().unwrap();
649 while guard.is_none() {
650 guard = cvar.wait(guard).unwrap();
651 }
652 guard
653 .take()
654 .ok_or_else(|| RemindersError::FetchFailed("Unknown error".to_string()))
655 }
656
657 pub fn create_reminder(&self, draft: &ReminderDraft<'_>) -> Result<ReminderItem> {
660 self.ensure_authorized()?;
661
662 let reminder = unsafe { EKReminder::reminderWithEventStore(&self.store) };
663
664 let ns_title = NSString::from_str(draft.title);
665 unsafe { reminder.setTitle(Some(&ns_title)) };
666
667 if let Some(notes_text) = draft.notes {
668 let ns_notes = NSString::from_str(notes_text);
669 unsafe { reminder.setNotes(Some(&ns_notes)) };
670 }
671
672 if let Some(p) = draft.priority {
673 unsafe { reminder.setPriority(p) };
674 }
675
676 if let Some(due) = draft.due_date {
677 let components = datetime_to_date_components(due);
678 unsafe { reminder.setDueDateComponents(Some(&components)) };
679 }
680
681 if let Some(start) = draft.start_date {
682 let components = datetime_to_date_components(start);
683 unsafe { reminder.setStartDateComponents(Some(&components)) };
684 }
685
686 if draft.URL.is_some() {
687 set_item_URL(&reminder, draft.URL)?;
688 }
689 if draft.location.is_some() {
690 set_item_location(&reminder, draft.location);
691 }
692 if let Some(sl) = draft.structured_location {
693 set_reminder_structured_location(&reminder, Some(sl));
694 }
695 if draft.due_date_timezone.is_some() {
696 set_reminder_due_date_timezone(&reminder, draft.due_date_timezone);
697 }
698
699 let calendar = if let Some(cal_title) = draft.calendar_title {
700 self.find_calendar_by_title(cal_title)?
701 } else {
702 unsafe { self.store.defaultCalendarForNewReminders() }
703 .ok_or(RemindersError::NoDefaultCalendar)?
704 };
705 unsafe { reminder.setCalendar(Some(&calendar)) };
706
707 self.save_reminder_and_refresh(&reminder)?;
708
709 Ok(reminder_to_item(&reminder))
710 }
711
712 pub fn update_reminder(
716 &self,
717 identifier: &str,
718 patch: &ReminderPatch<'_>,
719 ) -> Result<ReminderItem> {
720 self.ensure_authorized()?;
721
722 let reminder = self.find_reminder_by_id(identifier)?;
723
724 if let Some(t) = patch.title {
725 let ns_title = NSString::from_str(t);
726 unsafe { reminder.setTitle(Some(&ns_title)) };
727 }
728
729 if let Some(n) = patch.notes {
730 let ns_notes = NSString::from_str(n);
731 unsafe { reminder.setNotes(Some(&ns_notes)) };
732 }
733
734 if let Some(c) = patch.completed {
735 unsafe { reminder.setCompleted(c) };
736 }
737
738 if let Some(p) = patch.priority {
739 unsafe { reminder.setPriority(p) };
740 }
741
742 if let Some(due_opt) = patch.due_date {
743 match due_opt {
744 Some(due) => {
745 let components = datetime_to_date_components(due);
746 unsafe { reminder.setDueDateComponents(Some(&components)) };
747 }
748 None => unsafe { reminder.setDueDateComponents(None) },
749 }
750 }
751
752 if let Some(start_opt) = patch.start_date {
753 match start_opt {
754 Some(start) => {
755 let components = datetime_to_date_components(start);
756 unsafe { reminder.setStartDateComponents(Some(&components)) };
757 }
758 None => unsafe { reminder.setStartDateComponents(None) },
759 }
760 }
761
762 if let Some(url_opt) = patch.URL {
763 set_item_URL(&reminder, url_opt)?;
764 }
765 if let Some(loc_opt) = patch.location {
766 set_item_location(&reminder, loc_opt);
767 }
768 if let Some(sl_opt) = patch.structured_location {
769 set_reminder_structured_location(&reminder, sl_opt);
770 }
771 if let Some(tz_opt) = patch.due_date_timezone {
772 set_reminder_due_date_timezone(&reminder, tz_opt);
773 }
774
775 if let Some(cd_opt) = patch.completion_date {
778 match cd_opt {
779 Some(d) => {
780 let nsdate = datetime_to_nsdate(d);
781 unsafe { reminder.setCompletionDate(Some(&nsdate)) };
782 }
783 None => unsafe { reminder.setCompletionDate(None) },
784 }
785 }
786
787 if let Some(cal_title) = patch.calendar_title {
788 let calendar = self.find_calendar_by_title(cal_title)?;
789 unsafe { reminder.setCalendar(Some(&calendar)) };
790 }
791
792 self.save_reminder_and_refresh(&reminder)?;
793
794 Ok(reminder_to_item(&reminder))
795 }
796
797 pub fn complete_reminder(&self, identifier: &str) -> Result<ReminderItem> {
799 self.update_reminder(
800 identifier,
801 &ReminderPatch {
802 completed: Some(true),
803 ..Default::default()
804 },
805 )
806 }
807
808 pub fn uncomplete_reminder(&self, identifier: &str) -> Result<ReminderItem> {
810 self.update_reminder(
811 identifier,
812 &ReminderPatch {
813 completed: Some(false),
814 ..Default::default()
815 },
816 )
817 }
818
819 pub fn delete_reminder(&self, identifier: &str) -> Result<()> {
821 self.ensure_authorized()?;
822
823 let reminder = self.find_reminder_by_id(identifier)?;
824
825 unsafe {
826 self.store
827 .removeReminder_commit_error(&reminder, true)
828 .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
829 }
830
831 Ok(())
832 }
833
834 pub fn get_reminder(&self, identifier: &str) -> Result<ReminderItem> {
836 self.ensure_authorized()?;
837 let reminder = self.find_reminder_by_id(identifier)?;
838 Ok(reminder_to_item(&reminder))
839 }
840
841 pub fn dump_reminder_raw(&self, identifier: &str, read_values: bool) -> Result<String> {
852 self.ensure_authorized()?;
853 let reminder = self.find_reminder_by_id(identifier)?;
854
855 let mut out = String::new();
856 out.push_str(&reflect_object_full(
857 "EKReminder (instance)",
858 &*reminder,
859 read_values,
860 ));
861
862 if let Some(calendar) = unsafe { reminder.calendar() } {
863 out.push('\n');
864 out.push_str(&reflect_object_full(
865 "EKCalendar (reminder.calendar)",
866 &*calendar,
867 read_values,
868 ));
869
870 if let Some(source) = unsafe { calendar.source() } {
871 out.push('\n');
872 out.push_str(&reflect_object_full(
873 "EKSource (calendar.source)",
874 &*source,
875 read_values,
876 ));
877 }
878 }
879
880 Ok(out)
881 }
882
883 pub fn dump_reminder_private(&self, identifier: &str) -> Result<String> {
889 self.ensure_authorized()?;
890 let reminder = self.find_reminder_by_id(identifier)?;
891 Ok(probe_private_selectors(&reminder))
892 }
893
894 pub fn get_alarms(&self, identifier: &str) -> Result<Vec<AlarmInfo>> {
900 self.ensure_authorized()?;
901 let reminder = self.find_reminder_by_id(identifier)?;
902 Ok(get_item_alarms(&reminder))
903 }
904
905 pub fn add_alarm(&self, identifier: &str, alarm: &AlarmInfo) -> Result<()> {
907 self.ensure_authorized()?;
908 let reminder = self.find_reminder_by_id(identifier)?;
909 add_item_alarm(&reminder, alarm)?;
910 self.save_reminder_and_refresh(&reminder)?;
911 Ok(())
912 }
913
914 pub fn remove_all_alarms(&self, identifier: &str) -> Result<()> {
916 self.ensure_authorized()?;
917 let reminder = self.find_reminder_by_id(identifier)?;
918 clear_item_alarms(&reminder);
919 self.save_reminder_and_refresh(&reminder)?;
920 Ok(())
921 }
922
923 pub fn remove_alarm(&self, identifier: &str, index: usize) -> Result<()> {
925 self.ensure_authorized()?;
926 let reminder = self.find_reminder_by_id(identifier)?;
927 remove_item_alarm(&reminder, index)?;
928 self.save_reminder_and_refresh(&reminder)?;
929 Ok(())
930 }
931
932 #[allow(non_snake_case)]
938 pub fn set_URL(&self, identifier: &str, url: Option<&str>) -> Result<()> {
939 self.ensure_authorized()?;
940 let reminder = self.find_reminder_by_id(identifier)?;
941 set_item_URL(&reminder, url)?;
942 self.save_reminder_and_refresh(&reminder)?;
943 Ok(())
944 }
945
946 pub fn set_location(&self, identifier: &str, location: Option<&str>) -> Result<()> {
948 self.ensure_authorized()?;
949 let reminder = self.find_reminder_by_id(identifier)?;
950 set_item_location(&reminder, location);
951 self.save_reminder_and_refresh(&reminder)?;
952 Ok(())
953 }
954
955 pub fn set_due_date_timezone(&self, identifier: &str, tz_name: Option<&str>) -> Result<()> {
958 self.ensure_authorized()?;
959 let reminder = self.find_reminder_by_id(identifier)?;
960 set_reminder_due_date_timezone(&reminder, tz_name);
961 self.save_reminder_and_refresh(&reminder)?;
962 Ok(())
963 }
964
965 pub fn set_structured_location(
976 &self,
977 identifier: &str,
978 loc: Option<&StructuredLocation>,
979 ) -> Result<()> {
980 self.ensure_authorized()?;
981 let reminder = self.find_reminder_by_id(identifier)?;
982 set_reminder_structured_location(&reminder, loc);
983 self.save_reminder_and_refresh(&reminder)?;
984 Ok(())
985 }
986
987 pub fn set_geofence(
994 &self,
995 identifier: &str,
996 geofence: Option<(&StructuredLocation, AlarmProximity)>,
997 ) -> Result<()> {
998 self.ensure_authorized()?;
999
1000 #[cfg(feature = "location")]
1003 if geofence.is_some() {
1004 use crate::location::{LocationAuthorizationStatus as L, LocationManager};
1005 let loc_mgr = LocationManager::new();
1006 if loc_mgr.authorization_status() == L::NotDetermined {
1007 loc_mgr.request_when_in_use_authorization();
1008 std::thread::sleep(std::time::Duration::from_millis(500));
1011 }
1012 match loc_mgr.authorization_status() {
1013 L::Authorized => {}
1014 L::Denied => return Err(EventKitError::AuthorizationDenied),
1015 L::Restricted => return Err(EventKitError::AuthorizationRestricted),
1016 L::NotDetermined => return Err(EventKitError::AuthorizationNotDetermined),
1017 }
1018 }
1019
1020 let reminder = self.find_reminder_by_id(identifier)?;
1021 set_reminder_geofence(&reminder, geofence);
1022 self.save_reminder_and_refresh(&reminder)?;
1023 Ok(())
1024 }
1025
1026 pub fn get_recurrence_rules(&self, identifier: &str) -> Result<Vec<RecurrenceRule>> {
1032 self.ensure_authorized()?;
1033 let reminder = self.find_reminder_by_id(identifier)?;
1034 Ok(get_item_recurrence_rules(&reminder))
1035 }
1036
1037 pub fn set_recurrence_rule(&self, identifier: &str, rule: &RecurrenceRule) -> Result<()> {
1039 self.ensure_authorized()?;
1040 let reminder = self.find_reminder_by_id(identifier)?;
1041 set_item_recurrence_rule(&reminder, rule);
1042 self.save_reminder_and_refresh(&reminder)?;
1043 Ok(())
1044 }
1045
1046 pub fn remove_recurrence_rules(&self, identifier: &str) -> Result<()> {
1048 self.ensure_authorized()?;
1049 let reminder = self.find_reminder_by_id(identifier)?;
1050 clear_item_recurrence_rules(&reminder);
1051 self.save_reminder_and_refresh(&reminder)?;
1052 Ok(())
1053 }
1054
1055 pub fn create_calendar(&self, title: &str) -> Result<CalendarInfo> {
1063 self.ensure_authorized()?;
1064
1065 let calendar = unsafe {
1067 EKCalendar::calendarForEntityType_eventStore(EKEntityType::Reminder, &self.store)
1068 };
1069
1070 let ns_title = NSString::from_str(title);
1072 unsafe { calendar.setTitle(&ns_title) };
1073
1074 let source = self.find_best_source_for_reminders()?;
1076 unsafe { calendar.setSource(Some(&source)) };
1077
1078 unsafe {
1080 self.store
1081 .saveCalendar_commit_error(&calendar, true)
1082 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1083 }
1084
1085 Ok(calendar_to_info(&calendar))
1086 }
1087
1088 pub fn rename_calendar(&self, identifier: &str, new_title: &str) -> Result<CalendarInfo> {
1091 self.update_calendar(identifier, Some(new_title), None)
1092 }
1093
1094 pub fn update_calendar(
1096 &self,
1097 identifier: &str,
1098 new_title: Option<&str>,
1099 color_rgba: Option<(f64, f64, f64, f64)>,
1100 ) -> Result<CalendarInfo> {
1101 self.ensure_authorized()?;
1102 let calendar = self.find_calendar_by_id(identifier)?;
1103
1104 if !unsafe { calendar.allowsContentModifications() } {
1105 return Err(EventKitError::SaveFailed(
1106 "Calendar does not allow modifications".to_string(),
1107 ));
1108 }
1109
1110 if let Some(title) = new_title {
1111 let ns_title = NSString::from_str(title);
1112 unsafe { calendar.setTitle(&ns_title) };
1113 }
1114
1115 if let Some((r, g, b, a)) = color_rgba {
1116 let cg = objc2_core_graphics::CGColor::new_srgb(r, g, b, a);
1117 unsafe { calendar.setCGColor(Some(&cg)) };
1118 }
1119
1120 unsafe {
1121 self.store
1122 .saveCalendar_commit_error(&calendar, true)
1123 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1124 }
1125
1126 Ok(calendar_to_info(&calendar))
1127 }
1128
1129 pub fn delete_calendar(&self, identifier: &str) -> Result<()> {
1133 self.ensure_authorized()?;
1134
1135 let calendar = self.find_calendar_by_id(identifier)?;
1136
1137 if !unsafe { calendar.allowsContentModifications() } {
1139 return Err(EventKitError::DeleteFailed(
1140 "Calendar does not allow modifications".to_string(),
1141 ));
1142 }
1143
1144 unsafe {
1145 self.store
1146 .removeCalendar_commit_error(&calendar, true)
1147 .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
1148 }
1149
1150 Ok(())
1151 }
1152
1153 pub fn get_calendar(&self, identifier: &str) -> Result<CalendarInfo> {
1155 self.ensure_authorized()?;
1156 let calendar = self.find_calendar_by_id(identifier)?;
1157 Ok(calendar_to_info(&calendar))
1158 }
1159
1160 fn find_best_source_for_reminders(&self) -> Result<Retained<objc2_event_kit::EKSource>> {
1162 if let Some(default_cal) = unsafe { self.store.defaultCalendarForNewReminders() }
1164 && let Some(source) = unsafe { default_cal.source() }
1165 {
1166 return Ok(source);
1167 }
1168
1169 let sources = unsafe { self.store.sources() };
1171 for source in sources.iter() {
1172 let calendars = unsafe { source.calendarsForEntityType(EKEntityType::Reminder) };
1174 if !calendars.is_empty() {
1175 return Ok(source.retain());
1176 }
1177 }
1178
1179 Err(EventKitError::SaveFailed(
1180 "No suitable source found for creating reminder calendar".to_string(),
1181 ))
1182 }
1183
1184 fn find_calendar_by_id(&self, identifier: &str) -> Result<Retained<EKCalendar>> {
1186 let ns_id = NSString::from_str(identifier);
1187 let calendar = unsafe { self.store.calendarWithIdentifier(&ns_id) };
1188
1189 match calendar {
1190 Some(cal) => Ok(cal),
1191 None => Err(EventKitError::CalendarNotFound(identifier.to_string())),
1192 }
1193 }
1194
1195 fn find_calendar_by_title(&self, title: &str) -> Result<Retained<EKCalendar>> {
1197 let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
1198
1199 for cal in calendars.iter() {
1200 let cal_title = unsafe { cal.title() };
1201 if cal_title.to_string() == title {
1202 return Ok(cal.retain());
1203 }
1204 }
1205
1206 Err(RemindersError::CalendarNotFound(title.to_string()))
1207 }
1208
1209 fn save_reminder_and_refresh(&self, reminder: &EKReminder) -> Result<()> {
1215 unsafe {
1216 self.store
1217 .saveReminder_commit_error(reminder, true)
1218 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1219 self.store.refreshSourcesIfNecessary();
1220 }
1221 Ok(())
1222 }
1223
1224 fn find_reminder_by_id(&self, identifier: &str) -> Result<Retained<EKReminder>> {
1225 unsafe { self.store.refreshSourcesIfNecessary() };
1229
1230 let ns_id = NSString::from_str(identifier);
1231 let item = unsafe { self.store.calendarItemWithIdentifier(&ns_id) };
1232
1233 match item {
1234 Some(item) => {
1235 if let Some(reminder) = item.downcast_ref::<EKReminder>() {
1237 Ok(reminder.retain())
1238 } else {
1239 Err(EventKitError::ItemNotFound(identifier.to_string()))
1240 }
1241 }
1242 None => Err(EventKitError::ItemNotFound(identifier.to_string())),
1243 }
1244 }
1245}
1246
1247impl Default for RemindersManager {
1248 fn default() -> Self {
1249 Self::new()
1250 }
1251}
1252
1253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1255pub enum AuthorizationStatus {
1256 NotDetermined,
1258 Restricted,
1260 Denied,
1262 FullAccess,
1264 WriteOnly,
1266}
1267
1268impl From<EKAuthorizationStatus> for AuthorizationStatus {
1269 fn from(status: EKAuthorizationStatus) -> Self {
1270 if status == EKAuthorizationStatus::NotDetermined {
1271 AuthorizationStatus::NotDetermined
1272 } else if status == EKAuthorizationStatus::Restricted {
1273 AuthorizationStatus::Restricted
1274 } else if status == EKAuthorizationStatus::Denied {
1275 AuthorizationStatus::Denied
1276 } else if status == EKAuthorizationStatus::FullAccess {
1277 AuthorizationStatus::FullAccess
1278 } else if status == EKAuthorizationStatus::WriteOnly {
1279 AuthorizationStatus::WriteOnly
1280 } else {
1281 AuthorizationStatus::NotDetermined
1282 }
1283 }
1284}
1285
1286impl std::fmt::Display for AuthorizationStatus {
1287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1288 match self {
1289 AuthorizationStatus::NotDetermined => write!(f, "Not Determined"),
1290 AuthorizationStatus::Restricted => write!(f, "Restricted"),
1291 AuthorizationStatus::Denied => write!(f, "Denied"),
1292 AuthorizationStatus::FullAccess => write!(f, "Full Access"),
1293 AuthorizationStatus::WriteOnly => write!(f, "Write Only"),
1294 }
1295 }
1296}
1297
1298const REFLECT_VALUE_DENYLIST: &[&str] =
1309 &["objectID", "objectIDURIRepresentation", "URIRepresentation"];
1310
1311fn reflect_object_full<T: Message>(header: &str, obj: &T, read_values: bool) -> String {
1312 use objc2::ffi::{class_copyPropertyList, free, property_getAttributes, property_getName};
1313 use objc2::msg_send;
1314 use objc2::rc::Retained;
1315 use objc2::runtime::{AnyClass, AnyObject};
1316 use std::ffi::CStr;
1317
1318 let obj: &AnyObject = unsafe { &*(obj as *const T as *const AnyObject) };
1321
1322 let mut out = String::new();
1323 out.push_str(&format!("=== {header} ===\n"));
1324
1325 let mut chain: Vec<&'static AnyClass> = Vec::new();
1327 let mut current = Some(obj.class());
1328 while let Some(cls) = current {
1329 chain.push(cls);
1330 current = cls.superclass();
1331 }
1332 out.push_str("Class chain: ");
1333 out.push_str(
1334 &chain
1335 .iter()
1336 .map(|c| c.name().to_string_lossy().into_owned())
1337 .collect::<Vec<_>>()
1338 .join(" -> "),
1339 );
1340 out.push_str("\n\n");
1341
1342 for cls in &chain {
1343 let cls_name = cls.name().to_string_lossy().into_owned();
1344 if cls_name == "NSObject" {
1346 continue;
1347 }
1348
1349 let mut count: u32 = 0;
1350 let props_ptr =
1351 unsafe { class_copyPropertyList(*cls as *const AnyClass, &mut count as *mut u32) };
1352 if props_ptr.is_null() || count == 0 {
1353 if !props_ptr.is_null() {
1354 unsafe { free(props_ptr as *mut _) };
1355 }
1356 continue;
1357 }
1358
1359 let mut entries: Vec<(String, String, String)> = Vec::new();
1360 for i in 0..count as isize {
1361 let prop = unsafe { *props_ptr.offset(i) };
1362 if prop.is_null() {
1363 continue;
1364 }
1365 let name_ptr = unsafe { property_getName(prop) };
1366 let attrs_ptr = unsafe { property_getAttributes(prop) };
1367 let name = if name_ptr.is_null() {
1368 String::from("<?>")
1369 } else {
1370 unsafe { CStr::from_ptr(name_ptr) }
1371 .to_string_lossy()
1372 .into_owned()
1373 };
1374 let attrs = if attrs_ptr.is_null() {
1375 String::new()
1376 } else {
1377 unsafe { CStr::from_ptr(attrs_ptr) }
1378 .to_string_lossy()
1379 .into_owned()
1380 };
1381
1382 let type_is_objectid = attrs.contains("ObjectID");
1387
1388 let value_str = if !read_values {
1389 String::from("<value read skipped (pass --values to attempt)>")
1390 } else if REFLECT_VALUE_DENYLIST.contains(&name.as_str()) || type_is_objectid {
1391 String::from("<skipped: type/name denylisted (would abort process)>")
1392 } else {
1393 let name_for_closure = name.clone();
1397 let obj_ptr = obj as *const AnyObject;
1398 match objc2::exception::catch(std::panic::AssertUnwindSafe(move || {
1399 let key = NSString::from_str(&name_for_closure);
1400 let obj_ref: &AnyObject = unsafe { &*obj_ptr };
1401 let value_obj: Option<Retained<AnyObject>> =
1402 unsafe { msg_send![obj_ref, valueForKey: &*key] };
1403 match value_obj {
1404 None => String::from("(null)"),
1405 Some(v) => describe_object(&v),
1406 }
1407 })) {
1408 Ok(s) => s,
1409 Err(Some(exc)) => {
1410 format!("<NSException: {}>", describe_object(&**exc as &AnyObject))
1411 }
1412 Err(None) => String::from("<unknown ObjC exception>"),
1413 }
1414 };
1415
1416 entries.push((name, attrs, value_str));
1417 }
1418 unsafe { free(props_ptr as *mut _) };
1419
1420 if entries.is_empty() {
1421 continue;
1422 }
1423 entries.sort_by(|a, b| a.0.cmp(&b.0));
1424
1425 out.push_str(&format!("[{cls_name}]\n"));
1426 for (name, attrs, value) in entries {
1427 out.push_str(&format!(" {name}\n"));
1428 out.push_str(&format!(" attrs: {attrs}\n"));
1429 let indented = value.replace('\n', "\n ");
1431 out.push_str(&format!(" value: {indented}\n"));
1432 }
1433 out.push('\n');
1434 }
1435
1436 out
1437}
1438
1439fn probe_private_selectors(reminder: &EKReminder) -> String {
1449 use objc2::msg_send;
1450 use objc2::rc::Retained;
1451 use objc2::runtime::{AnyObject, Sel};
1452 use std::ffi::CString;
1453
1454 const PROBES: &[&str] = &[
1455 "appLink", "URLString", "externalData", "externalModificationTag",
1460 "richLink",
1461 "richLinkData",
1462 "URLWithPreview",
1463 "urlPreview",
1464 "linkPreview",
1465 "siriSuggestedURL",
1466 "displayURL",
1467 "flaggedURL",
1468 "_richLink",
1469 "_url",
1470 "primaryLink",
1471 "structuredLocation",
1473 "geoLocation",
1474 "place",
1475 "placemark",
1476 "tags",
1478 "tagDictionaries",
1479 "tagNames",
1480 "userTags",
1481 "_tags",
1482 "_tagDictionaries",
1483 "hashtags",
1484 "structuredData",
1486 "localStructuredData",
1487 "_structuredData",
1488 "meltedObject",
1490 "unfrozen",
1491 "unfrozenObject",
1492 "writableInstance",
1493 "flags",
1495 "flags2",
1496 "isFlagged",
1497 "image",
1498 "imageData",
1499 "thumbnail",
1500 "color",
1501 "creatorBundleID",
1502 "parentReminder",
1504 "subreminders",
1505 "subtasks",
1506 "childReminders",
1507 ];
1508
1509 let obj: &AnyObject = unsafe { &*(reminder as *const EKReminder as *const AnyObject) };
1510 let mut out = String::from("=== Private-selector probe on EKReminder ===\n");
1511 out.push_str("(each call wrapped in @try/@catch — caught exceptions mean the selector doesn't exist on this class)\n\n");
1512
1513 for name in PROBES {
1514 let c_name = CString::new(*name).expect("selector name has no NUL");
1515 let sel = Sel::register(&c_name);
1516 let name_owned = name.to_string();
1517 let obj_ptr = obj as *const AnyObject;
1518 let line = match objc2::exception::catch(std::panic::AssertUnwindSafe(move || {
1519 let obj_ref: &AnyObject = unsafe { &*obj_ptr };
1522 let responds: bool = unsafe { msg_send![obj_ref, respondsToSelector: sel] };
1523 if !responds {
1524 return format!("{name_owned}: <does not respond>");
1525 }
1526 let result: Option<Retained<AnyObject>> =
1527 unsafe { msg_send![obj_ref, performSelector: sel] };
1528 match result {
1529 None => format!("{name_owned}: (null)"),
1530 Some(v) => {
1531 let cls: &objc2::runtime::AnyClass = (*v).class();
1532 let cls_name = cls.name().to_string_lossy().into_owned();
1533 let desc = describe_object(&v);
1534 let one_line = desc.replace('\n', "\\n");
1535 format!("{name_owned}: [{cls_name}] {one_line}")
1536 }
1537 }
1538 })) {
1539 Ok(s) => s,
1540 Err(Some(exc)) => format!(
1541 "{name}: <NSException: {}>",
1542 describe_object(&**exc as &AnyObject)
1543 ),
1544 Err(None) => format!("{name}: <unknown ObjC exception>"),
1545 };
1546 out.push_str(&line);
1547 out.push('\n');
1548 }
1549
1550 let backing: Option<Retained<AnyObject>> = unsafe { msg_send![obj, backingObject] };
1554 if let Some(backing) = backing {
1555 let bcls = (*backing).class().name().to_string_lossy().into_owned();
1556 out.push_str(&format!("\n--- backingObject ({bcls}) probe ---\n"));
1557 for name in PROBES {
1558 let c_name = std::ffi::CString::new(*name).expect("no NUL");
1559 let sel = objc2::runtime::Sel::register(&c_name);
1560 let name_owned = name.to_string();
1561 let backing_ptr = &*backing as *const AnyObject;
1562 let line = match objc2::exception::catch(std::panic::AssertUnwindSafe(move || {
1563 let bref: &AnyObject = unsafe { &*backing_ptr };
1564 let responds: bool = unsafe { msg_send![bref, respondsToSelector: sel] };
1565 if !responds {
1566 return format!("{name_owned}: <does not respond>");
1567 }
1568 let result: Option<Retained<AnyObject>> =
1569 unsafe { msg_send![bref, performSelector: sel] };
1570 match result {
1571 None => format!("{name_owned}: (null)"),
1572 Some(v) => {
1573 let cls = (*v).class().name().to_string_lossy().into_owned();
1574 format!(
1575 "{name_owned}: [{cls}] {}",
1576 describe_object(&v).replace('\n', "\\n")
1577 )
1578 }
1579 }
1580 })) {
1581 Ok(s) => s,
1582 Err(Some(exc)) => format!(
1583 "{name}: <NSException: {}>",
1584 describe_object(&**exc as &AnyObject)
1585 ),
1586 Err(None) => format!("{name}: <unknown ObjC exception>"),
1587 };
1588 out.push_str(&line);
1589 out.push('\n');
1590 }
1591 }
1592
1593 out.push_str(
1597 "\n--- All zero-arg instance methods on EKReminder containing url/tag/link/data ---\n",
1598 );
1599 out.push_str(&list_interesting_methods(obj.class()));
1600 out
1601}
1602
1603fn list_interesting_methods(start: &objc2::runtime::AnyClass) -> String {
1607 use objc2::ffi::{class_copyMethodList, free, method_getName};
1608 use objc2::runtime::AnyClass;
1609 use std::ffi::CStr;
1610
1611 let needles = [
1612 "url",
1613 "tag",
1614 "link",
1615 "data",
1616 "flag",
1617 "image",
1618 "preview",
1619 "attachment",
1620 "rich",
1621 "location",
1622 "structured",
1623 "place",
1624 "address",
1625 "geo",
1626 ];
1627 let mut hits: Vec<(String, String)> = Vec::new();
1628
1629 let mut current: Option<&AnyClass> = Some(start);
1630 while let Some(cls) = current {
1631 let cls_name = cls.name().to_string_lossy().into_owned();
1632 if cls_name == "NSObject" {
1633 break;
1634 }
1635 let mut count: u32 = 0;
1636 let ptr = unsafe { class_copyMethodList(cls as *const AnyClass, &mut count as *mut u32) };
1637 if !ptr.is_null() {
1638 for i in 0..count as isize {
1639 let m = unsafe { *ptr.offset(i) };
1640 if m.is_null() {
1641 continue;
1642 }
1643 let Some(sel) = (unsafe { method_getName(m) }) else {
1644 continue;
1645 };
1646 let name_cstr_ptr = unsafe { objc2::ffi::sel_getName(sel) };
1647 if name_cstr_ptr.is_null() {
1648 continue;
1649 }
1650 let name = unsafe { CStr::from_ptr(name_cstr_ptr) }
1651 .to_string_lossy()
1652 .into_owned();
1653 let lower = name.to_lowercase();
1654 if needles.iter().any(|n| lower.contains(n)) {
1655 hits.push((cls_name.clone(), name));
1656 }
1657 }
1658 unsafe { free(ptr as *mut _) };
1659 }
1660 current = cls.superclass();
1661 }
1662 hits.sort();
1663 hits.dedup();
1664 if hits.is_empty() {
1665 return String::from("(none)\n");
1666 }
1667 let mut out = String::new();
1668 for (cls, sel) in hits {
1669 out.push_str(&format!(" [{cls}] {sel}\n"));
1670 }
1671 out
1672}
1673
1674fn describe_object(obj: &objc2::runtime::AnyObject) -> String {
1677 use objc2::msg_send;
1678 use objc2::rc::Retained;
1679 use objc2::runtime::AnyObject;
1680
1681 let desc: Option<Retained<AnyObject>> = unsafe { msg_send![obj, description] };
1682 match desc {
1683 None => String::from("<no description>"),
1684 Some(d) => {
1685 let any: &AnyObject = &d;
1687 let ns: &NSString = unsafe { &*(any as *const AnyObject as *const NSString) };
1688 ns.to_string()
1689 }
1690 }
1691}
1692
1693fn reminder_to_item(reminder: &EKReminder) -> ReminderItem {
1694 let identifier = unsafe { reminder.calendarItemIdentifier() }.to_string();
1695 let title = unsafe { reminder.title() }.to_string();
1696 let notes = unsafe { reminder.notes() }.map(|n| n.to_string());
1697 let completed = unsafe { reminder.isCompleted() };
1698 let priority = unsafe { reminder.priority() };
1699 let cal = unsafe { reminder.calendar() };
1700 let calendar_title = cal.as_ref().map(|c| unsafe { c.title() }.to_string());
1701 let calendar_id = cal
1702 .as_ref()
1703 .map(|c| unsafe { c.calendarIdentifier() }.to_string());
1704
1705 let due_date = unsafe { reminder.dueDateComponents() }
1707 .and_then(|components| date_components_to_datetime(&components));
1708
1709 let start_date = unsafe { reminder.startDateComponents() }
1711 .and_then(|components| date_components_to_datetime(&components));
1712
1713 let completion_date =
1715 unsafe { reminder.completionDate() }.map(|date| nsdate_to_datetime(&date));
1716
1717 let external_identifier =
1719 unsafe { reminder.calendarItemExternalIdentifier() }.map(|id| id.to_string());
1720 let location = unsafe { reminder.location() }.map(|loc| loc.to_string());
1721 #[allow(non_snake_case)]
1722 let URL = unsafe { reminder.URL() }
1723 .as_ref()
1724 .and_then(|url_ref| url_ref.absoluteString())
1725 .map(|abs_str| abs_str.to_string());
1726 let creation_date = unsafe { reminder.creationDate() }.map(|date| nsdate_to_datetime(&date));
1727 let last_modified_date =
1728 unsafe { reminder.lastModifiedDate() }.map(|date| nsdate_to_datetime(&date));
1729 let timezone = unsafe { reminder.timeZone() }.map(|tz| tz.name().to_string());
1730 let due_date_timezone = get_reminder_due_date_timezone(reminder);
1731 let structured_location = get_reminder_structured_location(reminder);
1732 let parent_id = get_reminder_parent_id(reminder);
1733 let attachments_count = get_item_attachments_count(reminder);
1734 let has_alarms = unsafe { reminder.hasAlarms() };
1735 let has_recurrence_rules = unsafe { reminder.hasRecurrenceRules() };
1736 let has_attendees = unsafe { reminder.hasAttendees() };
1737 let has_notes = unsafe { reminder.hasNotes() };
1738
1739 ReminderItem {
1740 identifier,
1741 title,
1742 notes,
1743 completed,
1744 priority,
1745 calendar_title,
1746 calendar_id,
1747 due_date,
1748 start_date,
1749 completion_date,
1750 external_identifier,
1751 location,
1752 URL,
1753 creation_date,
1754 last_modified_date,
1755 timezone,
1756 due_date_timezone,
1757 structured_location,
1758 parent_id,
1759 attachments_count,
1760 has_alarms,
1761 has_recurrence_rules,
1762 has_attendees,
1763 has_notes,
1764 attendees: get_item_attendees(reminder),
1765 }
1766}
1767
1768fn get_reminder_due_date_timezone(reminder: &EKReminder) -> Option<String> {
1771 use objc2::msg_send;
1772 use objc2::rc::Retained;
1773 let tz: Option<Retained<objc2_foundation::NSTimeZone>> =
1774 unsafe { msg_send![reminder, dueDateTimeZone] };
1775 tz.map(|tz| tz.name().to_string())
1776}
1777
1778fn get_reminder_structured_location(reminder: &EKReminder) -> Option<StructuredLocation> {
1786 if let Some(sl) = direct_structured_location(reminder) {
1787 return Some(sl);
1788 }
1789 let alarms = unsafe { reminder.alarms() }?;
1791 for i in 0..alarms.len() {
1792 let alarm = alarms.objectAtIndex(i);
1793 let prox = unsafe { alarm.proximity() };
1794 if prox == EKAlarmProximity::None {
1795 continue;
1796 }
1797 let Some(structured) = (unsafe { alarm.structuredLocation() }) else {
1798 continue;
1799 };
1800 return Some(structured_to_plain(&structured));
1801 }
1802 None
1803}
1804
1805fn direct_structured_location(reminder: &EKReminder) -> Option<StructuredLocation> {
1806 use objc2::msg_send;
1807 use objc2::rc::Retained;
1808 let sl: Option<Retained<EKStructuredLocation>> =
1809 unsafe { msg_send![reminder, structuredLocation] };
1810 sl.map(|s| structured_to_plain(&s))
1811}
1812
1813fn structured_to_plain(structured: &EKStructuredLocation) -> StructuredLocation {
1814 let title = unsafe { structured.title() }
1815 .map(|t| t.to_string())
1816 .unwrap_or_default();
1817 let radius = unsafe { structured.radius() };
1818 let (latitude, longitude) = read_geo_location_coords(structured);
1819 StructuredLocation {
1820 title,
1821 latitude,
1822 longitude,
1823 radius,
1824 }
1825}
1826
1827#[cfg(feature = "location")]
1831fn read_geo_location_coords(structured: &EKStructuredLocation) -> (f64, f64) {
1832 let cl = unsafe { structured.geoLocation() };
1833 let Some(cl) = cl else { return (0.0, 0.0) };
1834 let coord = unsafe { cl.coordinate() };
1835 (coord.latitude, coord.longitude)
1836}
1837
1838#[cfg(not(feature = "location"))]
1839fn read_geo_location_coords(_structured: &EKStructuredLocation) -> (f64, f64) {
1840 (0.0, 0.0)
1841}
1842
1843fn get_reminder_parent_id(reminder: &EKReminder) -> Option<String> {
1847 use objc2::msg_send;
1848 use objc2::rc::Retained;
1849 use objc2::runtime::AnyObject;
1850 let reminder_obj: &AnyObject = unsafe { &*(reminder as *const EKReminder as *const AnyObject) };
1851 let reminder_ptr = reminder_obj as *const AnyObject;
1852 let value = objc2::exception::catch(std::panic::AssertUnwindSafe(move || {
1853 let key = NSString::from_str("parentID");
1854 let obj_ref: &AnyObject = unsafe { &*reminder_ptr };
1855 let v: Option<Retained<AnyObject>> = unsafe { msg_send![obj_ref, valueForKey: &*key] };
1856 v
1857 }))
1858 .ok()
1859 .flatten()?;
1860 let desc: Retained<AnyObject> = unsafe { msg_send![&*value, description] };
1861 let ns: &NSString = unsafe { &*(&*desc as *const AnyObject as *const NSString) };
1862 Some(ns.to_string())
1863}
1864
1865fn get_item_attachments_count(item: &EKCalendarItem) -> usize {
1868 use objc2::msg_send;
1869 use objc2::rc::Retained;
1870 use objc2_foundation::NSArray;
1871 let attachments: Option<Retained<NSArray>> = unsafe { msg_send![item, attachments] };
1872 attachments.map(|a| a.len()).unwrap_or(0)
1873}
1874
1875fn source_to_info(source: &EKSource) -> SourceInfo {
1877 let identifier = unsafe { source.sourceIdentifier() }.to_string();
1878 let title = unsafe { source.title() }.to_string();
1879 let source_type = unsafe { source.sourceType() };
1881 let source_type = match source_type.0 {
1882 0 => "local",
1883 1 => "exchange",
1884 2 => "caldav",
1885 3 => "mobileme",
1886 4 => "subscribed",
1887 5 => "birthdays",
1888 _ => "unknown",
1889 }
1890 .to_string();
1891
1892 SourceInfo {
1893 identifier,
1894 title,
1895 source_type,
1896 }
1897}
1898
1899fn calendar_to_info(calendar: &EKCalendar) -> CalendarInfo {
1900 let identifier = unsafe { calendar.calendarIdentifier() }.to_string();
1901 let title = unsafe { calendar.title() }.to_string();
1902 let source = unsafe { calendar.source() }.map(|s| unsafe { s.title() }.to_string());
1903 let source_id =
1904 unsafe { calendar.source() }.map(|s| unsafe { s.sourceIdentifier() }.to_string());
1905 let allows_modifications = unsafe { calendar.allowsContentModifications() };
1906 let is_immutable = unsafe { calendar.isImmutable() };
1907 let is_subscribed = unsafe { calendar.isSubscribed() };
1908
1909 let cal_type = unsafe { calendar.r#type() };
1911 let calendar_type = match cal_type.0 {
1912 0 => CalendarType::Local,
1913 1 => CalendarType::CalDAV,
1914 2 => CalendarType::Exchange,
1915 3 => CalendarType::Subscription,
1916 4 => CalendarType::Birthday,
1917 _ => CalendarType::Unknown,
1918 };
1919
1920 let color: Option<(f64, f64, f64, f64)> = unsafe {
1922 calendar.CGColor().and_then(|cg| {
1923 use objc2_core_graphics::CGColor as CG;
1924 let n = CG::number_of_components(Some(&cg));
1925 if n >= 3 {
1926 let ptr = CG::components(Some(&cg));
1927 let r = *ptr;
1928 let g = *ptr.add(1);
1929 let b = *ptr.add(2);
1930 let a = if n >= 4 { *ptr.add(3) } else { 1.0 };
1931 Some((r, g, b, a))
1932 } else {
1933 None
1934 }
1935 })
1936 };
1937
1938 let entity_mask = unsafe { calendar.allowedEntityTypes() };
1940 let mut allowed_entity_types = Vec::new();
1941 if entity_mask.0 & 1 != 0 {
1942 allowed_entity_types.push("event".to_string());
1943 }
1944 if entity_mask.0 & 2 != 0 {
1945 allowed_entity_types.push("reminder".to_string());
1946 }
1947
1948 let avail_mask = unsafe { calendar.supportedEventAvailabilities() };
1952 let mut supported_event_availabilities = Vec::new();
1953 if avail_mask.contains(EKCalendarEventAvailabilityMask::Busy) {
1954 supported_event_availabilities.push("busy".to_string());
1955 }
1956 if avail_mask.contains(EKCalendarEventAvailabilityMask::Free) {
1957 supported_event_availabilities.push("free".to_string());
1958 }
1959 if avail_mask.contains(EKCalendarEventAvailabilityMask::Tentative) {
1960 supported_event_availabilities.push("tentative".to_string());
1961 }
1962 if avail_mask.contains(EKCalendarEventAvailabilityMask::Unavailable) {
1963 supported_event_availabilities.push("unavailable".to_string());
1964 }
1965
1966 CalendarInfo {
1967 identifier,
1968 title,
1969 source,
1970 source_id,
1971 calendar_type,
1972 allows_modifications,
1973 is_immutable,
1974 is_subscribed,
1975 color,
1976 allowed_entity_types,
1977 supported_event_availabilities,
1978 }
1979}
1980
1981#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1987pub enum EventAvailability {
1988 NotSupported,
1989 #[default]
1990 Busy,
1991 Free,
1992 Tentative,
1993 Unavailable,
1994}
1995
1996impl EventAvailability {
1997 pub fn to_ek(self) -> EKEventAvailability {
1999 match self {
2000 Self::NotSupported => EKEventAvailability::NotSupported,
2001 Self::Busy => EKEventAvailability::Busy,
2002 Self::Free => EKEventAvailability::Free,
2003 Self::Tentative => EKEventAvailability::Tentative,
2004 Self::Unavailable => EKEventAvailability::Unavailable,
2005 }
2006 }
2007
2008 pub fn from_ek(raw: EKEventAvailability) -> Self {
2011 match raw {
2012 EKEventAvailability::NotSupported => Self::NotSupported,
2013 EKEventAvailability::Busy => Self::Busy,
2014 EKEventAvailability::Free => Self::Free,
2015 EKEventAvailability::Tentative => Self::Tentative,
2016 EKEventAvailability::Unavailable => Self::Unavailable,
2017 _ => Self::NotSupported,
2018 }
2019 }
2020}
2021
2022#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2024pub enum EventStatus {
2025 #[default]
2026 None,
2027 Confirmed,
2028 Tentative,
2029 Canceled,
2030}
2031
2032impl EventStatus {
2033 pub fn from_ek(raw: EKEventStatus) -> Self {
2034 match raw {
2035 EKEventStatus::None => Self::None,
2036 EKEventStatus::Confirmed => Self::Confirmed,
2037 EKEventStatus::Tentative => Self::Tentative,
2038 EKEventStatus::Canceled => Self::Canceled,
2039 _ => Self::None,
2040 }
2041 }
2042}
2043
2044#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2049pub enum EventSpan {
2050 #[default]
2051 This,
2052 Future,
2053}
2054
2055impl EventSpan {
2056 pub(crate) fn to_ek(self) -> EKSpan {
2057 match self {
2058 Self::This => EKSpan::ThisEvent,
2059 Self::Future => EKSpan::FutureEvents,
2060 }
2061 }
2062}
2063
2064#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2066pub enum ParticipantRole {
2067 Unknown,
2068 Required,
2069 Optional,
2070 Chair,
2071 NonParticipant,
2072}
2073
2074#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2076pub enum ParticipantStatus {
2077 Unknown,
2078 Pending,
2079 Accepted,
2080 Declined,
2081 Tentative,
2082 Delegated,
2083 Completed,
2084 InProcess,
2085}
2086
2087#[derive(Debug, Clone)]
2089#[allow(non_snake_case)]
2090pub struct ParticipantInfo {
2091 pub name: Option<String>,
2092 pub URL: Option<String>,
2093 pub role: ParticipantRole,
2094 pub status: ParticipantStatus,
2095 pub is_current_user: bool,
2096}
2097
2098#[derive(Debug, Clone)]
2100#[allow(non_snake_case)]
2101pub struct EventItem {
2102 pub identifier: String,
2104 pub title: String,
2106 pub notes: Option<String>,
2108 pub location: Option<String>,
2110 pub start_date: DateTime<Local>,
2112 pub end_date: DateTime<Local>,
2114 pub all_day: bool,
2116 pub calendar_title: Option<String>,
2118 pub calendar_id: Option<String>,
2120 #[allow(non_snake_case)]
2122 pub URL: Option<String>,
2123 pub availability: EventAvailability,
2125 pub status: EventStatus,
2127 pub is_detached: bool,
2129 pub occurrence_date: Option<DateTime<Local>>,
2131 pub structured_location: Option<StructuredLocation>,
2133 pub creation_date: Option<DateTime<Local>>,
2135 pub last_modified_date: Option<DateTime<Local>>,
2137 pub external_identifier: Option<String>,
2139 pub timezone: Option<String>,
2142 pub attachments_count: usize,
2144 pub attendees: Vec<ParticipantInfo>,
2146 pub organizer: Option<ParticipantInfo>,
2148}
2149
2150#[derive(Debug, Clone, Default)]
2154#[allow(non_snake_case)]
2155pub struct EventDraft<'a> {
2156 pub title: &'a str,
2157 pub start: Option<DateTime<Local>>,
2158 pub end: Option<DateTime<Local>>,
2159 pub notes: Option<&'a str>,
2160 pub location: Option<&'a str>,
2161 pub calendar_title: Option<&'a str>,
2162 pub all_day: bool,
2163 pub URL: Option<&'a str>,
2164 pub availability: Option<EventAvailability>,
2165 pub structured_location: Option<&'a StructuredLocation>,
2166}
2167
2168#[derive(Debug, Clone, Default)]
2174#[allow(non_snake_case)]
2175pub struct EventPatch<'a> {
2176 pub title: Option<&'a str>,
2177 pub notes: Option<Option<&'a str>>,
2178 pub location: Option<Option<&'a str>>,
2179 pub start: Option<DateTime<Local>>,
2180 pub end: Option<DateTime<Local>>,
2181 pub all_day: Option<bool>,
2182 pub calendar_title: Option<&'a str>,
2184 pub URL: Option<Option<&'a str>>,
2185 pub availability: Option<EventAvailability>,
2186 pub structured_location: Option<Option<&'a StructuredLocation>>,
2187 pub span: EventSpan,
2189}
2190
2191pub struct EventsManager {
2193 store: Retained<EKEventStore>,
2194}
2195
2196impl EventsManager {
2197 pub fn new() -> Self {
2199 let store = unsafe { EKEventStore::new() };
2200 Self { store }
2201 }
2202
2203 pub fn authorization_status() -> AuthorizationStatus {
2205 let status = unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Event) };
2206 status.into()
2207 }
2208
2209 pub fn request_access(&self) -> Result<bool> {
2213 let result = Arc::new((Mutex::new(None::<(bool, Option<String>)>), Condvar::new()));
2214 let result_clone = Arc::clone(&result);
2215
2216 let completion = RcBlock::new(move |granted: Bool, error: *mut NSError| {
2217 let error_msg = if !error.is_null() {
2218 let error_ref = unsafe { &*error };
2219 Some(format!("{:?}", error_ref))
2220 } else {
2221 None
2222 };
2223
2224 let (lock, cvar) = &*result_clone;
2225 let mut res = lock.lock().unwrap();
2226 *res = Some((granted.as_bool(), error_msg));
2227 cvar.notify_one();
2228 });
2229
2230 unsafe {
2231 let block_ptr = &*completion as *const _ as *mut _;
2232 self.store
2233 .requestFullAccessToEventsWithCompletion(block_ptr);
2234 }
2235
2236 let (lock, cvar) = &*result;
2237 let mut res = lock.lock().unwrap();
2238 while res.is_none() {
2239 res = cvar.wait(res).unwrap();
2240 }
2241
2242 match res.take() {
2243 Some((granted, None)) => Ok(granted),
2244 Some((_, Some(error))) => Err(EventKitError::AuthorizationRequestFailed(error)),
2245 None => Err(EventKitError::AuthorizationRequestFailed(
2246 "Unknown error".to_string(),
2247 )),
2248 }
2249 }
2250
2251 pub fn ensure_authorized(&self) -> Result<()> {
2253 match Self::authorization_status() {
2254 AuthorizationStatus::FullAccess => Ok(()),
2255 AuthorizationStatus::NotDetermined => {
2256 if self.request_access()? {
2257 Ok(())
2258 } else {
2259 Err(EventKitError::AuthorizationDenied)
2260 }
2261 }
2262 AuthorizationStatus::Denied => Err(EventKitError::AuthorizationDenied),
2263 AuthorizationStatus::Restricted => Err(EventKitError::AuthorizationRestricted),
2264 AuthorizationStatus::WriteOnly => Ok(()),
2265 }
2266 }
2267
2268 pub fn list_calendars(&self) -> Result<Vec<CalendarInfo>> {
2270 self.ensure_authorized()?;
2271
2272 let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
2273
2274 let mut result = Vec::new();
2275 for calendar in calendars.iter() {
2276 result.push(calendar_to_info(&calendar));
2277 }
2278
2279 Ok(result)
2280 }
2281
2282 pub fn default_calendar(&self) -> Result<CalendarInfo> {
2284 self.ensure_authorized()?;
2285
2286 let calendar = unsafe { self.store.defaultCalendarForNewEvents() };
2287
2288 match calendar {
2289 Some(cal) => Ok(calendar_to_info(&cal)),
2290 None => Err(EventKitError::NoDefaultCalendar),
2291 }
2292 }
2293
2294 pub fn fetch_today_events(&self) -> Result<Vec<EventItem>> {
2296 let now = Local::now();
2297 let start = now.date_naive().and_hms_opt(0, 0, 0).unwrap();
2298 let end = now.date_naive().and_hms_opt(23, 59, 59).unwrap();
2299
2300 self.fetch_events(
2301 Local.from_local_datetime(&start).unwrap(),
2302 Local.from_local_datetime(&end).unwrap(),
2303 None,
2304 )
2305 }
2306
2307 pub fn fetch_upcoming_events(&self, days: i64) -> Result<Vec<EventItem>> {
2309 let now = Local::now();
2310 let end = now + Duration::days(days);
2311 self.fetch_events(now, end, None)
2312 }
2313
2314 pub fn fetch_events(
2316 &self,
2317 start: DateTime<Local>,
2318 end: DateTime<Local>,
2319 calendar_titles: Option<&[&str]>,
2320 ) -> Result<Vec<EventItem>> {
2321 self.ensure_authorized()?;
2322
2323 if start >= end {
2324 return Err(EventKitError::InvalidDateRange);
2325 }
2326
2327 let calendars: Option<Retained<NSArray<EKCalendar>>> = match calendar_titles {
2328 Some(titles) => {
2329 let all_calendars =
2330 unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
2331 let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
2332
2333 for cal in all_calendars.iter() {
2334 let title = unsafe { cal.title() };
2335 let title_str = title.to_string();
2336 if titles.iter().any(|t| *t == title_str) {
2337 matching.push(cal.retain());
2338 }
2339 }
2340
2341 if matching.is_empty() {
2342 return Err(EventKitError::CalendarNotFound(titles.join(", ")));
2343 }
2344
2345 Some(NSArray::from_retained_slice(&matching))
2346 }
2347 None => None,
2348 };
2349
2350 let start_date = datetime_to_nsdate(start);
2351 let end_date = datetime_to_nsdate(end);
2352
2353 let predicate = unsafe {
2354 self.store
2355 .predicateForEventsWithStartDate_endDate_calendars(
2356 &start_date,
2357 &end_date,
2358 calendars.as_deref(),
2359 )
2360 };
2361
2362 let events = unsafe { self.store.eventsMatchingPredicate(&predicate) };
2363
2364 let mut items = Vec::new();
2365 for event in events.iter() {
2366 items.push(event_to_item(&event));
2367 }
2368
2369 items.sort_by_key(|a| a.start_date);
2371
2372 Ok(items)
2373 }
2374
2375 pub fn create_event(&self, draft: &EventDraft<'_>) -> Result<EventItem> {
2379 self.ensure_authorized()?;
2380
2381 let start = draft.start.ok_or(EventKitError::InvalidDateRange)?;
2382 let end = draft.end.ok_or(EventKitError::InvalidDateRange)?;
2383
2384 let event = unsafe { EKEvent::eventWithEventStore(&self.store) };
2385
2386 let ns_title = NSString::from_str(draft.title);
2387 unsafe { event.setTitle(Some(&ns_title)) };
2388
2389 let start_date = datetime_to_nsdate(start);
2390 let end_date = datetime_to_nsdate(end);
2391 unsafe {
2392 event.setStartDate(Some(&start_date));
2393 event.setEndDate(Some(&end_date));
2394 event.setAllDay(draft.all_day);
2395 }
2396
2397 if let Some(notes_text) = draft.notes {
2398 let ns_notes = NSString::from_str(notes_text);
2399 unsafe { event.setNotes(Some(&ns_notes)) };
2400 }
2401
2402 if let Some(loc) = draft.location {
2403 let ns_location = NSString::from_str(loc);
2404 unsafe { event.setLocation(Some(&ns_location)) };
2405 }
2406
2407 if draft.URL.is_some() {
2408 set_item_URL(&event, draft.URL)?;
2409 }
2410
2411 if let Some(av) = draft.availability {
2412 unsafe { event.setAvailability(av.to_ek()) };
2413 }
2414
2415 if let Some(sl) = draft.structured_location {
2416 let built = build_ek_structured_location(sl);
2417 unsafe { event.setStructuredLocation(Some(&built)) };
2418 }
2419
2420 let calendar = if let Some(cal_title) = draft.calendar_title {
2421 self.find_calendar_by_title(cal_title)?
2422 } else {
2423 unsafe { self.store.defaultCalendarForNewEvents() }
2424 .ok_or(EventKitError::NoDefaultCalendar)?
2425 };
2426 unsafe { event.setCalendar(Some(&calendar)) };
2427
2428 self.save_event_and_refresh(&event, EKSpan::ThisEvent)?;
2429
2430 Ok(event_to_item(&event))
2431 }
2432
2433 pub fn update_event(&self, identifier: &str, patch: &EventPatch<'_>) -> Result<EventItem> {
2440 self.ensure_authorized()?;
2441
2442 let event = self.find_event_by_id(identifier)?;
2443
2444 if let Some(t) = patch.title {
2445 let ns_title = NSString::from_str(t);
2446 unsafe { event.setTitle(Some(&ns_title)) };
2447 }
2448
2449 if let Some(notes_opt) = patch.notes {
2450 let ns = notes_opt.map(NSString::from_str);
2451 unsafe { event.setNotes(ns.as_deref()) };
2452 }
2453
2454 if let Some(loc_opt) = patch.location {
2455 let ns = loc_opt.map(NSString::from_str);
2456 unsafe { event.setLocation(ns.as_deref()) };
2457 }
2458
2459 if let Some(s) = patch.start {
2460 let start_date = datetime_to_nsdate(s);
2461 unsafe { event.setStartDate(Some(&start_date)) };
2462 }
2463
2464 if let Some(e) = patch.end {
2465 let end_date = datetime_to_nsdate(e);
2466 unsafe { event.setEndDate(Some(&end_date)) };
2467 }
2468
2469 if let Some(ad) = patch.all_day {
2470 unsafe { event.setAllDay(ad) };
2471 }
2472
2473 if let Some(url_opt) = patch.URL {
2474 set_item_URL(&event, url_opt)?;
2475 }
2476
2477 if let Some(av) = patch.availability {
2478 unsafe { event.setAvailability(av.to_ek()) };
2479 }
2480
2481 if let Some(sl_opt) = patch.structured_location {
2482 let built = sl_opt.map(build_ek_structured_location);
2483 unsafe { event.setStructuredLocation(built.as_deref()) };
2484 }
2485
2486 if let Some(cal_title) = patch.calendar_title {
2487 let calendar = self.find_calendar_by_title(cal_title)?;
2488 unsafe { event.setCalendar(Some(&calendar)) };
2489 }
2490
2491 self.save_event_and_refresh(&event, patch.span.to_ek())?;
2492
2493 Ok(event_to_item(&event))
2494 }
2495
2496 pub fn delete_event(&self, identifier: &str, affect_future: bool) -> Result<()> {
2498 self.ensure_authorized()?;
2499
2500 let event = self.find_event_by_id(identifier)?;
2501 let span = if affect_future {
2502 EKSpan::FutureEvents
2503 } else {
2504 EKSpan::ThisEvent
2505 };
2506
2507 unsafe {
2508 self.store
2509 .removeEvent_span_commit_error(&event, span, true)
2510 .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
2511 }
2512
2513 Ok(())
2514 }
2515
2516 pub fn get_event(&self, identifier: &str) -> Result<EventItem> {
2518 self.ensure_authorized()?;
2519 let event = self.find_event_by_id(identifier)?;
2520 Ok(event_to_item(&event))
2521 }
2522
2523 pub fn create_event_calendar(&self, title: &str) -> Result<CalendarInfo> {
2529 self.ensure_authorized()?;
2530 let calendar = unsafe {
2531 EKCalendar::calendarForEntityType_eventStore(EKEntityType::Event, &self.store)
2532 };
2533 let ns_title = NSString::from_str(title);
2534 unsafe { calendar.setTitle(&ns_title) };
2535
2536 if let Some(default_cal) = unsafe { self.store.defaultCalendarForNewEvents() }
2538 && let Some(source) = unsafe { default_cal.source() }
2539 {
2540 unsafe { calendar.setSource(Some(&source)) };
2541 }
2542
2543 unsafe {
2544 self.store
2545 .saveCalendar_commit_error(&calendar, true)
2546 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
2547 }
2548 Ok(calendar_to_info(&calendar))
2549 }
2550
2551 pub fn rename_event_calendar(&self, identifier: &str, new_title: &str) -> Result<CalendarInfo> {
2554 self.update_event_calendar(identifier, Some(new_title), None)
2555 }
2556
2557 pub fn update_event_calendar(
2559 &self,
2560 identifier: &str,
2561 new_title: Option<&str>,
2562 color_rgba: Option<(f64, f64, f64, f64)>,
2563 ) -> Result<CalendarInfo> {
2564 self.ensure_authorized()?;
2565 let calendar = unsafe {
2566 self.store
2567 .calendarWithIdentifier(&NSString::from_str(identifier))
2568 }
2569 .ok_or_else(|| EventKitError::CalendarNotFound(identifier.to_string()))?;
2570
2571 if let Some(title) = new_title {
2572 let ns_title = NSString::from_str(title);
2573 unsafe { calendar.setTitle(&ns_title) };
2574 }
2575
2576 if let Some((r, g, b, a)) = color_rgba {
2577 let cg = objc2_core_graphics::CGColor::new_srgb(r, g, b, a);
2578 unsafe { calendar.setCGColor(Some(&cg)) };
2579 }
2580
2581 unsafe {
2582 self.store
2583 .saveCalendar_commit_error(&calendar, true)
2584 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
2585 }
2586 Ok(calendar_to_info(&calendar))
2587 }
2588
2589 pub fn delete_event_calendar(&self, identifier: &str) -> Result<()> {
2591 self.ensure_authorized()?;
2592 let calendar = unsafe {
2593 self.store
2594 .calendarWithIdentifier(&NSString::from_str(identifier))
2595 }
2596 .ok_or_else(|| EventKitError::CalendarNotFound(identifier.to_string()))?;
2597
2598 unsafe {
2599 self.store
2600 .removeCalendar_commit_error(&calendar, true)
2601 .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
2602 }
2603 Ok(())
2604 }
2605
2606 pub fn get_event_alarms(&self, identifier: &str) -> Result<Vec<AlarmInfo>> {
2612 self.ensure_authorized()?;
2613 let event = self.find_event_by_id(identifier)?;
2614 Ok(get_item_alarms(&event))
2615 }
2616
2617 pub fn add_event_alarm(&self, identifier: &str, alarm: &AlarmInfo) -> Result<()> {
2619 self.ensure_authorized()?;
2620 let event = self.find_event_by_id(identifier)?;
2621 add_item_alarm(&event, alarm)?;
2622 self.save_event_and_refresh(&event, EKSpan::ThisEvent)?;
2623 Ok(())
2624 }
2625
2626 pub fn get_event_recurrence_rules(&self, identifier: &str) -> Result<Vec<RecurrenceRule>> {
2632 self.ensure_authorized()?;
2633 let event = self.find_event_by_id(identifier)?;
2634 Ok(get_item_recurrence_rules(&event))
2635 }
2636
2637 pub fn set_event_recurrence_rule(&self, identifier: &str, rule: &RecurrenceRule) -> Result<()> {
2639 self.ensure_authorized()?;
2640 let event = self.find_event_by_id(identifier)?;
2641 set_item_recurrence_rule(&event, rule);
2642 self.save_event_and_refresh(&event, EKSpan::ThisEvent)?;
2643 Ok(())
2644 }
2645
2646 pub fn remove_event_recurrence_rules(&self, identifier: &str) -> Result<()> {
2648 self.ensure_authorized()?;
2649 let event = self.find_event_by_id(identifier)?;
2650 clear_item_recurrence_rules(&event);
2651 self.save_event_and_refresh(&event, EKSpan::ThisEvent)?;
2652 Ok(())
2653 }
2654
2655 pub fn remove_event_alarm(&self, identifier: &str, index: usize) -> Result<()> {
2657 self.ensure_authorized()?;
2658 let event = self.find_event_by_id(identifier)?;
2659 remove_item_alarm(&event, index)?;
2660 self.save_event_and_refresh(&event, EKSpan::ThisEvent)?;
2661 Ok(())
2662 }
2663
2664 #[allow(non_snake_case)]
2666 pub fn set_event_URL(&self, identifier: &str, url: Option<&str>) -> Result<()> {
2667 self.ensure_authorized()?;
2668 let event = self.find_event_by_id(identifier)?;
2669 set_item_URL(&event, url)?;
2670 self.save_event_and_refresh(&event, EKSpan::ThisEvent)?;
2671 Ok(())
2672 }
2673
2674 pub fn set_event_availability(
2678 &self,
2679 identifier: &str,
2680 availability: EventAvailability,
2681 ) -> Result<()> {
2682 self.ensure_authorized()?;
2683 let event = self.find_event_by_id(identifier)?;
2684 unsafe { event.setAvailability(availability.to_ek()) };
2685 self.save_event_and_refresh(&event, EKSpan::ThisEvent)
2686 }
2687
2688 fn find_calendar_by_title(&self, title: &str) -> Result<Retained<EKCalendar>> {
2690 let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
2691
2692 for cal in calendars.iter() {
2693 let cal_title = unsafe { cal.title() };
2694 if cal_title.to_string() == title {
2695 return Ok(cal.retain());
2696 }
2697 }
2698
2699 Err(EventKitError::CalendarNotFound(title.to_string()))
2700 }
2701
2702 fn find_event_by_id(&self, identifier: &str) -> Result<Retained<EKEvent>> {
2705 unsafe { self.store.refreshSourcesIfNecessary() };
2706 let ns_id = NSString::from_str(identifier);
2707 let event = unsafe { self.store.eventWithIdentifier(&ns_id) };
2708
2709 match event {
2710 Some(e) => Ok(e),
2711 None => Err(EventKitError::ItemNotFound(identifier.to_string())),
2712 }
2713 }
2714
2715 fn save_event_and_refresh(&self, event: &EKEvent, span: EKSpan) -> Result<()> {
2719 unsafe {
2720 self.store
2721 .saveEvent_span_commit_error(event, span, true)
2722 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
2723 self.store.refreshSourcesIfNecessary();
2724 }
2725 Ok(())
2726 }
2727}
2728
2729impl Default for EventsManager {
2730 fn default() -> Self {
2731 Self::new()
2732 }
2733}
2734
2735fn event_to_item(event: &EKEvent) -> EventItem {
2737 let identifier = unsafe { event.eventIdentifier() }
2738 .map(|s| s.to_string())
2739 .unwrap_or_default();
2740 let title = unsafe { event.title() }.to_string();
2741 let notes = unsafe { event.notes() }.map(|n| n.to_string());
2742 let location = unsafe { event.location() }.map(|l| l.to_string());
2743 let all_day = unsafe { event.isAllDay() };
2744 let cal = unsafe { event.calendar() };
2745 let calendar_title = cal.as_ref().map(|c| unsafe { c.title() }.to_string());
2746 let calendar_id = cal
2747 .as_ref()
2748 .map(|c| unsafe { c.calendarIdentifier() }.to_string());
2749
2750 let start_ns: Retained<NSDate> = unsafe { event.startDate() };
2751 let end_ns: Retained<NSDate> = unsafe { event.endDate() };
2752 let start_date = nsdate_to_datetime(&start_ns);
2753 let end_date = nsdate_to_datetime(&end_ns);
2754
2755 #[allow(non_snake_case)]
2756 let URL = get_item_URL(event);
2757
2758 let availability = EventAvailability::from_ek(unsafe { event.availability() });
2759 let status = EventStatus::from_ek(unsafe { event.status() });
2760
2761 let is_detached = unsafe { event.isDetached() };
2762 let occurrence_date = unsafe { event.occurrenceDate() }.map(|d| nsdate_to_datetime(&d));
2763
2764 let structured_location = unsafe { event.structuredLocation() }.map(|loc| {
2766 let title = unsafe { loc.title() }
2767 .map(|t| t.to_string())
2768 .unwrap_or_default();
2769 let radius = unsafe { loc.radius() };
2770 let (latitude, longitude) = unsafe { loc.geoLocation() }
2771 .map(|geo| {
2772 let coord = unsafe { geo.coordinate() };
2773 (coord.latitude, coord.longitude)
2774 })
2775 .unwrap_or((0.0, 0.0));
2776 StructuredLocation {
2777 title,
2778 latitude,
2779 longitude,
2780 radius,
2781 }
2782 });
2783
2784 let attendees = get_item_attendees(event);
2786
2787 let organizer = unsafe { event.organizer() }.map(|p| participant_to_info(&p));
2789
2790 let creation_date = unsafe { event.creationDate() }.map(|d| nsdate_to_datetime(&d));
2792 let last_modified_date = unsafe { event.lastModifiedDate() }.map(|d| nsdate_to_datetime(&d));
2793 let external_identifier =
2794 unsafe { event.calendarItemExternalIdentifier() }.map(|s| s.to_string());
2795 let timezone = unsafe { event.timeZone() }.map(|tz| tz.name().to_string());
2796 let attachments_count = get_item_attachments_count(event);
2797
2798 EventItem {
2799 identifier,
2800 title,
2801 notes,
2802 location,
2803 start_date,
2804 end_date,
2805 all_day,
2806 calendar_title,
2807 calendar_id,
2808 URL,
2809 availability,
2810 status,
2811 is_detached,
2812 occurrence_date,
2813 structured_location,
2814 creation_date,
2815 last_modified_date,
2816 external_identifier,
2817 timezone,
2818 attachments_count,
2819 attendees,
2820 organizer,
2821 }
2822}
2823
2824fn get_item_attendees(item: &EKCalendarItem) -> Vec<ParticipantInfo> {
2826 let attendees = unsafe { item.attendees() };
2827 let Some(attendees) = attendees else {
2828 return Vec::new();
2829 };
2830 let mut result = Vec::new();
2831 for i in 0..attendees.len() {
2832 let p = attendees.objectAtIndex(i);
2833 result.push(participant_to_info(&p));
2834 }
2835 result
2836}
2837
2838fn participant_to_info(p: &objc2_event_kit::EKParticipant) -> ParticipantInfo {
2840 let name = unsafe { p.name() }.map(|n| n.to_string());
2841 #[allow(non_snake_case)]
2842 let URL = unsafe { p.URL() }.absoluteString().map(|s| s.to_string());
2843
2844 let role = unsafe { p.participantRole() };
2846 let role = match role.0 {
2847 1 => ParticipantRole::Required,
2848 2 => ParticipantRole::Optional,
2849 3 => ParticipantRole::Chair,
2850 4 => ParticipantRole::NonParticipant,
2851 _ => ParticipantRole::Unknown,
2852 };
2853
2854 let status = unsafe { p.participantStatus() };
2857 let status = match status.0 {
2858 1 => ParticipantStatus::Pending,
2859 2 => ParticipantStatus::Accepted,
2860 3 => ParticipantStatus::Declined,
2861 4 => ParticipantStatus::Tentative,
2862 5 => ParticipantStatus::Delegated,
2863 6 => ParticipantStatus::Completed,
2864 7 => ParticipantStatus::InProcess,
2865 _ => ParticipantStatus::Unknown,
2866 };
2867
2868 let is_current_user = unsafe { p.isCurrentUser() };
2869
2870 ParticipantInfo {
2871 name,
2872 URL,
2873 role,
2874 status,
2875 is_current_user,
2876 }
2877}
2878
2879fn datetime_to_nsdate(dt: DateTime<Local>) -> Retained<NSDate> {
2881 let timestamp = dt.timestamp() as f64;
2882 NSDate::dateWithTimeIntervalSince1970(timestamp)
2883}
2884
2885fn nsdate_to_datetime(date: &NSDate) -> DateTime<Local> {
2887 let timestamp = date.timeIntervalSince1970();
2888 Local.timestamp_opt(timestamp as i64, 0).unwrap()
2889}
2890
2891fn date_components_to_datetime(components: &NSDateComponents) -> Option<DateTime<Local>> {
2893 let calendar = NSCalendar::currentCalendar();
2895
2896 let date = calendar.dateFromComponents(components)?;
2898
2899 Some(nsdate_to_datetime(&date))
2900}
2901
2902fn datetime_to_date_components(dt: DateTime<Local>) -> Retained<NSDateComponents> {
2904 let components = NSDateComponents::new();
2905
2906 components.setYear(dt.year() as isize);
2907 components.setMonth(dt.month() as isize);
2908 components.setDay(dt.day() as isize);
2909 components.setHour(dt.hour() as isize);
2910 components.setMinute(dt.minute() as isize);
2911 components.setSecond(dt.second() as isize);
2912
2913 components
2914}
2915
2916fn get_item_alarms(item: &EKCalendarItem) -> Vec<AlarmInfo> {
2924 let alarms = unsafe { item.alarms() };
2925 let Some(alarms) = alarms else {
2926 return Vec::new();
2927 };
2928 let mut result = Vec::new();
2929 for i in 0..alarms.len() {
2930 let alarm = alarms.objectAtIndex(i);
2931 result.push(alarm_to_info(&alarm));
2932 }
2933 result
2934}
2935
2936fn add_item_alarm(item: &EKCalendarItem, alarm: &AlarmInfo) -> Result<()> {
2938 let ek_alarm = create_ek_alarm(alarm)?;
2939 unsafe { item.addAlarm(&ek_alarm) };
2940 Ok(())
2941}
2942
2943fn remove_item_alarm(item: &EKCalendarItem, index: usize) -> Result<()> {
2945 let alarms = unsafe { item.alarms() };
2946 let Some(alarms) = alarms else {
2947 return Err(EventKitError::ItemNotFound("No alarms on this item".into()));
2948 };
2949 if index >= alarms.len() {
2950 return Err(EventKitError::ItemNotFound(format!(
2951 "Alarm index {} out of range ({})",
2952 index,
2953 alarms.len()
2954 )));
2955 }
2956 let alarm = alarms.objectAtIndex(index);
2957 unsafe { item.removeAlarm(&alarm) };
2958 Ok(())
2959}
2960
2961fn clear_item_alarms(item: &EKCalendarItem) {
2963 unsafe { item.setAlarms(None) };
2964}
2965
2966fn get_item_recurrence_rules(item: &EKCalendarItem) -> Vec<RecurrenceRule> {
2968 let rules = unsafe { item.recurrenceRules() };
2969 let Some(rules) = rules else {
2970 return Vec::new();
2971 };
2972 let mut result = Vec::new();
2973 for i in 0..rules.len() {
2974 let rule = rules.objectAtIndex(i);
2975 result.push(recurrence_rule_to_info(&rule));
2976 }
2977 result
2978}
2979
2980fn set_item_recurrence_rule(item: &EKCalendarItem, rule: &RecurrenceRule) {
2982 let ek_rule = create_ek_recurrence_rule(rule);
2983 unsafe {
2984 let rules = NSArray::from_retained_slice(&[ek_rule]);
2985 item.setRecurrenceRules(Some(&rules));
2986 }
2987}
2988
2989fn clear_item_recurrence_rules(item: &EKCalendarItem) {
2991 unsafe { item.setRecurrenceRules(None) };
2992}
2993
2994#[allow(non_snake_case)]
3000fn set_item_URL(item: &EKCalendarItem, url: Option<&str>) -> Result<()> {
3001 let ns_url = match url {
3002 None => None,
3003 Some(s) => {
3004 let ns_str = NSString::from_str(s);
3005 let parsed =
3006 objc2_foundation::NSURL::URLWithString_encodingInvalidCharacters(&ns_str, false);
3007 match parsed {
3008 Some(u) => Some(u),
3009 None => return Err(EventKitError::InvalidURL(s.to_string())),
3010 }
3011 }
3012 };
3013 unsafe { item.setURL(ns_url.as_deref()) };
3014 Ok(())
3015}
3016
3017#[allow(non_snake_case)]
3019fn get_item_URL(item: &EKCalendarItem) -> Option<String> {
3020 unsafe { item.URL() }.map(|u| u.absoluteString().unwrap().to_string())
3021}
3022
3023fn set_item_location(item: &EKCalendarItem, location: Option<&str>) {
3032 match location {
3033 Some(text) => {
3034 let ns_loc = NSString::from_str(text);
3035 unsafe { item.setLocation(Some(&ns_loc)) };
3036 }
3037 None => unsafe { item.setLocation(None) },
3038 }
3039}
3040
3041fn set_reminder_due_date_timezone(reminder: &EKReminder, tz_name: Option<&str>) {
3050 use objc2_foundation::NSTimeZone;
3051 let Some(components) = (unsafe { reminder.dueDateComponents() }) else {
3052 return;
3053 };
3054 let tz = tz_name.and_then(|n| {
3055 let ns_n = NSString::from_str(n);
3056 NSTimeZone::timeZoneWithName(&ns_n)
3057 });
3058 components.setTimeZone(tz.as_deref());
3059 unsafe { reminder.setDueDateComponents(Some(&components)) };
3060}
3061
3062fn set_reminder_structured_location(reminder: &EKReminder, loc: Option<&StructuredLocation>) {
3070 use objc2::msg_send;
3071 let ek = loc.map(build_ek_structured_location);
3072 let _: () =
3079 unsafe { msg_send![reminder, setStructuredLocationWithoutPrediction: ek.as_deref()] };
3080 let _: () = unsafe { msg_send![reminder, setStructuredLocation: ek.as_deref()] };
3081}
3082
3083fn build_ek_structured_location(loc: &StructuredLocation) -> Retained<EKStructuredLocation> {
3086 let title = NSString::from_str(&loc.title);
3087 let structured = unsafe { EKStructuredLocation::locationWithTitle(&title) };
3088 unsafe { structured.setRadius(loc.radius) };
3089 #[cfg(feature = "location")]
3090 {
3091 use objc2::AnyThread;
3092 use objc2_core_location::CLLocation;
3093 let cl = unsafe {
3094 CLLocation::initWithLatitude_longitude(CLLocation::alloc(), loc.latitude, loc.longitude)
3095 };
3096 unsafe { structured.setGeoLocation(Some(&cl)) };
3097 }
3098 structured
3099}
3100
3101fn set_reminder_geofence(
3106 reminder: &EKReminder,
3107 geofence: Option<(&StructuredLocation, AlarmProximity)>,
3108) {
3109 if let Some(alarms) = unsafe { reminder.alarms() } {
3111 for i in (0..alarms.len()).rev() {
3112 let alarm = alarms.objectAtIndex(i);
3113 if unsafe { alarm.proximity() } != EKAlarmProximity::None {
3114 unsafe { reminder.removeAlarm(&alarm) };
3115 }
3116 }
3117 }
3118
3119 let Some((loc, proximity)) = geofence else {
3120 return;
3121 };
3122
3123 let info = AlarmInfo {
3124 relative_offset: Some(0.0),
3125 proximity,
3126 location: Some(loc.clone()),
3127 ..Default::default()
3128 };
3129 let alarm = create_ek_alarm(&info).expect("geofence alarm has no URL to parse");
3131 unsafe { reminder.addAlarm(&alarm) };
3132}
3133
3134fn recurrence_rule_to_info(rule: &EKRecurrenceRule) -> RecurrenceRule {
3140 let frequency = unsafe { rule.frequency() };
3141 let frequency = match frequency {
3142 EKRecurrenceFrequency::Daily => RecurrenceFrequency::Daily,
3143 EKRecurrenceFrequency::Weekly => RecurrenceFrequency::Weekly,
3144 EKRecurrenceFrequency::Monthly => RecurrenceFrequency::Monthly,
3145 EKRecurrenceFrequency::Yearly => RecurrenceFrequency::Yearly,
3146 _ => RecurrenceFrequency::Daily,
3147 };
3148
3149 let interval = unsafe { rule.interval() } as usize;
3150
3151 let end = unsafe { rule.recurrenceEnd() }
3152 .map(|end| {
3153 let count = unsafe { end.occurrenceCount() };
3154 if count > 0 {
3155 RecurrenceEndCondition::AfterCount(count)
3156 } else if let Some(date) = unsafe { end.endDate() } {
3157 RecurrenceEndCondition::OnDate(nsdate_to_datetime(&date))
3158 } else {
3159 RecurrenceEndCondition::Never
3160 }
3161 })
3162 .unwrap_or(RecurrenceEndCondition::Never);
3163
3164 let days_of_week = unsafe { rule.daysOfTheWeek() }.map(|days| {
3165 let mut result = Vec::new();
3166 for i in 0..days.len() {
3167 let day = days.objectAtIndex(i);
3168 let weekday = unsafe { day.dayOfTheWeek() };
3169 result.push(weekday.0 as u8);
3170 }
3171 result
3172 });
3173
3174 fn nsnumber_array_to_vec(arr: Option<Retained<NSArray<NSNumber>>>) -> Option<Vec<i32>> {
3175 arr.map(|days| {
3176 let mut result = Vec::with_capacity(days.len());
3177 for i in 0..days.len() {
3178 result.push(days.objectAtIndex(i).intValue());
3179 }
3180 result
3181 })
3182 }
3183
3184 let days_of_month = nsnumber_array_to_vec(unsafe { rule.daysOfTheMonth() });
3185 let months_of_year = nsnumber_array_to_vec(unsafe { rule.monthsOfTheYear() });
3186 let weeks_of_year = nsnumber_array_to_vec(unsafe { rule.weeksOfTheYear() });
3187 let days_of_year = nsnumber_array_to_vec(unsafe { rule.daysOfTheYear() });
3188 let set_positions = nsnumber_array_to_vec(unsafe { rule.setPositions() });
3189
3190 RecurrenceRule {
3191 frequency,
3192 interval,
3193 end,
3194 days_of_week,
3195 days_of_month,
3196 months_of_year,
3197 weeks_of_year,
3198 days_of_year,
3199 set_positions,
3200 }
3201}
3202
3203fn create_ek_recurrence_rule(rule: &RecurrenceRule) -> Retained<EKRecurrenceRule> {
3205 let frequency = match rule.frequency {
3206 RecurrenceFrequency::Daily => EKRecurrenceFrequency::Daily,
3207 RecurrenceFrequency::Weekly => EKRecurrenceFrequency::Weekly,
3208 RecurrenceFrequency::Monthly => EKRecurrenceFrequency::Monthly,
3209 RecurrenceFrequency::Yearly => EKRecurrenceFrequency::Yearly,
3210 };
3211
3212 let end = match &rule.end {
3213 RecurrenceEndCondition::Never => None,
3214 RecurrenceEndCondition::AfterCount(count) => {
3215 Some(unsafe { EKRecurrenceEnd::recurrenceEndWithOccurrenceCount(*count) })
3216 }
3217 RecurrenceEndCondition::OnDate(date) => {
3218 let nsdate = datetime_to_nsdate(*date);
3219 Some(unsafe { EKRecurrenceEnd::recurrenceEndWithEndDate(&nsdate) })
3220 }
3221 };
3222
3223 let days_of_week: Option<Vec<Retained<EKRecurrenceDayOfWeek>>> =
3224 rule.days_of_week.as_ref().map(|days| {
3225 days.iter()
3226 .map(|&d| {
3227 let weekday = EKWeekday(d as isize);
3228 unsafe { EKRecurrenceDayOfWeek::dayOfWeek(weekday) }
3229 })
3230 .collect()
3231 });
3232
3233 fn vec_to_nsnumber_array(v: Option<&Vec<i32>>) -> Option<Vec<Retained<NSNumber>>> {
3234 v.map(|nums| nums.iter().map(|&n| NSNumber::new_i32(n)).collect())
3235 }
3236 let days_of_month = vec_to_nsnumber_array(rule.days_of_month.as_ref());
3237 let months_of_year = vec_to_nsnumber_array(rule.months_of_year.as_ref());
3238 let weeks_of_year = vec_to_nsnumber_array(rule.weeks_of_year.as_ref());
3239 let days_of_year = vec_to_nsnumber_array(rule.days_of_year.as_ref());
3240 let set_positions = vec_to_nsnumber_array(rule.set_positions.as_ref());
3241
3242 let days_of_week_arr = days_of_week
3243 .as_ref()
3244 .map(|v| NSArray::from_retained_slice(v));
3245 let days_of_month_arr = days_of_month
3246 .as_ref()
3247 .map(|v| NSArray::from_retained_slice(v));
3248 let months_of_year_arr = months_of_year
3249 .as_ref()
3250 .map(|v| NSArray::from_retained_slice(v));
3251 let weeks_of_year_arr = weeks_of_year
3252 .as_ref()
3253 .map(|v| NSArray::from_retained_slice(v));
3254 let days_of_year_arr = days_of_year
3255 .as_ref()
3256 .map(|v| NSArray::from_retained_slice(v));
3257 let set_positions_arr = set_positions
3258 .as_ref()
3259 .map(|v| NSArray::from_retained_slice(v));
3260
3261 unsafe {
3262 use objc2::AnyThread;
3263 EKRecurrenceRule::initRecurrenceWithFrequency_interval_daysOfTheWeek_daysOfTheMonth_monthsOfTheYear_weeksOfTheYear_daysOfTheYear_setPositions_end(
3264 EKRecurrenceRule::alloc(),
3265 frequency,
3266 rule.interval as isize,
3267 days_of_week_arr.as_deref(),
3268 days_of_month_arr.as_deref(),
3269 months_of_year_arr.as_deref(),
3270 weeks_of_year_arr.as_deref(),
3271 days_of_year_arr.as_deref(),
3272 set_positions_arr.as_deref(),
3273 end.as_deref(),
3274 )
3275 }
3276}
3277
3278fn alarm_to_info(alarm: &EKAlarm) -> AlarmInfo {
3280 let relative_offset = unsafe { alarm.relativeOffset() };
3281 let absolute_date = unsafe { alarm.absoluteDate() }.map(|d| nsdate_to_datetime(&d));
3282
3283 let proximity = unsafe { alarm.proximity() };
3284 let proximity = match proximity {
3285 EKAlarmProximity::Enter => AlarmProximity::Enter,
3286 EKAlarmProximity::Leave => AlarmProximity::Leave,
3287 _ => AlarmProximity::None,
3288 };
3289
3290 let location = unsafe { alarm.structuredLocation() }.map(|loc| {
3291 let title = unsafe { loc.title() }
3292 .map(|t| t.to_string())
3293 .unwrap_or_default();
3294 let radius = unsafe { loc.radius() };
3295 let (latitude, longitude) = unsafe { loc.geoLocation() }
3296 .map(|geo| {
3297 let coord = unsafe { geo.coordinate() };
3298 (coord.latitude, coord.longitude)
3299 })
3300 .unwrap_or((0.0, 0.0));
3301
3302 StructuredLocation {
3303 title,
3304 latitude,
3305 longitude,
3306 radius,
3307 }
3308 });
3309
3310 let email_address = unsafe { alarm.emailAddress() }.map(|s| s.to_string());
3311 let sound_name = unsafe { alarm.soundName() }.map(|s| s.to_string());
3312 #[allow(deprecated)]
3313 let url = unsafe { alarm.url() }
3314 .as_ref()
3315 .and_then(|u| u.absoluteString())
3316 .map(|s| s.to_string());
3317
3318 let alarm_type = match unsafe { alarm.r#type() } {
3319 EKAlarmType::Display => AlarmType::Display,
3320 EKAlarmType::Audio => AlarmType::Audio,
3321 EKAlarmType::Procedure => AlarmType::Procedure,
3322 EKAlarmType::Email => AlarmType::Email,
3323 _ => AlarmType::Unknown,
3324 };
3325
3326 AlarmInfo {
3327 relative_offset: Some(relative_offset),
3329 absolute_date,
3330 proximity,
3331 location,
3332 email_address,
3333 sound_name,
3334 url,
3335 alarm_type,
3336 }
3337}
3338
3339fn create_ek_alarm(info: &AlarmInfo) -> Result<Retained<EKAlarm>> {
3342 let alarm = if let Some(date) = &info.absolute_date {
3343 let nsdate = datetime_to_nsdate(*date);
3344 unsafe { EKAlarm::alarmWithAbsoluteDate(&nsdate) }
3345 } else {
3346 let offset = info.relative_offset.unwrap_or(0.0);
3347 unsafe { EKAlarm::alarmWithRelativeOffset(offset) }
3348 };
3349
3350 let prox = match info.proximity {
3351 AlarmProximity::Enter => EKAlarmProximity::Enter,
3352 AlarmProximity::Leave => EKAlarmProximity::Leave,
3353 AlarmProximity::None => EKAlarmProximity::None,
3354 };
3355 unsafe { alarm.setProximity(prox) };
3356
3357 if let Some(loc) = &info.location {
3358 let structured = build_ek_structured_location(loc);
3359 unsafe { alarm.setStructuredLocation(Some(&structured)) };
3360 }
3361
3362 if let Some(email) = &info.email_address {
3365 let ns = NSString::from_str(email);
3366 unsafe { alarm.setEmailAddress(Some(&ns)) };
3367 }
3368 if let Some(sound) = &info.sound_name {
3369 let ns = NSString::from_str(sound);
3370 unsafe { alarm.setSoundName(Some(&ns)) };
3371 }
3372 if let Some(u) = &info.url {
3373 let ns_str = NSString::from_str(u);
3374 let parsed =
3375 objc2_foundation::NSURL::URLWithString_encodingInvalidCharacters(&ns_str, false)
3376 .ok_or_else(|| EventKitError::InvalidURL(u.clone()))?;
3377 #[allow(deprecated)]
3378 unsafe {
3379 alarm.setUrl(Some(&parsed))
3380 };
3381 }
3382
3383 Ok(alarm)
3384}
3385
3386#[cfg(test)]
3387mod tests {
3388 use super::*;
3389
3390 #[test]
3391 fn event_span_default_is_this_and_maps_to_ek() {
3392 assert_eq!(EventSpan::default(), EventSpan::This);
3393 assert_eq!(EventSpan::This.to_ek(), EKSpan::ThisEvent);
3394 assert_eq!(EventSpan::Future.to_ek(), EKSpan::FutureEvents);
3395 }
3396
3397 #[test]
3398 fn event_availability_round_trips_every_variant() {
3399 for v in [
3400 EventAvailability::NotSupported,
3401 EventAvailability::Busy,
3402 EventAvailability::Free,
3403 EventAvailability::Tentative,
3404 EventAvailability::Unavailable,
3405 ] {
3406 assert_eq!(EventAvailability::from_ek(v.to_ek()), v);
3407 }
3408 }
3409
3410 #[test]
3411 fn event_status_from_ek_covers_each_variant() {
3412 assert_eq!(EventStatus::from_ek(EKEventStatus::None), EventStatus::None);
3413 assert_eq!(
3414 EventStatus::from_ek(EKEventStatus::Confirmed),
3415 EventStatus::Confirmed
3416 );
3417 assert_eq!(
3418 EventStatus::from_ek(EKEventStatus::Tentative),
3419 EventStatus::Tentative
3420 );
3421 assert_eq!(
3422 EventStatus::from_ek(EKEventStatus::Canceled),
3423 EventStatus::Canceled
3424 );
3425 }
3426
3427 #[test]
3428 fn event_draft_default_has_empty_title_and_zero_dates() {
3429 let d: EventDraft<'_> = EventDraft::default();
3433 assert_eq!(d.title, "");
3434 assert!(d.start.is_none());
3435 assert!(d.end.is_none());
3436 assert!(!d.all_day);
3437 assert!(d.URL.is_none());
3438 assert!(d.availability.is_none());
3439 }
3440
3441 #[test]
3442 fn event_patch_default_touches_nothing() {
3443 let p: EventPatch<'_> = EventPatch::default();
3444 assert!(p.title.is_none());
3445 assert!(p.notes.is_none());
3446 assert!(p.URL.is_none());
3447 assert!(p.availability.is_none());
3448 assert!(p.all_day.is_none());
3449 assert_eq!(p.span, EventSpan::This);
3450 }
3451
3452 #[test]
3453 fn test_authorization_status_display() {
3454 assert_eq!(
3455 format!("{}", AuthorizationStatus::NotDetermined),
3456 "Not Determined"
3457 );
3458 assert_eq!(
3459 format!("{}", AuthorizationStatus::FullAccess),
3460 "Full Access"
3461 );
3462 }
3463
3464 #[test]
3465 fn test_event_item_debug() {
3466 let event = EventItem {
3467 identifier: "test".to_string(),
3468 title: "Test Event".to_string(),
3469 notes: None,
3470 location: None,
3471 start_date: Local::now(),
3472 end_date: Local::now(),
3473 all_day: false,
3474 calendar_title: None,
3475 calendar_id: None,
3476 URL: None,
3477 availability: EventAvailability::Busy,
3478 status: EventStatus::None,
3479 is_detached: false,
3480 occurrence_date: None,
3481 structured_location: None,
3482 creation_date: None,
3483 last_modified_date: None,
3484 external_identifier: None,
3485 timezone: None,
3486 attachments_count: 0,
3487 attendees: Vec::new(),
3488 organizer: None,
3489 };
3490 assert!(format!("{:?}", event).contains("Test Event"));
3491 }
3492}