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, EKAuthorizationStatus, EKCalendar, EKCalendarItem, EKEntityType,
8 EKEvent, EKEventStore, EKRecurrenceDayOfWeek, EKRecurrenceEnd, EKRecurrenceFrequency,
9 EKRecurrenceRule, EKReminder, EKSource, EKSpan, EKStructuredLocation, EKWeekday,
10};
11use objc2_foundation::{
12 NSArray, NSCalendar, NSDate, NSDateComponents, NSError, NSNumber, NSString,
13};
14use std::sync::{Arc, Condvar, Mutex};
15use thiserror::Error;
16
17#[cfg(feature = "location")]
18#[path = "location.rs"]
19pub mod location;
20
21#[cfg(feature = "mcp")]
22#[path = "mcp.rs"]
23pub mod mcp;
24
25#[derive(Error, Debug)]
27pub enum EventKitError {
28 #[error("Authorization denied")]
29 AuthorizationDenied,
30
31 #[error("Authorization restricted by system policy")]
32 AuthorizationRestricted,
33
34 #[error("Authorization not determined")]
35 AuthorizationNotDetermined,
36
37 #[error("Failed to request authorization: {0}")]
38 AuthorizationRequestFailed(String),
39
40 #[error("No default calendar")]
41 NoDefaultCalendar,
42
43 #[error("Calendar not found: {0}")]
44 CalendarNotFound(String),
45
46 #[error("Item not found: {0}")]
47 ItemNotFound(String),
48
49 #[error("Failed to save: {0}")]
50 SaveFailed(String),
51
52 #[error("Failed to delete: {0}")]
53 DeleteFailed(String),
54
55 #[error("Failed to fetch: {0}")]
56 FetchFailed(String),
57
58 #[error("EventKit error: {0}")]
59 EventKitError(String),
60
61 #[error("Invalid date range")]
62 InvalidDateRange,
63}
64
65pub type RemindersError = EventKitError;
67
68pub type Result<T> = std::result::Result<T, EventKitError>;
70
71#[derive(Debug, Clone)]
73pub struct ReminderItem {
74 pub identifier: String,
76 pub title: String,
78 pub notes: Option<String>,
80 pub completed: bool,
82 pub priority: usize,
84 pub calendar_title: Option<String>,
86 pub calendar_id: Option<String>,
88 pub due_date: Option<DateTime<Local>>,
90 pub start_date: Option<DateTime<Local>>,
92 pub completion_date: Option<DateTime<Local>>,
94 pub external_identifier: Option<String>,
96 pub location: Option<String>,
98 pub url: Option<String>,
100 pub creation_date: Option<DateTime<Local>>,
102 pub last_modified_date: Option<DateTime<Local>>,
104 pub timezone: Option<String>,
106 pub has_alarms: bool,
108 pub has_recurrence_rules: bool,
110 pub has_attendees: bool,
112 pub has_notes: bool,
114 pub attendees: Vec<ParticipantInfo>,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum CalendarType {
121 Local,
122 CalDAV,
123 Exchange,
124 Subscription,
125 Birthday,
126 Unknown,
127}
128
129#[derive(Debug, Clone)]
131pub struct SourceInfo {
132 pub identifier: String,
133 pub title: String,
134 pub source_type: String,
135}
136
137#[derive(Debug, Clone)]
139pub struct CalendarInfo {
140 pub identifier: String,
142 pub title: String,
144 pub source: Option<String>,
146 pub source_id: Option<String>,
148 pub calendar_type: CalendarType,
150 pub allows_modifications: bool,
152 pub is_immutable: bool,
154 pub is_subscribed: bool,
156 pub color: Option<(f64, f64, f64, f64)>,
158 pub allowed_entity_types: Vec<String>,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum AlarmProximity {
165 None,
167 Enter,
169 Leave,
171}
172
173#[derive(Debug, Clone)]
175pub struct StructuredLocation {
176 pub title: String,
178 pub latitude: f64,
180 pub longitude: f64,
182 pub radius: f64,
184}
185
186#[derive(Debug, Clone)]
188pub struct AlarmInfo {
189 pub relative_offset: Option<f64>,
191 pub absolute_date: Option<DateTime<Local>>,
193 pub proximity: AlarmProximity,
195 pub location: Option<StructuredLocation>,
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum RecurrenceFrequency {
202 Daily,
203 Weekly,
204 Monthly,
205 Yearly,
206}
207
208#[derive(Debug, Clone)]
210pub enum RecurrenceEndCondition {
211 Never,
213 AfterCount(usize),
215 OnDate(DateTime<Local>),
217}
218
219#[derive(Debug, Clone)]
221pub struct RecurrenceRule {
222 pub frequency: RecurrenceFrequency,
224 pub interval: usize,
226 pub end: RecurrenceEndCondition,
228 pub days_of_week: Option<Vec<u8>>,
230 pub days_of_month: Option<Vec<i32>>,
232}
233
234pub struct RemindersManager {
236 store: Retained<EKEventStore>,
237}
238
239impl RemindersManager {
240 pub fn new() -> Self {
242 let store = unsafe { EKEventStore::new() };
243 Self { store }
244 }
245
246 pub fn authorization_status() -> AuthorizationStatus {
248 let status =
249 unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Reminder) };
250 status.into()
251 }
252
253 pub fn request_access(&self) -> Result<bool> {
257 let result = Arc::new((Mutex::new(None::<(bool, Option<String>)>), Condvar::new()));
258 let result_clone = Arc::clone(&result);
259
260 let completion = RcBlock::new(move |granted: Bool, error: *mut NSError| {
261 let error_msg = if !error.is_null() {
262 let error_ref = unsafe { &*error };
263 Some(format!("{:?}", error_ref))
264 } else {
265 None
266 };
267
268 let (lock, cvar) = &*result_clone;
269 let mut res = lock.lock().unwrap();
270 *res = Some((granted.as_bool(), error_msg));
271 cvar.notify_one();
272 });
273
274 unsafe {
275 let block_ptr = &*completion as *const _ as *mut _;
277 self.store
278 .requestFullAccessToRemindersWithCompletion(block_ptr);
279 }
280
281 let (lock, cvar) = &*result;
282 let mut res = lock.lock().unwrap();
283 while res.is_none() {
284 res = cvar.wait(res).unwrap();
285 }
286
287 match res.take() {
288 Some((granted, None)) => Ok(granted),
289 Some((_, Some(error))) => Err(RemindersError::AuthorizationRequestFailed(error)),
290 None => Err(RemindersError::AuthorizationRequestFailed(
291 "Unknown error".to_string(),
292 )),
293 }
294 }
295
296 pub fn ensure_authorized(&self) -> Result<()> {
298 match Self::authorization_status() {
299 AuthorizationStatus::FullAccess => Ok(()),
300 AuthorizationStatus::NotDetermined => {
301 if self.request_access()? {
302 Ok(())
303 } else {
304 Err(RemindersError::AuthorizationDenied)
305 }
306 }
307 AuthorizationStatus::Denied => Err(RemindersError::AuthorizationDenied),
308 AuthorizationStatus::Restricted => Err(RemindersError::AuthorizationRestricted),
309 AuthorizationStatus::WriteOnly => Ok(()), }
311 }
312
313 pub fn list_calendars(&self) -> Result<Vec<CalendarInfo>> {
315 self.ensure_authorized()?;
316
317 let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
318
319 let mut result = Vec::new();
320 for calendar in calendars.iter() {
321 result.push(calendar_to_info(&calendar));
322 }
323
324 Ok(result)
325 }
326
327 pub fn list_sources(&self) -> Result<Vec<SourceInfo>> {
329 self.ensure_authorized()?;
330 let sources = unsafe { self.store.sources() };
331 let mut result = Vec::new();
332 for source in sources.iter() {
333 result.push(source_to_info(&source));
334 }
335 Ok(result)
336 }
337
338 pub fn default_calendar(&self) -> Result<CalendarInfo> {
340 self.ensure_authorized()?;
341
342 let calendar = unsafe { self.store.defaultCalendarForNewReminders() };
343
344 match calendar {
345 Some(cal) => Ok(calendar_to_info(&cal)),
346 None => Err(RemindersError::NoDefaultCalendar),
347 }
348 }
349
350 pub fn fetch_all_reminders(&self) -> Result<Vec<ReminderItem>> {
352 self.fetch_reminders(None)
353 }
354
355 pub fn fetch_reminders(&self, calendar_titles: Option<&[&str]>) -> Result<Vec<ReminderItem>> {
357 self.ensure_authorized()?;
358
359 let calendars: Option<Retained<NSArray<EKCalendar>>> = match calendar_titles {
360 Some(titles) => {
361 let all_calendars =
362 unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
363 let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
364
365 for cal in all_calendars.iter() {
366 let title = unsafe { cal.title() };
367 let title_str = title.to_string();
368 if titles.iter().any(|t| *t == title_str) {
369 matching.push(cal.retain());
370 }
371 }
372
373 if matching.is_empty() {
374 return Err(RemindersError::CalendarNotFound(titles.join(", ")));
375 }
376
377 Some(NSArray::from_retained_slice(&matching))
378 }
379 None => None,
380 };
381
382 let predicate = unsafe {
383 self.store
384 .predicateForRemindersInCalendars(calendars.as_deref())
385 };
386
387 let result = Arc::new((Mutex::new(None::<Vec<ReminderItem>>), Condvar::new()));
388 let result_clone = Arc::clone(&result);
389
390 let completion = RcBlock::new(move |reminders: *mut NSArray<EKReminder>| {
391 let items = if reminders.is_null() {
392 Vec::new()
393 } else {
394 let reminders = unsafe { Retained::retain(reminders).unwrap() };
395 reminders.iter().map(|r| reminder_to_item(&r)).collect()
396 };
397 let (lock, cvar) = &*result_clone;
398 let mut guard = lock.lock().unwrap();
399 *guard = Some(items);
400 cvar.notify_one();
401 });
402
403 unsafe {
404 self.store
405 .fetchRemindersMatchingPredicate_completion(&predicate, &completion);
406 }
407
408 let (lock, cvar) = &*result;
409 let mut guard = lock.lock().unwrap();
410 while guard.is_none() {
411 guard = cvar.wait(guard).unwrap();
412 }
413
414 guard
415 .take()
416 .ok_or_else(|| RemindersError::FetchFailed("Unknown error".to_string()))
417 }
418
419 pub fn fetch_incomplete_reminders(&self) -> Result<Vec<ReminderItem>> {
421 self.ensure_authorized()?;
422
423 let predicate = unsafe {
424 self.store
425 .predicateForIncompleteRemindersWithDueDateStarting_ending_calendars(
426 None, None, None,
427 )
428 };
429
430 let result = Arc::new((Mutex::new(None::<Vec<ReminderItem>>), Condvar::new()));
431 let result_clone = Arc::clone(&result);
432
433 let completion = RcBlock::new(move |reminders: *mut NSArray<EKReminder>| {
434 let items = if reminders.is_null() {
435 Vec::new()
436 } else {
437 let reminders = unsafe { Retained::retain(reminders).unwrap() };
438 reminders.iter().map(|r| reminder_to_item(&r)).collect()
439 };
440 let (lock, cvar) = &*result_clone;
441 let mut guard = lock.lock().unwrap();
442 *guard = Some(items);
443 cvar.notify_one();
444 });
445
446 unsafe {
447 self.store
448 .fetchRemindersMatchingPredicate_completion(&predicate, &completion);
449 }
450
451 let (lock, cvar) = &*result;
452 let mut guard = lock.lock().unwrap();
453 while guard.is_none() {
454 guard = cvar.wait(guard).unwrap();
455 }
456
457 guard
458 .take()
459 .ok_or_else(|| RemindersError::FetchFailed("Unknown error".to_string()))
460 }
461
462 #[allow(clippy::too_many_arguments)]
472 pub fn create_reminder(
473 &self,
474 title: &str,
475 notes: Option<&str>,
476 calendar_title: Option<&str>,
477 priority: Option<usize>,
478 due_date: Option<DateTime<Local>>,
479 start_date: Option<DateTime<Local>>,
480 ) -> Result<ReminderItem> {
481 self.ensure_authorized()?;
482
483 let reminder = unsafe { EKReminder::reminderWithEventStore(&self.store) };
484
485 let ns_title = NSString::from_str(title);
487 unsafe { reminder.setTitle(Some(&ns_title)) };
488
489 if let Some(notes_text) = notes {
491 let ns_notes = NSString::from_str(notes_text);
492 unsafe { reminder.setNotes(Some(&ns_notes)) };
493 }
494
495 if let Some(p) = priority {
497 unsafe { reminder.setPriority(p) };
498 }
499
500 if let Some(due) = due_date {
502 let components = datetime_to_date_components(due);
503 unsafe { reminder.setDueDateComponents(Some(&components)) };
504 }
505
506 if let Some(start) = start_date {
508 let components = datetime_to_date_components(start);
509 unsafe { reminder.setStartDateComponents(Some(&components)) };
510 }
511
512 let calendar = if let Some(cal_title) = calendar_title {
514 self.find_calendar_by_title(cal_title)?
515 } else {
516 unsafe { self.store.defaultCalendarForNewReminders() }
517 .ok_or(RemindersError::NoDefaultCalendar)?
518 };
519 unsafe { reminder.setCalendar(Some(&calendar)) };
520
521 unsafe {
523 self.store
524 .saveReminder_commit_error(&reminder, true)
525 .map_err(|e| RemindersError::SaveFailed(format!("{:?}", e)))?;
526 }
527
528 Ok(reminder_to_item(&reminder))
529 }
530
531 #[allow(clippy::too_many_arguments)]
537 pub fn update_reminder(
538 &self,
539 identifier: &str,
540 title: Option<&str>,
541 notes: Option<&str>,
542 completed: Option<bool>,
543 priority: Option<usize>,
544 due_date: Option<Option<DateTime<Local>>>,
545 start_date: Option<Option<DateTime<Local>>>,
546 calendar_title: Option<&str>,
547 ) -> Result<ReminderItem> {
548 self.ensure_authorized()?;
549
550 let reminder = self.find_reminder_by_id(identifier)?;
551
552 if let Some(t) = title {
553 let ns_title = NSString::from_str(t);
554 unsafe { reminder.setTitle(Some(&ns_title)) };
555 }
556
557 if let Some(n) = notes {
558 let ns_notes = NSString::from_str(n);
559 unsafe { reminder.setNotes(Some(&ns_notes)) };
560 }
561
562 if let Some(c) = completed {
563 unsafe { reminder.setCompleted(c) };
564 }
565
566 if let Some(p) = priority {
567 unsafe { reminder.setPriority(p) };
568 }
569
570 if let Some(due_opt) = due_date {
572 match due_opt {
573 Some(due) => {
574 let components = datetime_to_date_components(due);
575 unsafe { reminder.setDueDateComponents(Some(&components)) };
576 }
577 None => {
578 unsafe { reminder.setDueDateComponents(None) };
579 }
580 }
581 }
582
583 if let Some(start_opt) = start_date {
585 match start_opt {
586 Some(start) => {
587 let components = datetime_to_date_components(start);
588 unsafe { reminder.setStartDateComponents(Some(&components)) };
589 }
590 None => {
591 unsafe { reminder.setStartDateComponents(None) };
592 }
593 }
594 }
595
596 if let Some(cal_title) = calendar_title {
598 let calendar = self.find_calendar_by_title(cal_title)?;
599 unsafe { reminder.setCalendar(Some(&calendar)) };
600 }
601
602 unsafe {
603 self.store
604 .saveReminder_commit_error(&reminder, true)
605 .map_err(|e| RemindersError::SaveFailed(format!("{:?}", e)))?;
606 }
607
608 Ok(reminder_to_item(&reminder))
609 }
610
611 pub fn complete_reminder(&self, identifier: &str) -> Result<ReminderItem> {
613 self.update_reminder(identifier, None, None, Some(true), None, None, None, None)
614 }
615
616 pub fn uncomplete_reminder(&self, identifier: &str) -> Result<ReminderItem> {
618 self.update_reminder(identifier, None, None, Some(false), None, None, None, None)
619 }
620
621 pub fn delete_reminder(&self, identifier: &str) -> Result<()> {
623 self.ensure_authorized()?;
624
625 let reminder = self.find_reminder_by_id(identifier)?;
626
627 unsafe {
628 self.store
629 .removeReminder_commit_error(&reminder, true)
630 .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
631 }
632
633 Ok(())
634 }
635
636 pub fn get_reminder(&self, identifier: &str) -> Result<ReminderItem> {
638 self.ensure_authorized()?;
639 let reminder = self.find_reminder_by_id(identifier)?;
640 Ok(reminder_to_item(&reminder))
641 }
642
643 pub fn get_alarms(&self, identifier: &str) -> Result<Vec<AlarmInfo>> {
649 self.ensure_authorized()?;
650 let reminder = self.find_reminder_by_id(identifier)?;
651 Ok(get_item_alarms(&reminder))
652 }
653
654 pub fn add_alarm(&self, identifier: &str, alarm: &AlarmInfo) -> Result<()> {
656 self.ensure_authorized()?;
657 let reminder = self.find_reminder_by_id(identifier)?;
658 add_item_alarm(&reminder, alarm);
659 unsafe {
660 self.store
661 .saveReminder_commit_error(&reminder, true)
662 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
663 }
664 Ok(())
665 }
666
667 pub fn remove_all_alarms(&self, identifier: &str) -> Result<()> {
669 self.ensure_authorized()?;
670 let reminder = self.find_reminder_by_id(identifier)?;
671 clear_item_alarms(&reminder);
672 unsafe {
673 self.store
674 .saveReminder_commit_error(&reminder, true)
675 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
676 }
677 Ok(())
678 }
679
680 pub fn remove_alarm(&self, identifier: &str, index: usize) -> Result<()> {
682 self.ensure_authorized()?;
683 let reminder = self.find_reminder_by_id(identifier)?;
684 remove_item_alarm(&reminder, index)?;
685 unsafe {
686 self.store
687 .saveReminder_commit_error(&reminder, true)
688 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
689 }
690 Ok(())
691 }
692
693 pub fn set_url(&self, identifier: &str, url: Option<&str>) -> Result<()> {
699 self.ensure_authorized()?;
700 let reminder = self.find_reminder_by_id(identifier)?;
701 set_item_url(&reminder, url);
702 unsafe {
703 self.store
704 .saveReminder_commit_error(&reminder, true)
705 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
706 }
707 Ok(())
708 }
709
710 pub fn get_recurrence_rules(&self, identifier: &str) -> Result<Vec<RecurrenceRule>> {
716 self.ensure_authorized()?;
717 let reminder = self.find_reminder_by_id(identifier)?;
718 Ok(get_item_recurrence_rules(&reminder))
719 }
720
721 pub fn set_recurrence_rule(&self, identifier: &str, rule: &RecurrenceRule) -> Result<()> {
723 self.ensure_authorized()?;
724 let reminder = self.find_reminder_by_id(identifier)?;
725 set_item_recurrence_rule(&reminder, rule);
726 unsafe {
727 self.store
728 .saveReminder_commit_error(&reminder, true)
729 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
730 }
731 Ok(())
732 }
733
734 pub fn remove_recurrence_rules(&self, identifier: &str) -> Result<()> {
736 self.ensure_authorized()?;
737 let reminder = self.find_reminder_by_id(identifier)?;
738 clear_item_recurrence_rules(&reminder);
739 unsafe {
740 self.store
741 .saveReminder_commit_error(&reminder, true)
742 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
743 }
744 Ok(())
745 }
746
747 pub fn create_calendar(&self, title: &str) -> Result<CalendarInfo> {
755 self.ensure_authorized()?;
756
757 let calendar = unsafe {
759 EKCalendar::calendarForEntityType_eventStore(EKEntityType::Reminder, &self.store)
760 };
761
762 let ns_title = NSString::from_str(title);
764 unsafe { calendar.setTitle(&ns_title) };
765
766 let source = self.find_best_source_for_reminders()?;
768 unsafe { calendar.setSource(Some(&source)) };
769
770 unsafe {
772 self.store
773 .saveCalendar_commit_error(&calendar, true)
774 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
775 }
776
777 Ok(calendar_to_info(&calendar))
778 }
779
780 pub fn rename_calendar(&self, identifier: &str, new_title: &str) -> Result<CalendarInfo> {
783 self.update_calendar(identifier, Some(new_title), None)
784 }
785
786 pub fn update_calendar(
788 &self,
789 identifier: &str,
790 new_title: Option<&str>,
791 color_rgba: Option<(f64, f64, f64, f64)>,
792 ) -> Result<CalendarInfo> {
793 self.ensure_authorized()?;
794 let calendar = self.find_calendar_by_id(identifier)?;
795
796 if !unsafe { calendar.allowsContentModifications() } {
797 return Err(EventKitError::SaveFailed(
798 "Calendar does not allow modifications".to_string(),
799 ));
800 }
801
802 if let Some(title) = new_title {
803 let ns_title = NSString::from_str(title);
804 unsafe { calendar.setTitle(&ns_title) };
805 }
806
807 if let Some((r, g, b, a)) = color_rgba {
808 let cg = objc2_core_graphics::CGColor::new_srgb(r, g, b, a);
809 unsafe { calendar.setCGColor(Some(&cg)) };
810 }
811
812 unsafe {
813 self.store
814 .saveCalendar_commit_error(&calendar, true)
815 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
816 }
817
818 Ok(calendar_to_info(&calendar))
819 }
820
821 pub fn delete_calendar(&self, identifier: &str) -> Result<()> {
825 self.ensure_authorized()?;
826
827 let calendar = self.find_calendar_by_id(identifier)?;
828
829 if !unsafe { calendar.allowsContentModifications() } {
831 return Err(EventKitError::DeleteFailed(
832 "Calendar does not allow modifications".to_string(),
833 ));
834 }
835
836 unsafe {
837 self.store
838 .removeCalendar_commit_error(&calendar, true)
839 .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
840 }
841
842 Ok(())
843 }
844
845 pub fn get_calendar(&self, identifier: &str) -> Result<CalendarInfo> {
847 self.ensure_authorized()?;
848 let calendar = self.find_calendar_by_id(identifier)?;
849 Ok(calendar_to_info(&calendar))
850 }
851
852 fn find_best_source_for_reminders(&self) -> Result<Retained<objc2_event_kit::EKSource>> {
854 if let Some(default_cal) = unsafe { self.store.defaultCalendarForNewReminders() }
856 && let Some(source) = unsafe { default_cal.source() }
857 {
858 return Ok(source);
859 }
860
861 let sources = unsafe { self.store.sources() };
863 for source in sources.iter() {
864 let calendars = unsafe { source.calendarsForEntityType(EKEntityType::Reminder) };
866 if !calendars.is_empty() {
867 return Ok(source.retain());
868 }
869 }
870
871 Err(EventKitError::SaveFailed(
872 "No suitable source found for creating reminder calendar".to_string(),
873 ))
874 }
875
876 fn find_calendar_by_id(&self, identifier: &str) -> Result<Retained<EKCalendar>> {
878 let ns_id = NSString::from_str(identifier);
879 let calendar = unsafe { self.store.calendarWithIdentifier(&ns_id) };
880
881 match calendar {
882 Some(cal) => Ok(cal),
883 None => Err(EventKitError::CalendarNotFound(identifier.to_string())),
884 }
885 }
886
887 fn find_calendar_by_title(&self, title: &str) -> Result<Retained<EKCalendar>> {
889 let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
890
891 for cal in calendars.iter() {
892 let cal_title = unsafe { cal.title() };
893 if cal_title.to_string() == title {
894 return Ok(cal.retain());
895 }
896 }
897
898 Err(RemindersError::CalendarNotFound(title.to_string()))
899 }
900
901 fn find_reminder_by_id(&self, identifier: &str) -> Result<Retained<EKReminder>> {
903 let ns_id = NSString::from_str(identifier);
904 let item = unsafe { self.store.calendarItemWithIdentifier(&ns_id) };
905
906 match item {
907 Some(item) => {
908 if let Some(reminder) = item.downcast_ref::<EKReminder>() {
910 Ok(reminder.retain())
911 } else {
912 Err(EventKitError::ItemNotFound(identifier.to_string()))
913 }
914 }
915 None => Err(EventKitError::ItemNotFound(identifier.to_string())),
916 }
917 }
918}
919
920impl Default for RemindersManager {
921 fn default() -> Self {
922 Self::new()
923 }
924}
925
926#[derive(Debug, Clone, Copy, PartialEq, Eq)]
928pub enum AuthorizationStatus {
929 NotDetermined,
931 Restricted,
933 Denied,
935 FullAccess,
937 WriteOnly,
939}
940
941impl From<EKAuthorizationStatus> for AuthorizationStatus {
942 fn from(status: EKAuthorizationStatus) -> Self {
943 if status == EKAuthorizationStatus::NotDetermined {
944 AuthorizationStatus::NotDetermined
945 } else if status == EKAuthorizationStatus::Restricted {
946 AuthorizationStatus::Restricted
947 } else if status == EKAuthorizationStatus::Denied {
948 AuthorizationStatus::Denied
949 } else if status == EKAuthorizationStatus::FullAccess {
950 AuthorizationStatus::FullAccess
951 } else if status == EKAuthorizationStatus::WriteOnly {
952 AuthorizationStatus::WriteOnly
953 } else {
954 AuthorizationStatus::NotDetermined
955 }
956 }
957}
958
959impl std::fmt::Display for AuthorizationStatus {
960 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
961 match self {
962 AuthorizationStatus::NotDetermined => write!(f, "Not Determined"),
963 AuthorizationStatus::Restricted => write!(f, "Restricted"),
964 AuthorizationStatus::Denied => write!(f, "Denied"),
965 AuthorizationStatus::FullAccess => write!(f, "Full Access"),
966 AuthorizationStatus::WriteOnly => write!(f, "Write Only"),
967 }
968 }
969}
970
971fn reminder_to_item(reminder: &EKReminder) -> ReminderItem {
973 let identifier = unsafe { reminder.calendarItemIdentifier() }.to_string();
974 let title = unsafe { reminder.title() }.to_string();
975 let notes = unsafe { reminder.notes() }.map(|n| n.to_string());
976 let completed = unsafe { reminder.isCompleted() };
977 let priority = unsafe { reminder.priority() };
978 let cal = unsafe { reminder.calendar() };
979 let calendar_title = cal.as_ref().map(|c| unsafe { c.title() }.to_string());
980 let calendar_id = cal
981 .as_ref()
982 .map(|c| unsafe { c.calendarIdentifier() }.to_string());
983
984 let due_date = unsafe { reminder.dueDateComponents() }
986 .and_then(|components| date_components_to_datetime(&components));
987
988 let start_date = unsafe { reminder.startDateComponents() }
990 .and_then(|components| date_components_to_datetime(&components));
991
992 let completion_date =
994 unsafe { reminder.completionDate() }.map(|date| nsdate_to_datetime(&date));
995
996 let external_identifier =
998 unsafe { reminder.calendarItemExternalIdentifier() }.map(|id| id.to_string());
999 let location = unsafe { reminder.location() }.map(|loc| loc.to_string());
1000 let url = unsafe { reminder.URL() }
1001 .as_ref()
1002 .and_then(|url_ref| url_ref.absoluteString())
1003 .map(|abs_str| abs_str.to_string());
1004 let creation_date = unsafe { reminder.creationDate() }.map(|date| nsdate_to_datetime(&date));
1005 let last_modified_date =
1006 unsafe { reminder.lastModifiedDate() }.map(|date| nsdate_to_datetime(&date));
1007 let timezone = unsafe { reminder.timeZone() }.map(|tz| tz.name().to_string());
1008 let has_alarms = unsafe { reminder.hasAlarms() };
1009 let has_recurrence_rules = unsafe { reminder.hasRecurrenceRules() };
1010 let has_attendees = unsafe { reminder.hasAttendees() };
1011 let has_notes = unsafe { reminder.hasNotes() };
1012
1013 ReminderItem {
1014 identifier,
1015 title,
1016 notes,
1017 completed,
1018 priority,
1019 calendar_title,
1020 calendar_id,
1021 due_date,
1022 start_date,
1023 completion_date,
1024 external_identifier,
1025 location,
1026 url,
1027 creation_date,
1028 last_modified_date,
1029 timezone,
1030 has_alarms,
1031 has_recurrence_rules,
1032 has_attendees,
1033 has_notes,
1034 attendees: get_item_attendees(reminder),
1035 }
1036}
1037
1038fn source_to_info(source: &EKSource) -> SourceInfo {
1040 let identifier = unsafe { source.sourceIdentifier() }.to_string();
1041 let title = unsafe { source.title() }.to_string();
1042 let source_type = unsafe { source.sourceType() };
1044 let source_type = match source_type.0 {
1045 0 => "local",
1046 1 => "exchange",
1047 2 => "caldav",
1048 3 => "mobileme",
1049 4 => "subscribed",
1050 5 => "birthdays",
1051 _ => "unknown",
1052 }
1053 .to_string();
1054
1055 SourceInfo {
1056 identifier,
1057 title,
1058 source_type,
1059 }
1060}
1061
1062fn calendar_to_info(calendar: &EKCalendar) -> CalendarInfo {
1063 let identifier = unsafe { calendar.calendarIdentifier() }.to_string();
1064 let title = unsafe { calendar.title() }.to_string();
1065 let source = unsafe { calendar.source() }.map(|s| unsafe { s.title() }.to_string());
1066 let source_id =
1067 unsafe { calendar.source() }.map(|s| unsafe { s.sourceIdentifier() }.to_string());
1068 let allows_modifications = unsafe { calendar.allowsContentModifications() };
1069 let is_immutable = unsafe { calendar.isImmutable() };
1070 let is_subscribed = unsafe { calendar.isSubscribed() };
1071
1072 let cal_type = unsafe { calendar.r#type() };
1074 let calendar_type = match cal_type.0 {
1075 0 => CalendarType::Local,
1076 1 => CalendarType::CalDAV,
1077 2 => CalendarType::Exchange,
1078 3 => CalendarType::Subscription,
1079 4 => CalendarType::Birthday,
1080 _ => CalendarType::Unknown,
1081 };
1082
1083 let color: Option<(f64, f64, f64, f64)> = unsafe {
1085 calendar.CGColor().and_then(|cg| {
1086 use objc2_core_graphics::CGColor as CG;
1087 let n = CG::number_of_components(Some(&cg));
1088 if n >= 3 {
1089 let ptr = CG::components(Some(&cg));
1090 let r = *ptr;
1091 let g = *ptr.add(1);
1092 let b = *ptr.add(2);
1093 let a = if n >= 4 { *ptr.add(3) } else { 1.0 };
1094 Some((r, g, b, a))
1095 } else {
1096 None
1097 }
1098 })
1099 };
1100
1101 let entity_mask = unsafe { calendar.allowedEntityTypes() };
1103 let mut allowed_entity_types = Vec::new();
1104 if entity_mask.0 & 1 != 0 {
1105 allowed_entity_types.push("event".to_string());
1106 }
1107 if entity_mask.0 & 2 != 0 {
1108 allowed_entity_types.push("reminder".to_string());
1109 }
1110
1111 CalendarInfo {
1112 identifier,
1113 title,
1114 source,
1115 source_id,
1116 calendar_type,
1117 allows_modifications,
1118 is_immutable,
1119 is_subscribed,
1120 color,
1121 allowed_entity_types,
1122 }
1123}
1124
1125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1131pub enum EventAvailability {
1132 NotSupported,
1133 Busy,
1134 Free,
1135 Tentative,
1136 Unavailable,
1137}
1138
1139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1141pub enum EventStatus {
1142 None,
1143 Confirmed,
1144 Tentative,
1145 Canceled,
1146}
1147
1148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1150pub enum ParticipantRole {
1151 Unknown,
1152 Required,
1153 Optional,
1154 Chair,
1155 NonParticipant,
1156}
1157
1158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1160pub enum ParticipantStatus {
1161 Unknown,
1162 Pending,
1163 Accepted,
1164 Declined,
1165 Tentative,
1166 Delegated,
1167 Completed,
1168 InProcess,
1169}
1170
1171#[derive(Debug, Clone)]
1173pub struct ParticipantInfo {
1174 pub name: Option<String>,
1175 pub url: Option<String>,
1176 pub role: ParticipantRole,
1177 pub status: ParticipantStatus,
1178 pub is_current_user: bool,
1179}
1180
1181#[derive(Debug, Clone)]
1183pub struct EventItem {
1184 pub identifier: String,
1186 pub title: String,
1188 pub notes: Option<String>,
1190 pub location: Option<String>,
1192 pub start_date: DateTime<Local>,
1194 pub end_date: DateTime<Local>,
1196 pub all_day: bool,
1198 pub calendar_title: Option<String>,
1200 pub calendar_id: Option<String>,
1202 pub url: Option<String>,
1204 pub availability: EventAvailability,
1206 pub status: EventStatus,
1208 pub is_detached: bool,
1210 pub occurrence_date: Option<DateTime<Local>>,
1212 pub structured_location: Option<StructuredLocation>,
1214 pub attendees: Vec<ParticipantInfo>,
1216 pub organizer: Option<ParticipantInfo>,
1218}
1219
1220pub struct EventsManager {
1222 store: Retained<EKEventStore>,
1223}
1224
1225impl EventsManager {
1226 pub fn new() -> Self {
1228 let store = unsafe { EKEventStore::new() };
1229 Self { store }
1230 }
1231
1232 pub fn authorization_status() -> AuthorizationStatus {
1234 let status = unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Event) };
1235 status.into()
1236 }
1237
1238 pub fn request_access(&self) -> Result<bool> {
1242 let result = Arc::new((Mutex::new(None::<(bool, Option<String>)>), Condvar::new()));
1243 let result_clone = Arc::clone(&result);
1244
1245 let completion = RcBlock::new(move |granted: Bool, error: *mut NSError| {
1246 let error_msg = if !error.is_null() {
1247 let error_ref = unsafe { &*error };
1248 Some(format!("{:?}", error_ref))
1249 } else {
1250 None
1251 };
1252
1253 let (lock, cvar) = &*result_clone;
1254 let mut res = lock.lock().unwrap();
1255 *res = Some((granted.as_bool(), error_msg));
1256 cvar.notify_one();
1257 });
1258
1259 unsafe {
1260 let block_ptr = &*completion as *const _ as *mut _;
1261 self.store
1262 .requestFullAccessToEventsWithCompletion(block_ptr);
1263 }
1264
1265 let (lock, cvar) = &*result;
1266 let mut res = lock.lock().unwrap();
1267 while res.is_none() {
1268 res = cvar.wait(res).unwrap();
1269 }
1270
1271 match res.take() {
1272 Some((granted, None)) => Ok(granted),
1273 Some((_, Some(error))) => Err(EventKitError::AuthorizationRequestFailed(error)),
1274 None => Err(EventKitError::AuthorizationRequestFailed(
1275 "Unknown error".to_string(),
1276 )),
1277 }
1278 }
1279
1280 pub fn ensure_authorized(&self) -> Result<()> {
1282 match Self::authorization_status() {
1283 AuthorizationStatus::FullAccess => Ok(()),
1284 AuthorizationStatus::NotDetermined => {
1285 if self.request_access()? {
1286 Ok(())
1287 } else {
1288 Err(EventKitError::AuthorizationDenied)
1289 }
1290 }
1291 AuthorizationStatus::Denied => Err(EventKitError::AuthorizationDenied),
1292 AuthorizationStatus::Restricted => Err(EventKitError::AuthorizationRestricted),
1293 AuthorizationStatus::WriteOnly => Ok(()),
1294 }
1295 }
1296
1297 pub fn list_calendars(&self) -> Result<Vec<CalendarInfo>> {
1299 self.ensure_authorized()?;
1300
1301 let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
1302
1303 let mut result = Vec::new();
1304 for calendar in calendars.iter() {
1305 result.push(calendar_to_info(&calendar));
1306 }
1307
1308 Ok(result)
1309 }
1310
1311 pub fn default_calendar(&self) -> Result<CalendarInfo> {
1313 self.ensure_authorized()?;
1314
1315 let calendar = unsafe { self.store.defaultCalendarForNewEvents() };
1316
1317 match calendar {
1318 Some(cal) => Ok(calendar_to_info(&cal)),
1319 None => Err(EventKitError::NoDefaultCalendar),
1320 }
1321 }
1322
1323 pub fn fetch_today_events(&self) -> Result<Vec<EventItem>> {
1325 let now = Local::now();
1326 let start = now.date_naive().and_hms_opt(0, 0, 0).unwrap();
1327 let end = now.date_naive().and_hms_opt(23, 59, 59).unwrap();
1328
1329 self.fetch_events(
1330 Local.from_local_datetime(&start).unwrap(),
1331 Local.from_local_datetime(&end).unwrap(),
1332 None,
1333 )
1334 }
1335
1336 pub fn fetch_upcoming_events(&self, days: i64) -> Result<Vec<EventItem>> {
1338 let now = Local::now();
1339 let end = now + Duration::days(days);
1340 self.fetch_events(now, end, None)
1341 }
1342
1343 pub fn fetch_events(
1345 &self,
1346 start: DateTime<Local>,
1347 end: DateTime<Local>,
1348 calendar_titles: Option<&[&str]>,
1349 ) -> Result<Vec<EventItem>> {
1350 self.ensure_authorized()?;
1351
1352 if start >= end {
1353 return Err(EventKitError::InvalidDateRange);
1354 }
1355
1356 let calendars: Option<Retained<NSArray<EKCalendar>>> = match calendar_titles {
1357 Some(titles) => {
1358 let all_calendars =
1359 unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
1360 let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
1361
1362 for cal in all_calendars.iter() {
1363 let title = unsafe { cal.title() };
1364 let title_str = title.to_string();
1365 if titles.iter().any(|t| *t == title_str) {
1366 matching.push(cal.retain());
1367 }
1368 }
1369
1370 if matching.is_empty() {
1371 return Err(EventKitError::CalendarNotFound(titles.join(", ")));
1372 }
1373
1374 Some(NSArray::from_retained_slice(&matching))
1375 }
1376 None => None,
1377 };
1378
1379 let start_date = datetime_to_nsdate(start);
1380 let end_date = datetime_to_nsdate(end);
1381
1382 let predicate = unsafe {
1383 self.store
1384 .predicateForEventsWithStartDate_endDate_calendars(
1385 &start_date,
1386 &end_date,
1387 calendars.as_deref(),
1388 )
1389 };
1390
1391 let events = unsafe { self.store.eventsMatchingPredicate(&predicate) };
1392
1393 let mut items = Vec::new();
1394 for event in events.iter() {
1395 items.push(event_to_item(&event));
1396 }
1397
1398 items.sort_by(|a, b| a.start_date.cmp(&b.start_date));
1400
1401 Ok(items)
1402 }
1403
1404 #[allow(clippy::too_many_arguments)]
1406 pub fn create_event(
1407 &self,
1408 title: &str,
1409 start: DateTime<Local>,
1410 end: DateTime<Local>,
1411 notes: Option<&str>,
1412 location: Option<&str>,
1413 calendar_title: Option<&str>,
1414 all_day: bool,
1415 ) -> Result<EventItem> {
1416 self.ensure_authorized()?;
1417
1418 let event = unsafe { EKEvent::eventWithEventStore(&self.store) };
1419
1420 let ns_title = NSString::from_str(title);
1422 unsafe { event.setTitle(Some(&ns_title)) };
1423
1424 let start_date = datetime_to_nsdate(start);
1426 let end_date = datetime_to_nsdate(end);
1427 unsafe {
1428 event.setStartDate(Some(&start_date));
1429 event.setEndDate(Some(&end_date));
1430 event.setAllDay(all_day);
1431 }
1432
1433 if let Some(notes_text) = notes {
1435 let ns_notes = NSString::from_str(notes_text);
1436 unsafe { event.setNotes(Some(&ns_notes)) };
1437 }
1438
1439 if let Some(loc) = location {
1441 let ns_location = NSString::from_str(loc);
1442 unsafe { event.setLocation(Some(&ns_location)) };
1443 }
1444
1445 let calendar = if let Some(cal_title) = calendar_title {
1447 self.find_calendar_by_title(cal_title)?
1448 } else {
1449 unsafe { self.store.defaultCalendarForNewEvents() }
1450 .ok_or(EventKitError::NoDefaultCalendar)?
1451 };
1452 unsafe { event.setCalendar(Some(&calendar)) };
1453
1454 unsafe {
1456 self.store
1457 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1458 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1459 }
1460
1461 Ok(event_to_item(&event))
1462 }
1463
1464 pub fn update_event(
1466 &self,
1467 identifier: &str,
1468 title: Option<&str>,
1469 notes: Option<&str>,
1470 location: Option<&str>,
1471 start: Option<DateTime<Local>>,
1472 end: Option<DateTime<Local>>,
1473 ) -> Result<EventItem> {
1474 self.ensure_authorized()?;
1475
1476 let event = self.find_event_by_id(identifier)?;
1477
1478 if let Some(t) = title {
1479 let ns_title = NSString::from_str(t);
1480 unsafe { event.setTitle(Some(&ns_title)) };
1481 }
1482
1483 if let Some(n) = notes {
1484 let ns_notes = NSString::from_str(n);
1485 unsafe { event.setNotes(Some(&ns_notes)) };
1486 }
1487
1488 if let Some(l) = location {
1489 let ns_location = NSString::from_str(l);
1490 unsafe { event.setLocation(Some(&ns_location)) };
1491 }
1492
1493 if let Some(s) = start {
1494 let start_date = datetime_to_nsdate(s);
1495 unsafe { event.setStartDate(Some(&start_date)) };
1496 }
1497
1498 if let Some(e) = end {
1499 let end_date = datetime_to_nsdate(e);
1500 unsafe { event.setEndDate(Some(&end_date)) };
1501 }
1502
1503 unsafe {
1504 self.store
1505 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1506 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1507 }
1508
1509 Ok(event_to_item(&event))
1510 }
1511
1512 pub fn delete_event(&self, identifier: &str, affect_future: bool) -> Result<()> {
1514 self.ensure_authorized()?;
1515
1516 let event = self.find_event_by_id(identifier)?;
1517 let span = if affect_future {
1518 EKSpan::FutureEvents
1519 } else {
1520 EKSpan::ThisEvent
1521 };
1522
1523 unsafe {
1524 self.store
1525 .removeEvent_span_commit_error(&event, span, true)
1526 .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
1527 }
1528
1529 Ok(())
1530 }
1531
1532 pub fn get_event(&self, identifier: &str) -> Result<EventItem> {
1534 self.ensure_authorized()?;
1535 let event = self.find_event_by_id(identifier)?;
1536 Ok(event_to_item(&event))
1537 }
1538
1539 pub fn create_event_calendar(&self, title: &str) -> Result<CalendarInfo> {
1545 self.ensure_authorized()?;
1546 let calendar = unsafe {
1547 EKCalendar::calendarForEntityType_eventStore(EKEntityType::Event, &self.store)
1548 };
1549 let ns_title = NSString::from_str(title);
1550 unsafe { calendar.setTitle(&ns_title) };
1551
1552 if let Some(default_cal) = unsafe { self.store.defaultCalendarForNewEvents() }
1554 && let Some(source) = unsafe { default_cal.source() }
1555 {
1556 unsafe { calendar.setSource(Some(&source)) };
1557 }
1558
1559 unsafe {
1560 self.store
1561 .saveCalendar_commit_error(&calendar, true)
1562 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1563 }
1564 Ok(calendar_to_info(&calendar))
1565 }
1566
1567 pub fn rename_event_calendar(&self, identifier: &str, new_title: &str) -> Result<CalendarInfo> {
1570 self.update_event_calendar(identifier, Some(new_title), None)
1571 }
1572
1573 pub fn update_event_calendar(
1575 &self,
1576 identifier: &str,
1577 new_title: Option<&str>,
1578 color_rgba: Option<(f64, f64, f64, f64)>,
1579 ) -> Result<CalendarInfo> {
1580 self.ensure_authorized()?;
1581 let calendar = unsafe {
1582 self.store
1583 .calendarWithIdentifier(&NSString::from_str(identifier))
1584 }
1585 .ok_or_else(|| EventKitError::CalendarNotFound(identifier.to_string()))?;
1586
1587 if let Some(title) = new_title {
1588 let ns_title = NSString::from_str(title);
1589 unsafe { calendar.setTitle(&ns_title) };
1590 }
1591
1592 if let Some((r, g, b, a)) = color_rgba {
1593 let cg = objc2_core_graphics::CGColor::new_srgb(r, g, b, a);
1594 unsafe { calendar.setCGColor(Some(&cg)) };
1595 }
1596
1597 unsafe {
1598 self.store
1599 .saveCalendar_commit_error(&calendar, true)
1600 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1601 }
1602 Ok(calendar_to_info(&calendar))
1603 }
1604
1605 pub fn delete_event_calendar(&self, identifier: &str) -> Result<()> {
1607 self.ensure_authorized()?;
1608 let calendar = unsafe {
1609 self.store
1610 .calendarWithIdentifier(&NSString::from_str(identifier))
1611 }
1612 .ok_or_else(|| EventKitError::CalendarNotFound(identifier.to_string()))?;
1613
1614 unsafe {
1615 self.store
1616 .removeCalendar_commit_error(&calendar, true)
1617 .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
1618 }
1619 Ok(())
1620 }
1621
1622 pub fn get_event_alarms(&self, identifier: &str) -> Result<Vec<AlarmInfo>> {
1628 self.ensure_authorized()?;
1629 let event = self.find_event_by_id(identifier)?;
1630 Ok(get_item_alarms(&event))
1631 }
1632
1633 pub fn add_event_alarm(&self, identifier: &str, alarm: &AlarmInfo) -> Result<()> {
1635 self.ensure_authorized()?;
1636 let event = self.find_event_by_id(identifier)?;
1637 add_item_alarm(&event, alarm);
1638 unsafe {
1639 self.store
1640 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1641 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1642 }
1643 Ok(())
1644 }
1645
1646 pub fn get_event_recurrence_rules(&self, identifier: &str) -> Result<Vec<RecurrenceRule>> {
1652 self.ensure_authorized()?;
1653 let event = self.find_event_by_id(identifier)?;
1654 Ok(get_item_recurrence_rules(&event))
1655 }
1656
1657 pub fn set_event_recurrence_rule(&self, identifier: &str, rule: &RecurrenceRule) -> Result<()> {
1659 self.ensure_authorized()?;
1660 let event = self.find_event_by_id(identifier)?;
1661 set_item_recurrence_rule(&event, rule);
1662 unsafe {
1663 self.store
1664 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1665 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1666 }
1667 Ok(())
1668 }
1669
1670 pub fn remove_event_recurrence_rules(&self, identifier: &str) -> Result<()> {
1672 self.ensure_authorized()?;
1673 let event = self.find_event_by_id(identifier)?;
1674 clear_item_recurrence_rules(&event);
1675 unsafe {
1676 self.store
1677 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1678 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1679 }
1680 Ok(())
1681 }
1682
1683 pub fn remove_event_alarm(&self, identifier: &str, index: usize) -> Result<()> {
1685 self.ensure_authorized()?;
1686 let event = self.find_event_by_id(identifier)?;
1687 remove_item_alarm(&event, index)?;
1688 unsafe {
1689 self.store
1690 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1691 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1692 }
1693 Ok(())
1694 }
1695
1696 pub fn set_event_url(&self, identifier: &str, url: Option<&str>) -> Result<()> {
1698 self.ensure_authorized()?;
1699 let event = self.find_event_by_id(identifier)?;
1700 set_item_url(&event, url);
1701 unsafe {
1702 self.store
1703 .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1704 .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1705 }
1706 Ok(())
1707 }
1708
1709 fn find_calendar_by_title(&self, title: &str) -> Result<Retained<EKCalendar>> {
1711 let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
1712
1713 for cal in calendars.iter() {
1714 let cal_title = unsafe { cal.title() };
1715 if cal_title.to_string() == title {
1716 return Ok(cal.retain());
1717 }
1718 }
1719
1720 Err(EventKitError::CalendarNotFound(title.to_string()))
1721 }
1722
1723 fn find_event_by_id(&self, identifier: &str) -> Result<Retained<EKEvent>> {
1725 let ns_id = NSString::from_str(identifier);
1726 let event = unsafe { self.store.eventWithIdentifier(&ns_id) };
1727
1728 match event {
1729 Some(e) => Ok(e),
1730 None => Err(EventKitError::ItemNotFound(identifier.to_string())),
1731 }
1732 }
1733}
1734
1735impl Default for EventsManager {
1736 fn default() -> Self {
1737 Self::new()
1738 }
1739}
1740
1741fn event_to_item(event: &EKEvent) -> EventItem {
1743 let identifier = unsafe { event.eventIdentifier() }
1744 .map(|s| s.to_string())
1745 .unwrap_or_default();
1746 let title = unsafe { event.title() }.to_string();
1747 let notes = unsafe { event.notes() }.map(|n| n.to_string());
1748 let location = unsafe { event.location() }.map(|l| l.to_string());
1749 let all_day = unsafe { event.isAllDay() };
1750 let cal = unsafe { event.calendar() };
1751 let calendar_title = cal.as_ref().map(|c| unsafe { c.title() }.to_string());
1752 let calendar_id = cal
1753 .as_ref()
1754 .map(|c| unsafe { c.calendarIdentifier() }.to_string());
1755
1756 let start_ns: Retained<NSDate> = unsafe { event.startDate() };
1757 let end_ns: Retained<NSDate> = unsafe { event.endDate() };
1758 let start_date = nsdate_to_datetime(&start_ns);
1759 let end_date = nsdate_to_datetime(&end_ns);
1760
1761 let url = get_item_url(event);
1762
1763 let avail = unsafe { event.availability() };
1765 let availability = match avail.0 {
1766 0 => EventAvailability::Busy,
1767 1 => EventAvailability::Free,
1768 2 => EventAvailability::Tentative,
1769 3 => EventAvailability::Unavailable,
1770 _ => EventAvailability::NotSupported,
1771 };
1772
1773 let stat = unsafe { event.status() };
1775 let status = match stat.0 {
1776 1 => EventStatus::Confirmed,
1777 2 => EventStatus::Tentative,
1778 3 => EventStatus::Canceled,
1779 _ => EventStatus::None,
1780 };
1781
1782 let is_detached = unsafe { event.isDetached() };
1783 let occurrence_date = unsafe { event.occurrenceDate() }.map(|d| nsdate_to_datetime(&d));
1784
1785 let structured_location = unsafe { event.structuredLocation() }.map(|loc| {
1787 let title = unsafe { loc.title() }
1788 .map(|t| t.to_string())
1789 .unwrap_or_default();
1790 let radius = unsafe { loc.radius() };
1791 let (latitude, longitude) = unsafe { loc.geoLocation() }
1792 .map(|geo| {
1793 let coord = unsafe { geo.coordinate() };
1794 (coord.latitude, coord.longitude)
1795 })
1796 .unwrap_or((0.0, 0.0));
1797 StructuredLocation {
1798 title,
1799 latitude,
1800 longitude,
1801 radius,
1802 }
1803 });
1804
1805 let attendees = get_item_attendees(event);
1807
1808 let organizer = unsafe { event.organizer() }.map(|p| participant_to_info(&p));
1810
1811 EventItem {
1812 identifier,
1813 title,
1814 notes,
1815 location,
1816 start_date,
1817 end_date,
1818 all_day,
1819 calendar_title,
1820 calendar_id,
1821 url,
1822 availability,
1823 status,
1824 is_detached,
1825 occurrence_date,
1826 structured_location,
1827 attendees,
1828 organizer,
1829 }
1830}
1831
1832fn get_item_attendees(item: &EKCalendarItem) -> Vec<ParticipantInfo> {
1834 let attendees = unsafe { item.attendees() };
1835 let Some(attendees) = attendees else {
1836 return Vec::new();
1837 };
1838 let mut result = Vec::new();
1839 for i in 0..attendees.len() {
1840 let p = attendees.objectAtIndex(i);
1841 result.push(participant_to_info(&p));
1842 }
1843 result
1844}
1845
1846fn participant_to_info(p: &objc2_event_kit::EKParticipant) -> ParticipantInfo {
1848 let name = unsafe { p.name() }.map(|n| n.to_string());
1849 let url = unsafe { p.URL() }.absoluteString().map(|s| s.to_string());
1850
1851 let role = unsafe { p.participantRole() };
1853 let role = match role.0 {
1854 1 => ParticipantRole::Required,
1855 2 => ParticipantRole::Optional,
1856 3 => ParticipantRole::Chair,
1857 4 => ParticipantRole::NonParticipant,
1858 _ => ParticipantRole::Unknown,
1859 };
1860
1861 let status = unsafe { p.participantStatus() };
1864 let status = match status.0 {
1865 1 => ParticipantStatus::Pending,
1866 2 => ParticipantStatus::Accepted,
1867 3 => ParticipantStatus::Declined,
1868 4 => ParticipantStatus::Tentative,
1869 5 => ParticipantStatus::Delegated,
1870 6 => ParticipantStatus::Completed,
1871 7 => ParticipantStatus::InProcess,
1872 _ => ParticipantStatus::Unknown,
1873 };
1874
1875 let is_current_user = unsafe { p.isCurrentUser() };
1876
1877 ParticipantInfo {
1878 name,
1879 url,
1880 role,
1881 status,
1882 is_current_user,
1883 }
1884}
1885
1886fn datetime_to_nsdate(dt: DateTime<Local>) -> Retained<NSDate> {
1888 let timestamp = dt.timestamp() as f64;
1889 NSDate::dateWithTimeIntervalSince1970(timestamp)
1890}
1891
1892fn nsdate_to_datetime(date: &NSDate) -> DateTime<Local> {
1894 let timestamp = date.timeIntervalSince1970();
1895 Local.timestamp_opt(timestamp as i64, 0).unwrap()
1896}
1897
1898fn date_components_to_datetime(components: &NSDateComponents) -> Option<DateTime<Local>> {
1900 let calendar = NSCalendar::currentCalendar();
1902
1903 let date = calendar.dateFromComponents(components)?;
1905
1906 Some(nsdate_to_datetime(&date))
1907}
1908
1909fn datetime_to_date_components(dt: DateTime<Local>) -> Retained<NSDateComponents> {
1911 let components = NSDateComponents::new();
1912
1913 components.setYear(dt.year() as isize);
1914 components.setMonth(dt.month() as isize);
1915 components.setDay(dt.day() as isize);
1916 components.setHour(dt.hour() as isize);
1917 components.setMinute(dt.minute() as isize);
1918 components.setSecond(dt.second() as isize);
1919
1920 components
1921}
1922
1923fn get_item_alarms(item: &EKCalendarItem) -> Vec<AlarmInfo> {
1931 let alarms = unsafe { item.alarms() };
1932 let Some(alarms) = alarms else {
1933 return Vec::new();
1934 };
1935 let mut result = Vec::new();
1936 for i in 0..alarms.len() {
1937 let alarm = alarms.objectAtIndex(i);
1938 result.push(alarm_to_info(&alarm));
1939 }
1940 result
1941}
1942
1943fn add_item_alarm(item: &EKCalendarItem, alarm: &AlarmInfo) {
1945 let ek_alarm = create_ek_alarm(alarm);
1946 unsafe { item.addAlarm(&ek_alarm) };
1947}
1948
1949fn remove_item_alarm(item: &EKCalendarItem, index: usize) -> Result<()> {
1951 let alarms = unsafe { item.alarms() };
1952 let Some(alarms) = alarms else {
1953 return Err(EventKitError::ItemNotFound("No alarms on this item".into()));
1954 };
1955 if index >= alarms.len() {
1956 return Err(EventKitError::ItemNotFound(format!(
1957 "Alarm index {} out of range ({})",
1958 index,
1959 alarms.len()
1960 )));
1961 }
1962 let alarm = alarms.objectAtIndex(index);
1963 unsafe { item.removeAlarm(&alarm) };
1964 Ok(())
1965}
1966
1967fn clear_item_alarms(item: &EKCalendarItem) {
1969 unsafe { item.setAlarms(None) };
1970}
1971
1972fn get_item_recurrence_rules(item: &EKCalendarItem) -> Vec<RecurrenceRule> {
1974 let rules = unsafe { item.recurrenceRules() };
1975 let Some(rules) = rules else {
1976 return Vec::new();
1977 };
1978 let mut result = Vec::new();
1979 for i in 0..rules.len() {
1980 let rule = rules.objectAtIndex(i);
1981 result.push(recurrence_rule_to_info(&rule));
1982 }
1983 result
1984}
1985
1986fn set_item_recurrence_rule(item: &EKCalendarItem, rule: &RecurrenceRule) {
1988 let ek_rule = create_ek_recurrence_rule(rule);
1989 unsafe {
1990 let rules = NSArray::from_retained_slice(&[ek_rule]);
1991 item.setRecurrenceRules(Some(&rules));
1992 }
1993}
1994
1995fn clear_item_recurrence_rules(item: &EKCalendarItem) {
1997 unsafe { item.setRecurrenceRules(None) };
1998}
1999
2000fn set_item_url(item: &EKCalendarItem, url: Option<&str>) {
2002 unsafe {
2003 let ns_url = url.map(|u| {
2004 let ns_str = NSString::from_str(u);
2005 objc2_foundation::NSURL::URLWithString(&ns_str).unwrap()
2006 });
2007 item.setURL(ns_url.as_deref());
2008 }
2009}
2010
2011fn get_item_url(item: &EKCalendarItem) -> Option<String> {
2013 unsafe { item.URL() }.map(|u| u.absoluteString().unwrap().to_string())
2014}
2015
2016fn recurrence_rule_to_info(rule: &EKRecurrenceRule) -> RecurrenceRule {
2022 let frequency = unsafe { rule.frequency() };
2023 let frequency = match frequency {
2024 EKRecurrenceFrequency::Daily => RecurrenceFrequency::Daily,
2025 EKRecurrenceFrequency::Weekly => RecurrenceFrequency::Weekly,
2026 EKRecurrenceFrequency::Monthly => RecurrenceFrequency::Monthly,
2027 EKRecurrenceFrequency::Yearly => RecurrenceFrequency::Yearly,
2028 _ => RecurrenceFrequency::Daily,
2029 };
2030
2031 let interval = unsafe { rule.interval() } as usize;
2032
2033 let end = unsafe { rule.recurrenceEnd() }
2034 .map(|end| {
2035 let count = unsafe { end.occurrenceCount() };
2036 if count > 0 {
2037 RecurrenceEndCondition::AfterCount(count)
2038 } else if let Some(date) = unsafe { end.endDate() } {
2039 RecurrenceEndCondition::OnDate(nsdate_to_datetime(&date))
2040 } else {
2041 RecurrenceEndCondition::Never
2042 }
2043 })
2044 .unwrap_or(RecurrenceEndCondition::Never);
2045
2046 let days_of_week = unsafe { rule.daysOfTheWeek() }.map(|days| {
2047 let mut result = Vec::new();
2048 for i in 0..days.len() {
2049 let day = days.objectAtIndex(i);
2050 let weekday = unsafe { day.dayOfTheWeek() };
2051 result.push(weekday.0 as u8);
2052 }
2053 result
2054 });
2055
2056 let days_of_month = unsafe { rule.daysOfTheMonth() }.map(|days| {
2057 let mut result = Vec::new();
2058 for i in 0..days.len() {
2059 let num = days.objectAtIndex(i);
2060 result.push(num.intValue());
2061 }
2062 result
2063 });
2064
2065 RecurrenceRule {
2066 frequency,
2067 interval,
2068 end,
2069 days_of_week,
2070 days_of_month,
2071 }
2072}
2073
2074fn create_ek_recurrence_rule(rule: &RecurrenceRule) -> Retained<EKRecurrenceRule> {
2076 let frequency = match rule.frequency {
2077 RecurrenceFrequency::Daily => EKRecurrenceFrequency::Daily,
2078 RecurrenceFrequency::Weekly => EKRecurrenceFrequency::Weekly,
2079 RecurrenceFrequency::Monthly => EKRecurrenceFrequency::Monthly,
2080 RecurrenceFrequency::Yearly => EKRecurrenceFrequency::Yearly,
2081 };
2082
2083 let end = match &rule.end {
2084 RecurrenceEndCondition::Never => None,
2085 RecurrenceEndCondition::AfterCount(count) => {
2086 Some(unsafe { EKRecurrenceEnd::recurrenceEndWithOccurrenceCount(*count) })
2087 }
2088 RecurrenceEndCondition::OnDate(date) => {
2089 let nsdate = datetime_to_nsdate(*date);
2090 Some(unsafe { EKRecurrenceEnd::recurrenceEndWithEndDate(&nsdate) })
2091 }
2092 };
2093
2094 let days_of_week: Option<Vec<Retained<EKRecurrenceDayOfWeek>>> =
2095 rule.days_of_week.as_ref().map(|days| {
2096 days.iter()
2097 .map(|&d| {
2098 let weekday = EKWeekday(d as isize);
2099 unsafe { EKRecurrenceDayOfWeek::dayOfWeek(weekday) }
2100 })
2101 .collect()
2102 });
2103
2104 let days_of_month: Option<Vec<Retained<NSNumber>>> = rule
2105 .days_of_month
2106 .as_ref()
2107 .map(|days| days.iter().map(|&d| NSNumber::new_i32(d)).collect());
2108
2109 let days_of_week_arr = days_of_week
2110 .as_ref()
2111 .map(|v| NSArray::from_retained_slice(v));
2112 let days_of_month_arr = days_of_month
2113 .as_ref()
2114 .map(|v| NSArray::from_retained_slice(v));
2115
2116 unsafe {
2117 use objc2::AnyThread;
2118 EKRecurrenceRule::initRecurrenceWithFrequency_interval_daysOfTheWeek_daysOfTheMonth_monthsOfTheYear_weeksOfTheYear_daysOfTheYear_setPositions_end(
2119 EKRecurrenceRule::alloc(),
2120 frequency,
2121 rule.interval as isize,
2122 days_of_week_arr.as_deref(),
2123 days_of_month_arr.as_deref(),
2124 None, None, None, None, end.as_deref(),
2129 )
2130 }
2131}
2132
2133fn alarm_to_info(alarm: &EKAlarm) -> AlarmInfo {
2135 let relative_offset = unsafe { alarm.relativeOffset() };
2136 let absolute_date = unsafe { alarm.absoluteDate() }.map(|d| nsdate_to_datetime(&d));
2137
2138 let proximity = unsafe { alarm.proximity() };
2139 let proximity = match proximity {
2140 EKAlarmProximity::Enter => AlarmProximity::Enter,
2141 EKAlarmProximity::Leave => AlarmProximity::Leave,
2142 _ => AlarmProximity::None,
2143 };
2144
2145 let location = unsafe { alarm.structuredLocation() }.map(|loc| {
2146 let title = unsafe { loc.title() }
2147 .map(|t| t.to_string())
2148 .unwrap_or_default();
2149 let radius = unsafe { loc.radius() };
2150 let (latitude, longitude) = unsafe { loc.geoLocation() }
2151 .map(|geo| {
2152 let coord = unsafe { geo.coordinate() };
2153 (coord.latitude, coord.longitude)
2154 })
2155 .unwrap_or((0.0, 0.0));
2156
2157 StructuredLocation {
2158 title,
2159 latitude,
2160 longitude,
2161 radius,
2162 }
2163 });
2164
2165 AlarmInfo {
2166 relative_offset: Some(relative_offset),
2168 absolute_date,
2169 proximity,
2170 location,
2171 }
2172}
2173
2174fn create_ek_alarm(info: &AlarmInfo) -> Retained<EKAlarm> {
2176 let alarm = if let Some(date) = &info.absolute_date {
2177 let nsdate = datetime_to_nsdate(*date);
2178 unsafe { EKAlarm::alarmWithAbsoluteDate(&nsdate) }
2179 } else {
2180 let offset = info.relative_offset.unwrap_or(0.0);
2181 unsafe { EKAlarm::alarmWithRelativeOffset(offset) }
2182 };
2183
2184 let prox = match info.proximity {
2186 AlarmProximity::Enter => EKAlarmProximity::Enter,
2187 AlarmProximity::Leave => EKAlarmProximity::Leave,
2188 AlarmProximity::None => EKAlarmProximity::None,
2189 };
2190 unsafe { alarm.setProximity(prox) };
2191
2192 if let Some(loc) = &info.location {
2194 let title = NSString::from_str(&loc.title);
2195 let structured = unsafe { EKStructuredLocation::locationWithTitle(&title) };
2196 unsafe { structured.setRadius(loc.radius) };
2197
2198 #[cfg(feature = "location")]
2200 {
2201 use objc2::AnyThread;
2202 use objc2_core_location::CLLocation;
2203 let cl_location = unsafe {
2204 CLLocation::initWithLatitude_longitude(
2205 CLLocation::alloc(),
2206 loc.latitude,
2207 loc.longitude,
2208 )
2209 };
2210 unsafe { structured.setGeoLocation(Some(&cl_location)) };
2211 }
2212
2213 unsafe { alarm.setStructuredLocation(Some(&structured)) };
2214 }
2215
2216 alarm
2217}
2218
2219#[cfg(test)]
2220mod tests {
2221 use super::*;
2222
2223 #[test]
2224 fn test_authorization_status_display() {
2225 assert_eq!(
2226 format!("{}", AuthorizationStatus::NotDetermined),
2227 "Not Determined"
2228 );
2229 assert_eq!(
2230 format!("{}", AuthorizationStatus::FullAccess),
2231 "Full Access"
2232 );
2233 }
2234
2235 #[test]
2236 fn test_event_item_debug() {
2237 let event = EventItem {
2238 identifier: "test".to_string(),
2239 title: "Test Event".to_string(),
2240 notes: None,
2241 location: None,
2242 start_date: Local::now(),
2243 end_date: Local::now(),
2244 all_day: false,
2245 calendar_title: None,
2246 calendar_id: None,
2247 url: None,
2248 availability: EventAvailability::Busy,
2249 status: EventStatus::None,
2250 is_detached: false,
2251 occurrence_date: None,
2252 structured_location: None,
2253 attendees: Vec::new(),
2254 organizer: None,
2255 };
2256 assert!(format!("{:?}", event).contains("Test Event"));
2257 }
2258}