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