use block2::RcBlock;
use chrono::{DateTime, Datelike, Duration, Local, TimeZone, Timelike};
use objc2::Message;
use objc2::rc::Retained;
use objc2::runtime::Bool;
use objc2_event_kit::{
EKAlarm, EKAlarmProximity, EKAuthorizationStatus, EKCalendar, EKCalendarItem, EKEntityType,
EKEvent, EKEventStore, EKRecurrenceDayOfWeek, EKRecurrenceEnd, EKRecurrenceFrequency,
EKRecurrenceRule, EKReminder, EKSource, EKSpan, EKStructuredLocation, EKWeekday,
};
use objc2_foundation::{
NSArray, NSCalendar, NSDate, NSDateComponents, NSError, NSNumber, NSString,
};
use std::sync::{Arc, Condvar, Mutex};
use thiserror::Error;
#[cfg(feature = "location")]
#[path = "location.rs"]
pub mod location;
#[cfg(feature = "mcp")]
#[path = "mcp.rs"]
pub mod mcp;
#[derive(Error, Debug)]
pub enum EventKitError {
#[error("Authorization denied")]
AuthorizationDenied,
#[error("Authorization restricted by system policy")]
AuthorizationRestricted,
#[error("Authorization not determined")]
AuthorizationNotDetermined,
#[error("Failed to request authorization: {0}")]
AuthorizationRequestFailed(String),
#[error("No default calendar")]
NoDefaultCalendar,
#[error("Calendar not found: {0}")]
CalendarNotFound(String),
#[error("Item not found: {0}")]
ItemNotFound(String),
#[error("Failed to save: {0}")]
SaveFailed(String),
#[error("Failed to delete: {0}")]
DeleteFailed(String),
#[error("Failed to fetch: {0}")]
FetchFailed(String),
#[error("EventKit error: {0}")]
EventKitError(String),
#[error("Invalid date range")]
InvalidDateRange,
}
pub type RemindersError = EventKitError;
pub type Result<T> = std::result::Result<T, EventKitError>;
#[derive(Debug, Clone)]
pub struct ReminderItem {
pub identifier: String,
pub title: String,
pub notes: Option<String>,
pub completed: bool,
pub priority: usize,
pub calendar_title: Option<String>,
pub calendar_id: Option<String>,
pub due_date: Option<DateTime<Local>>,
pub start_date: Option<DateTime<Local>>,
pub completion_date: Option<DateTime<Local>>,
pub external_identifier: Option<String>,
pub location: Option<String>,
pub url: Option<String>,
pub creation_date: Option<DateTime<Local>>,
pub last_modified_date: Option<DateTime<Local>>,
pub timezone: Option<String>,
pub has_alarms: bool,
pub has_recurrence_rules: bool,
pub has_attendees: bool,
pub has_notes: bool,
pub attendees: Vec<ParticipantInfo>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CalendarType {
Local,
CalDAV,
Exchange,
Subscription,
Birthday,
Unknown,
}
#[derive(Debug, Clone)]
pub struct SourceInfo {
pub identifier: String,
pub title: String,
pub source_type: String,
}
#[derive(Debug, Clone)]
pub struct CalendarInfo {
pub identifier: String,
pub title: String,
pub source: Option<String>,
pub source_id: Option<String>,
pub calendar_type: CalendarType,
pub allows_modifications: bool,
pub is_immutable: bool,
pub is_subscribed: bool,
pub color: Option<(f64, f64, f64, f64)>,
pub allowed_entity_types: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlarmProximity {
None,
Enter,
Leave,
}
#[derive(Debug, Clone)]
pub struct StructuredLocation {
pub title: String,
pub latitude: f64,
pub longitude: f64,
pub radius: f64,
}
#[derive(Debug, Clone)]
pub struct AlarmInfo {
pub relative_offset: Option<f64>,
pub absolute_date: Option<DateTime<Local>>,
pub proximity: AlarmProximity,
pub location: Option<StructuredLocation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecurrenceFrequency {
Daily,
Weekly,
Monthly,
Yearly,
}
#[derive(Debug, Clone)]
pub enum RecurrenceEndCondition {
Never,
AfterCount(usize),
OnDate(DateTime<Local>),
}
#[derive(Debug, Clone)]
pub struct RecurrenceRule {
pub frequency: RecurrenceFrequency,
pub interval: usize,
pub end: RecurrenceEndCondition,
pub days_of_week: Option<Vec<u8>>,
pub days_of_month: Option<Vec<i32>>,
}
pub struct RemindersManager {
store: Retained<EKEventStore>,
}
impl RemindersManager {
pub fn new() -> Self {
let store = unsafe { EKEventStore::new() };
Self { store }
}
pub fn authorization_status() -> AuthorizationStatus {
let status =
unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Reminder) };
status.into()
}
pub fn request_access(&self) -> Result<bool> {
let result = Arc::new((Mutex::new(None::<(bool, Option<String>)>), Condvar::new()));
let result_clone = Arc::clone(&result);
let completion = RcBlock::new(move |granted: Bool, error: *mut NSError| {
let error_msg = if !error.is_null() {
let error_ref = unsafe { &*error };
Some(format!("{:?}", error_ref))
} else {
None
};
let (lock, cvar) = &*result_clone;
let mut res = lock.lock().unwrap();
*res = Some((granted.as_bool(), error_msg));
cvar.notify_one();
});
unsafe {
let block_ptr = &*completion as *const _ as *mut _;
self.store
.requestFullAccessToRemindersWithCompletion(block_ptr);
}
let (lock, cvar) = &*result;
let mut res = lock.lock().unwrap();
while res.is_none() {
res = cvar.wait(res).unwrap();
}
match res.take() {
Some((granted, None)) => Ok(granted),
Some((_, Some(error))) => Err(RemindersError::AuthorizationRequestFailed(error)),
None => Err(RemindersError::AuthorizationRequestFailed(
"Unknown error".to_string(),
)),
}
}
pub fn ensure_authorized(&self) -> Result<()> {
match Self::authorization_status() {
AuthorizationStatus::FullAccess => Ok(()),
AuthorizationStatus::NotDetermined => {
if self.request_access()? {
Ok(())
} else {
Err(RemindersError::AuthorizationDenied)
}
}
AuthorizationStatus::Denied => Err(RemindersError::AuthorizationDenied),
AuthorizationStatus::Restricted => Err(RemindersError::AuthorizationRestricted),
AuthorizationStatus::WriteOnly => Ok(()), }
}
pub fn list_calendars(&self) -> Result<Vec<CalendarInfo>> {
self.ensure_authorized()?;
let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
let mut result = Vec::new();
for calendar in calendars.iter() {
result.push(calendar_to_info(&calendar));
}
Ok(result)
}
pub fn list_sources(&self) -> Result<Vec<SourceInfo>> {
self.ensure_authorized()?;
let sources = unsafe { self.store.sources() };
let mut result = Vec::new();
for source in sources.iter() {
result.push(source_to_info(&source));
}
Ok(result)
}
pub fn default_calendar(&self) -> Result<CalendarInfo> {
self.ensure_authorized()?;
let calendar = unsafe { self.store.defaultCalendarForNewReminders() };
match calendar {
Some(cal) => Ok(calendar_to_info(&cal)),
None => Err(RemindersError::NoDefaultCalendar),
}
}
pub fn fetch_all_reminders(&self) -> Result<Vec<ReminderItem>> {
self.fetch_reminders(None)
}
pub fn fetch_reminders(&self, calendar_titles: Option<&[&str]>) -> Result<Vec<ReminderItem>> {
self.ensure_authorized()?;
let calendars: Option<Retained<NSArray<EKCalendar>>> = match calendar_titles {
Some(titles) => {
let all_calendars =
unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
for cal in all_calendars.iter() {
let title = unsafe { cal.title() };
let title_str = title.to_string();
if titles.iter().any(|t| *t == title_str) {
matching.push(cal.retain());
}
}
if matching.is_empty() {
return Err(RemindersError::CalendarNotFound(titles.join(", ")));
}
Some(NSArray::from_retained_slice(&matching))
}
None => None,
};
let predicate = unsafe {
self.store
.predicateForRemindersInCalendars(calendars.as_deref())
};
let result = Arc::new((Mutex::new(None::<Vec<ReminderItem>>), Condvar::new()));
let result_clone = Arc::clone(&result);
let completion = RcBlock::new(move |reminders: *mut NSArray<EKReminder>| {
let items = if reminders.is_null() {
Vec::new()
} else {
let reminders = unsafe { Retained::retain(reminders).unwrap() };
reminders.iter().map(|r| reminder_to_item(&r)).collect()
};
let (lock, cvar) = &*result_clone;
let mut guard = lock.lock().unwrap();
*guard = Some(items);
cvar.notify_one();
});
unsafe {
self.store
.fetchRemindersMatchingPredicate_completion(&predicate, &completion);
}
let (lock, cvar) = &*result;
let mut guard = lock.lock().unwrap();
while guard.is_none() {
guard = cvar.wait(guard).unwrap();
}
guard
.take()
.ok_or_else(|| RemindersError::FetchFailed("Unknown error".to_string()))
}
pub fn fetch_incomplete_reminders(&self) -> Result<Vec<ReminderItem>> {
self.ensure_authorized()?;
let predicate = unsafe {
self.store
.predicateForIncompleteRemindersWithDueDateStarting_ending_calendars(
None, None, None,
)
};
let result = Arc::new((Mutex::new(None::<Vec<ReminderItem>>), Condvar::new()));
let result_clone = Arc::clone(&result);
let completion = RcBlock::new(move |reminders: *mut NSArray<EKReminder>| {
let items = if reminders.is_null() {
Vec::new()
} else {
let reminders = unsafe { Retained::retain(reminders).unwrap() };
reminders.iter().map(|r| reminder_to_item(&r)).collect()
};
let (lock, cvar) = &*result_clone;
let mut guard = lock.lock().unwrap();
*guard = Some(items);
cvar.notify_one();
});
unsafe {
self.store
.fetchRemindersMatchingPredicate_completion(&predicate, &completion);
}
let (lock, cvar) = &*result;
let mut guard = lock.lock().unwrap();
while guard.is_none() {
guard = cvar.wait(guard).unwrap();
}
guard
.take()
.ok_or_else(|| RemindersError::FetchFailed("Unknown error".to_string()))
}
#[allow(clippy::too_many_arguments)]
pub fn create_reminder(
&self,
title: &str,
notes: Option<&str>,
calendar_title: Option<&str>,
priority: Option<usize>,
due_date: Option<DateTime<Local>>,
start_date: Option<DateTime<Local>>,
) -> Result<ReminderItem> {
self.ensure_authorized()?;
let reminder = unsafe { EKReminder::reminderWithEventStore(&self.store) };
let ns_title = NSString::from_str(title);
unsafe { reminder.setTitle(Some(&ns_title)) };
if let Some(notes_text) = notes {
let ns_notes = NSString::from_str(notes_text);
unsafe { reminder.setNotes(Some(&ns_notes)) };
}
if let Some(p) = priority {
unsafe { reminder.setPriority(p) };
}
if let Some(due) = due_date {
let components = datetime_to_date_components(due);
unsafe { reminder.setDueDateComponents(Some(&components)) };
}
if let Some(start) = start_date {
let components = datetime_to_date_components(start);
unsafe { reminder.setStartDateComponents(Some(&components)) };
}
let calendar = if let Some(cal_title) = calendar_title {
self.find_calendar_by_title(cal_title)?
} else {
unsafe { self.store.defaultCalendarForNewReminders() }
.ok_or(RemindersError::NoDefaultCalendar)?
};
unsafe { reminder.setCalendar(Some(&calendar)) };
unsafe {
self.store
.saveReminder_commit_error(&reminder, true)
.map_err(|e| RemindersError::SaveFailed(format!("{:?}", e)))?;
}
Ok(reminder_to_item(&reminder))
}
#[allow(clippy::too_many_arguments)]
pub fn update_reminder(
&self,
identifier: &str,
title: Option<&str>,
notes: Option<&str>,
completed: Option<bool>,
priority: Option<usize>,
due_date: Option<Option<DateTime<Local>>>,
start_date: Option<Option<DateTime<Local>>>,
calendar_title: Option<&str>,
) -> Result<ReminderItem> {
self.ensure_authorized()?;
let reminder = self.find_reminder_by_id(identifier)?;
if let Some(t) = title {
let ns_title = NSString::from_str(t);
unsafe { reminder.setTitle(Some(&ns_title)) };
}
if let Some(n) = notes {
let ns_notes = NSString::from_str(n);
unsafe { reminder.setNotes(Some(&ns_notes)) };
}
if let Some(c) = completed {
unsafe { reminder.setCompleted(c) };
}
if let Some(p) = priority {
unsafe { reminder.setPriority(p) };
}
if let Some(due_opt) = due_date {
match due_opt {
Some(due) => {
let components = datetime_to_date_components(due);
unsafe { reminder.setDueDateComponents(Some(&components)) };
}
None => {
unsafe { reminder.setDueDateComponents(None) };
}
}
}
if let Some(start_opt) = start_date {
match start_opt {
Some(start) => {
let components = datetime_to_date_components(start);
unsafe { reminder.setStartDateComponents(Some(&components)) };
}
None => {
unsafe { reminder.setStartDateComponents(None) };
}
}
}
if let Some(cal_title) = calendar_title {
let calendar = self.find_calendar_by_title(cal_title)?;
unsafe { reminder.setCalendar(Some(&calendar)) };
}
unsafe {
self.store
.saveReminder_commit_error(&reminder, true)
.map_err(|e| RemindersError::SaveFailed(format!("{:?}", e)))?;
}
Ok(reminder_to_item(&reminder))
}
pub fn complete_reminder(&self, identifier: &str) -> Result<ReminderItem> {
self.update_reminder(identifier, None, None, Some(true), None, None, None, None)
}
pub fn uncomplete_reminder(&self, identifier: &str) -> Result<ReminderItem> {
self.update_reminder(identifier, None, None, Some(false), None, None, None, None)
}
pub fn delete_reminder(&self, identifier: &str) -> Result<()> {
self.ensure_authorized()?;
let reminder = self.find_reminder_by_id(identifier)?;
unsafe {
self.store
.removeReminder_commit_error(&reminder, true)
.map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn get_reminder(&self, identifier: &str) -> Result<ReminderItem> {
self.ensure_authorized()?;
let reminder = self.find_reminder_by_id(identifier)?;
Ok(reminder_to_item(&reminder))
}
pub fn get_alarms(&self, identifier: &str) -> Result<Vec<AlarmInfo>> {
self.ensure_authorized()?;
let reminder = self.find_reminder_by_id(identifier)?;
Ok(get_item_alarms(&reminder))
}
pub fn add_alarm(&self, identifier: &str, alarm: &AlarmInfo) -> Result<()> {
self.ensure_authorized()?;
let reminder = self.find_reminder_by_id(identifier)?;
add_item_alarm(&reminder, alarm);
unsafe {
self.store
.saveReminder_commit_error(&reminder, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn remove_all_alarms(&self, identifier: &str) -> Result<()> {
self.ensure_authorized()?;
let reminder = self.find_reminder_by_id(identifier)?;
clear_item_alarms(&reminder);
unsafe {
self.store
.saveReminder_commit_error(&reminder, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn remove_alarm(&self, identifier: &str, index: usize) -> Result<()> {
self.ensure_authorized()?;
let reminder = self.find_reminder_by_id(identifier)?;
remove_item_alarm(&reminder, index)?;
unsafe {
self.store
.saveReminder_commit_error(&reminder, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn set_url(&self, identifier: &str, url: Option<&str>) -> Result<()> {
self.ensure_authorized()?;
let reminder = self.find_reminder_by_id(identifier)?;
set_item_url(&reminder, url);
unsafe {
self.store
.saveReminder_commit_error(&reminder, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn get_recurrence_rules(&self, identifier: &str) -> Result<Vec<RecurrenceRule>> {
self.ensure_authorized()?;
let reminder = self.find_reminder_by_id(identifier)?;
Ok(get_item_recurrence_rules(&reminder))
}
pub fn set_recurrence_rule(&self, identifier: &str, rule: &RecurrenceRule) -> Result<()> {
self.ensure_authorized()?;
let reminder = self.find_reminder_by_id(identifier)?;
set_item_recurrence_rule(&reminder, rule);
unsafe {
self.store
.saveReminder_commit_error(&reminder, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn remove_recurrence_rules(&self, identifier: &str) -> Result<()> {
self.ensure_authorized()?;
let reminder = self.find_reminder_by_id(identifier)?;
clear_item_recurrence_rules(&reminder);
unsafe {
self.store
.saveReminder_commit_error(&reminder, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn create_calendar(&self, title: &str) -> Result<CalendarInfo> {
self.ensure_authorized()?;
let calendar = unsafe {
EKCalendar::calendarForEntityType_eventStore(EKEntityType::Reminder, &self.store)
};
let ns_title = NSString::from_str(title);
unsafe { calendar.setTitle(&ns_title) };
let source = self.find_best_source_for_reminders()?;
unsafe { calendar.setSource(Some(&source)) };
unsafe {
self.store
.saveCalendar_commit_error(&calendar, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(calendar_to_info(&calendar))
}
pub fn rename_calendar(&self, identifier: &str, new_title: &str) -> Result<CalendarInfo> {
self.update_calendar(identifier, Some(new_title), None)
}
pub fn update_calendar(
&self,
identifier: &str,
new_title: Option<&str>,
color_rgba: Option<(f64, f64, f64, f64)>,
) -> Result<CalendarInfo> {
self.ensure_authorized()?;
let calendar = self.find_calendar_by_id(identifier)?;
if !unsafe { calendar.allowsContentModifications() } {
return Err(EventKitError::SaveFailed(
"Calendar does not allow modifications".to_string(),
));
}
if let Some(title) = new_title {
let ns_title = NSString::from_str(title);
unsafe { calendar.setTitle(&ns_title) };
}
if let Some((r, g, b, a)) = color_rgba {
let cg = objc2_core_graphics::CGColor::new_srgb(r, g, b, a);
unsafe { calendar.setCGColor(Some(&cg)) };
}
unsafe {
self.store
.saveCalendar_commit_error(&calendar, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(calendar_to_info(&calendar))
}
pub fn delete_calendar(&self, identifier: &str) -> Result<()> {
self.ensure_authorized()?;
let calendar = self.find_calendar_by_id(identifier)?;
if !unsafe { calendar.allowsContentModifications() } {
return Err(EventKitError::DeleteFailed(
"Calendar does not allow modifications".to_string(),
));
}
unsafe {
self.store
.removeCalendar_commit_error(&calendar, true)
.map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn get_calendar(&self, identifier: &str) -> Result<CalendarInfo> {
self.ensure_authorized()?;
let calendar = self.find_calendar_by_id(identifier)?;
Ok(calendar_to_info(&calendar))
}
fn find_best_source_for_reminders(&self) -> Result<Retained<objc2_event_kit::EKSource>> {
if let Some(default_cal) = unsafe { self.store.defaultCalendarForNewReminders() }
&& let Some(source) = unsafe { default_cal.source() }
{
return Ok(source);
}
let sources = unsafe { self.store.sources() };
for source in sources.iter() {
let calendars = unsafe { source.calendarsForEntityType(EKEntityType::Reminder) };
if !calendars.is_empty() {
return Ok(source.retain());
}
}
Err(EventKitError::SaveFailed(
"No suitable source found for creating reminder calendar".to_string(),
))
}
fn find_calendar_by_id(&self, identifier: &str) -> Result<Retained<EKCalendar>> {
let ns_id = NSString::from_str(identifier);
let calendar = unsafe { self.store.calendarWithIdentifier(&ns_id) };
match calendar {
Some(cal) => Ok(cal),
None => Err(EventKitError::CalendarNotFound(identifier.to_string())),
}
}
fn find_calendar_by_title(&self, title: &str) -> Result<Retained<EKCalendar>> {
let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
for cal in calendars.iter() {
let cal_title = unsafe { cal.title() };
if cal_title.to_string() == title {
return Ok(cal.retain());
}
}
Err(RemindersError::CalendarNotFound(title.to_string()))
}
fn find_reminder_by_id(&self, identifier: &str) -> Result<Retained<EKReminder>> {
let ns_id = NSString::from_str(identifier);
let item = unsafe { self.store.calendarItemWithIdentifier(&ns_id) };
match item {
Some(item) => {
if let Some(reminder) = item.downcast_ref::<EKReminder>() {
Ok(reminder.retain())
} else {
Err(EventKitError::ItemNotFound(identifier.to_string()))
}
}
None => Err(EventKitError::ItemNotFound(identifier.to_string())),
}
}
}
impl Default for RemindersManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthorizationStatus {
NotDetermined,
Restricted,
Denied,
FullAccess,
WriteOnly,
}
impl From<EKAuthorizationStatus> for AuthorizationStatus {
fn from(status: EKAuthorizationStatus) -> Self {
if status == EKAuthorizationStatus::NotDetermined {
AuthorizationStatus::NotDetermined
} else if status == EKAuthorizationStatus::Restricted {
AuthorizationStatus::Restricted
} else if status == EKAuthorizationStatus::Denied {
AuthorizationStatus::Denied
} else if status == EKAuthorizationStatus::FullAccess {
AuthorizationStatus::FullAccess
} else if status == EKAuthorizationStatus::WriteOnly {
AuthorizationStatus::WriteOnly
} else {
AuthorizationStatus::NotDetermined
}
}
}
impl std::fmt::Display for AuthorizationStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthorizationStatus::NotDetermined => write!(f, "Not Determined"),
AuthorizationStatus::Restricted => write!(f, "Restricted"),
AuthorizationStatus::Denied => write!(f, "Denied"),
AuthorizationStatus::FullAccess => write!(f, "Full Access"),
AuthorizationStatus::WriteOnly => write!(f, "Write Only"),
}
}
}
fn reminder_to_item(reminder: &EKReminder) -> ReminderItem {
let identifier = unsafe { reminder.calendarItemIdentifier() }.to_string();
let title = unsafe { reminder.title() }.to_string();
let notes = unsafe { reminder.notes() }.map(|n| n.to_string());
let completed = unsafe { reminder.isCompleted() };
let priority = unsafe { reminder.priority() };
let cal = unsafe { reminder.calendar() };
let calendar_title = cal.as_ref().map(|c| unsafe { c.title() }.to_string());
let calendar_id = cal
.as_ref()
.map(|c| unsafe { c.calendarIdentifier() }.to_string());
let due_date = unsafe { reminder.dueDateComponents() }
.and_then(|components| date_components_to_datetime(&components));
let start_date = unsafe { reminder.startDateComponents() }
.and_then(|components| date_components_to_datetime(&components));
let completion_date =
unsafe { reminder.completionDate() }.map(|date| nsdate_to_datetime(&date));
let external_identifier =
unsafe { reminder.calendarItemExternalIdentifier() }.map(|id| id.to_string());
let location = unsafe { reminder.location() }.map(|loc| loc.to_string());
let url = unsafe { reminder.URL() }
.as_ref()
.and_then(|url_ref| url_ref.absoluteString())
.map(|abs_str| abs_str.to_string());
let creation_date = unsafe { reminder.creationDate() }.map(|date| nsdate_to_datetime(&date));
let last_modified_date =
unsafe { reminder.lastModifiedDate() }.map(|date| nsdate_to_datetime(&date));
let timezone = unsafe { reminder.timeZone() }.map(|tz| tz.name().to_string());
let has_alarms = unsafe { reminder.hasAlarms() };
let has_recurrence_rules = unsafe { reminder.hasRecurrenceRules() };
let has_attendees = unsafe { reminder.hasAttendees() };
let has_notes = unsafe { reminder.hasNotes() };
ReminderItem {
identifier,
title,
notes,
completed,
priority,
calendar_title,
calendar_id,
due_date,
start_date,
completion_date,
external_identifier,
location,
url,
creation_date,
last_modified_date,
timezone,
has_alarms,
has_recurrence_rules,
has_attendees,
has_notes,
attendees: get_item_attendees(reminder),
}
}
fn source_to_info(source: &EKSource) -> SourceInfo {
let identifier = unsafe { source.sourceIdentifier() }.to_string();
let title = unsafe { source.title() }.to_string();
let source_type = unsafe { source.sourceType() };
let source_type = match source_type.0 {
0 => "local",
1 => "exchange",
2 => "caldav",
3 => "mobileme",
4 => "subscribed",
5 => "birthdays",
_ => "unknown",
}
.to_string();
SourceInfo {
identifier,
title,
source_type,
}
}
fn calendar_to_info(calendar: &EKCalendar) -> CalendarInfo {
let identifier = unsafe { calendar.calendarIdentifier() }.to_string();
let title = unsafe { calendar.title() }.to_string();
let source = unsafe { calendar.source() }.map(|s| unsafe { s.title() }.to_string());
let source_id =
unsafe { calendar.source() }.map(|s| unsafe { s.sourceIdentifier() }.to_string());
let allows_modifications = unsafe { calendar.allowsContentModifications() };
let is_immutable = unsafe { calendar.isImmutable() };
let is_subscribed = unsafe { calendar.isSubscribed() };
let cal_type = unsafe { calendar.r#type() };
let calendar_type = match cal_type.0 {
0 => CalendarType::Local,
1 => CalendarType::CalDAV,
2 => CalendarType::Exchange,
3 => CalendarType::Subscription,
4 => CalendarType::Birthday,
_ => CalendarType::Unknown,
};
let color: Option<(f64, f64, f64, f64)> = unsafe {
calendar.CGColor().and_then(|cg| {
use objc2_core_graphics::CGColor as CG;
let n = CG::number_of_components(Some(&cg));
if n >= 3 {
let ptr = CG::components(Some(&cg));
let r = *ptr;
let g = *ptr.add(1);
let b = *ptr.add(2);
let a = if n >= 4 { *ptr.add(3) } else { 1.0 };
Some((r, g, b, a))
} else {
None
}
})
};
let entity_mask = unsafe { calendar.allowedEntityTypes() };
let mut allowed_entity_types = Vec::new();
if entity_mask.0 & 1 != 0 {
allowed_entity_types.push("event".to_string());
}
if entity_mask.0 & 2 != 0 {
allowed_entity_types.push("reminder".to_string());
}
CalendarInfo {
identifier,
title,
source,
source_id,
calendar_type,
allows_modifications,
is_immutable,
is_subscribed,
color,
allowed_entity_types,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventAvailability {
NotSupported,
Busy,
Free,
Tentative,
Unavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventStatus {
None,
Confirmed,
Tentative,
Canceled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParticipantRole {
Unknown,
Required,
Optional,
Chair,
NonParticipant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParticipantStatus {
Unknown,
Pending,
Accepted,
Declined,
Tentative,
Delegated,
Completed,
InProcess,
}
#[derive(Debug, Clone)]
pub struct ParticipantInfo {
pub name: Option<String>,
pub url: Option<String>,
pub role: ParticipantRole,
pub status: ParticipantStatus,
pub is_current_user: bool,
}
#[derive(Debug, Clone)]
pub struct EventItem {
pub identifier: String,
pub title: String,
pub notes: Option<String>,
pub location: Option<String>,
pub start_date: DateTime<Local>,
pub end_date: DateTime<Local>,
pub all_day: bool,
pub calendar_title: Option<String>,
pub calendar_id: Option<String>,
pub url: Option<String>,
pub availability: EventAvailability,
pub status: EventStatus,
pub is_detached: bool,
pub occurrence_date: Option<DateTime<Local>>,
pub structured_location: Option<StructuredLocation>,
pub attendees: Vec<ParticipantInfo>,
pub organizer: Option<ParticipantInfo>,
}
pub struct EventsManager {
store: Retained<EKEventStore>,
}
impl EventsManager {
pub fn new() -> Self {
let store = unsafe { EKEventStore::new() };
Self { store }
}
pub fn authorization_status() -> AuthorizationStatus {
let status = unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Event) };
status.into()
}
pub fn request_access(&self) -> Result<bool> {
let result = Arc::new((Mutex::new(None::<(bool, Option<String>)>), Condvar::new()));
let result_clone = Arc::clone(&result);
let completion = RcBlock::new(move |granted: Bool, error: *mut NSError| {
let error_msg = if !error.is_null() {
let error_ref = unsafe { &*error };
Some(format!("{:?}", error_ref))
} else {
None
};
let (lock, cvar) = &*result_clone;
let mut res = lock.lock().unwrap();
*res = Some((granted.as_bool(), error_msg));
cvar.notify_one();
});
unsafe {
let block_ptr = &*completion as *const _ as *mut _;
self.store
.requestFullAccessToEventsWithCompletion(block_ptr);
}
let (lock, cvar) = &*result;
let mut res = lock.lock().unwrap();
while res.is_none() {
res = cvar.wait(res).unwrap();
}
match res.take() {
Some((granted, None)) => Ok(granted),
Some((_, Some(error))) => Err(EventKitError::AuthorizationRequestFailed(error)),
None => Err(EventKitError::AuthorizationRequestFailed(
"Unknown error".to_string(),
)),
}
}
pub fn ensure_authorized(&self) -> Result<()> {
match Self::authorization_status() {
AuthorizationStatus::FullAccess => Ok(()),
AuthorizationStatus::NotDetermined => {
if self.request_access()? {
Ok(())
} else {
Err(EventKitError::AuthorizationDenied)
}
}
AuthorizationStatus::Denied => Err(EventKitError::AuthorizationDenied),
AuthorizationStatus::Restricted => Err(EventKitError::AuthorizationRestricted),
AuthorizationStatus::WriteOnly => Ok(()),
}
}
pub fn list_calendars(&self) -> Result<Vec<CalendarInfo>> {
self.ensure_authorized()?;
let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
let mut result = Vec::new();
for calendar in calendars.iter() {
result.push(calendar_to_info(&calendar));
}
Ok(result)
}
pub fn default_calendar(&self) -> Result<CalendarInfo> {
self.ensure_authorized()?;
let calendar = unsafe { self.store.defaultCalendarForNewEvents() };
match calendar {
Some(cal) => Ok(calendar_to_info(&cal)),
None => Err(EventKitError::NoDefaultCalendar),
}
}
pub fn fetch_today_events(&self) -> Result<Vec<EventItem>> {
let now = Local::now();
let start = now.date_naive().and_hms_opt(0, 0, 0).unwrap();
let end = now.date_naive().and_hms_opt(23, 59, 59).unwrap();
self.fetch_events(
Local.from_local_datetime(&start).unwrap(),
Local.from_local_datetime(&end).unwrap(),
None,
)
}
pub fn fetch_upcoming_events(&self, days: i64) -> Result<Vec<EventItem>> {
let now = Local::now();
let end = now + Duration::days(days);
self.fetch_events(now, end, None)
}
pub fn fetch_events(
&self,
start: DateTime<Local>,
end: DateTime<Local>,
calendar_titles: Option<&[&str]>,
) -> Result<Vec<EventItem>> {
self.ensure_authorized()?;
if start >= end {
return Err(EventKitError::InvalidDateRange);
}
let calendars: Option<Retained<NSArray<EKCalendar>>> = match calendar_titles {
Some(titles) => {
let all_calendars =
unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
for cal in all_calendars.iter() {
let title = unsafe { cal.title() };
let title_str = title.to_string();
if titles.iter().any(|t| *t == title_str) {
matching.push(cal.retain());
}
}
if matching.is_empty() {
return Err(EventKitError::CalendarNotFound(titles.join(", ")));
}
Some(NSArray::from_retained_slice(&matching))
}
None => None,
};
let start_date = datetime_to_nsdate(start);
let end_date = datetime_to_nsdate(end);
let predicate = unsafe {
self.store
.predicateForEventsWithStartDate_endDate_calendars(
&start_date,
&end_date,
calendars.as_deref(),
)
};
let events = unsafe { self.store.eventsMatchingPredicate(&predicate) };
let mut items = Vec::new();
for event in events.iter() {
items.push(event_to_item(&event));
}
items.sort_by(|a, b| a.start_date.cmp(&b.start_date));
Ok(items)
}
#[allow(clippy::too_many_arguments)]
pub fn create_event(
&self,
title: &str,
start: DateTime<Local>,
end: DateTime<Local>,
notes: Option<&str>,
location: Option<&str>,
calendar_title: Option<&str>,
all_day: bool,
) -> Result<EventItem> {
self.ensure_authorized()?;
let event = unsafe { EKEvent::eventWithEventStore(&self.store) };
let ns_title = NSString::from_str(title);
unsafe { event.setTitle(Some(&ns_title)) };
let start_date = datetime_to_nsdate(start);
let end_date = datetime_to_nsdate(end);
unsafe {
event.setStartDate(Some(&start_date));
event.setEndDate(Some(&end_date));
event.setAllDay(all_day);
}
if let Some(notes_text) = notes {
let ns_notes = NSString::from_str(notes_text);
unsafe { event.setNotes(Some(&ns_notes)) };
}
if let Some(loc) = location {
let ns_location = NSString::from_str(loc);
unsafe { event.setLocation(Some(&ns_location)) };
}
let calendar = if let Some(cal_title) = calendar_title {
self.find_calendar_by_title(cal_title)?
} else {
unsafe { self.store.defaultCalendarForNewEvents() }
.ok_or(EventKitError::NoDefaultCalendar)?
};
unsafe { event.setCalendar(Some(&calendar)) };
unsafe {
self.store
.saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(event_to_item(&event))
}
pub fn update_event(
&self,
identifier: &str,
title: Option<&str>,
notes: Option<&str>,
location: Option<&str>,
start: Option<DateTime<Local>>,
end: Option<DateTime<Local>>,
) -> Result<EventItem> {
self.ensure_authorized()?;
let event = self.find_event_by_id(identifier)?;
if let Some(t) = title {
let ns_title = NSString::from_str(t);
unsafe { event.setTitle(Some(&ns_title)) };
}
if let Some(n) = notes {
let ns_notes = NSString::from_str(n);
unsafe { event.setNotes(Some(&ns_notes)) };
}
if let Some(l) = location {
let ns_location = NSString::from_str(l);
unsafe { event.setLocation(Some(&ns_location)) };
}
if let Some(s) = start {
let start_date = datetime_to_nsdate(s);
unsafe { event.setStartDate(Some(&start_date)) };
}
if let Some(e) = end {
let end_date = datetime_to_nsdate(e);
unsafe { event.setEndDate(Some(&end_date)) };
}
unsafe {
self.store
.saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(event_to_item(&event))
}
pub fn delete_event(&self, identifier: &str, affect_future: bool) -> Result<()> {
self.ensure_authorized()?;
let event = self.find_event_by_id(identifier)?;
let span = if affect_future {
EKSpan::FutureEvents
} else {
EKSpan::ThisEvent
};
unsafe {
self.store
.removeEvent_span_commit_error(&event, span, true)
.map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn get_event(&self, identifier: &str) -> Result<EventItem> {
self.ensure_authorized()?;
let event = self.find_event_by_id(identifier)?;
Ok(event_to_item(&event))
}
pub fn create_event_calendar(&self, title: &str) -> Result<CalendarInfo> {
self.ensure_authorized()?;
let calendar = unsafe {
EKCalendar::calendarForEntityType_eventStore(EKEntityType::Event, &self.store)
};
let ns_title = NSString::from_str(title);
unsafe { calendar.setTitle(&ns_title) };
if let Some(default_cal) = unsafe { self.store.defaultCalendarForNewEvents() }
&& let Some(source) = unsafe { default_cal.source() }
{
unsafe { calendar.setSource(Some(&source)) };
}
unsafe {
self.store
.saveCalendar_commit_error(&calendar, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(calendar_to_info(&calendar))
}
pub fn rename_event_calendar(&self, identifier: &str, new_title: &str) -> Result<CalendarInfo> {
self.update_event_calendar(identifier, Some(new_title), None)
}
pub fn update_event_calendar(
&self,
identifier: &str,
new_title: Option<&str>,
color_rgba: Option<(f64, f64, f64, f64)>,
) -> Result<CalendarInfo> {
self.ensure_authorized()?;
let calendar = unsafe {
self.store
.calendarWithIdentifier(&NSString::from_str(identifier))
}
.ok_or_else(|| EventKitError::CalendarNotFound(identifier.to_string()))?;
if let Some(title) = new_title {
let ns_title = NSString::from_str(title);
unsafe { calendar.setTitle(&ns_title) };
}
if let Some((r, g, b, a)) = color_rgba {
let cg = objc2_core_graphics::CGColor::new_srgb(r, g, b, a);
unsafe { calendar.setCGColor(Some(&cg)) };
}
unsafe {
self.store
.saveCalendar_commit_error(&calendar, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(calendar_to_info(&calendar))
}
pub fn delete_event_calendar(&self, identifier: &str) -> Result<()> {
self.ensure_authorized()?;
let calendar = unsafe {
self.store
.calendarWithIdentifier(&NSString::from_str(identifier))
}
.ok_or_else(|| EventKitError::CalendarNotFound(identifier.to_string()))?;
unsafe {
self.store
.removeCalendar_commit_error(&calendar, true)
.map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn get_event_alarms(&self, identifier: &str) -> Result<Vec<AlarmInfo>> {
self.ensure_authorized()?;
let event = self.find_event_by_id(identifier)?;
Ok(get_item_alarms(&event))
}
pub fn add_event_alarm(&self, identifier: &str, alarm: &AlarmInfo) -> Result<()> {
self.ensure_authorized()?;
let event = self.find_event_by_id(identifier)?;
add_item_alarm(&event, alarm);
unsafe {
self.store
.saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn get_event_recurrence_rules(&self, identifier: &str) -> Result<Vec<RecurrenceRule>> {
self.ensure_authorized()?;
let event = self.find_event_by_id(identifier)?;
Ok(get_item_recurrence_rules(&event))
}
pub fn set_event_recurrence_rule(&self, identifier: &str, rule: &RecurrenceRule) -> Result<()> {
self.ensure_authorized()?;
let event = self.find_event_by_id(identifier)?;
set_item_recurrence_rule(&event, rule);
unsafe {
self.store
.saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn remove_event_recurrence_rules(&self, identifier: &str) -> Result<()> {
self.ensure_authorized()?;
let event = self.find_event_by_id(identifier)?;
clear_item_recurrence_rules(&event);
unsafe {
self.store
.saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn remove_event_alarm(&self, identifier: &str, index: usize) -> Result<()> {
self.ensure_authorized()?;
let event = self.find_event_by_id(identifier)?;
remove_item_alarm(&event, index)?;
unsafe {
self.store
.saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(())
}
pub fn set_event_url(&self, identifier: &str, url: Option<&str>) -> Result<()> {
self.ensure_authorized()?;
let event = self.find_event_by_id(identifier)?;
set_item_url(&event, url);
unsafe {
self.store
.saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
.map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
}
Ok(())
}
fn find_calendar_by_title(&self, title: &str) -> Result<Retained<EKCalendar>> {
let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
for cal in calendars.iter() {
let cal_title = unsafe { cal.title() };
if cal_title.to_string() == title {
return Ok(cal.retain());
}
}
Err(EventKitError::CalendarNotFound(title.to_string()))
}
fn find_event_by_id(&self, identifier: &str) -> Result<Retained<EKEvent>> {
let ns_id = NSString::from_str(identifier);
let event = unsafe { self.store.eventWithIdentifier(&ns_id) };
match event {
Some(e) => Ok(e),
None => Err(EventKitError::ItemNotFound(identifier.to_string())),
}
}
}
impl Default for EventsManager {
fn default() -> Self {
Self::new()
}
}
fn event_to_item(event: &EKEvent) -> EventItem {
let identifier = unsafe { event.eventIdentifier() }
.map(|s| s.to_string())
.unwrap_or_default();
let title = unsafe { event.title() }.to_string();
let notes = unsafe { event.notes() }.map(|n| n.to_string());
let location = unsafe { event.location() }.map(|l| l.to_string());
let all_day = unsafe { event.isAllDay() };
let cal = unsafe { event.calendar() };
let calendar_title = cal.as_ref().map(|c| unsafe { c.title() }.to_string());
let calendar_id = cal
.as_ref()
.map(|c| unsafe { c.calendarIdentifier() }.to_string());
let start_ns: Retained<NSDate> = unsafe { event.startDate() };
let end_ns: Retained<NSDate> = unsafe { event.endDate() };
let start_date = nsdate_to_datetime(&start_ns);
let end_date = nsdate_to_datetime(&end_ns);
let url = get_item_url(event);
let avail = unsafe { event.availability() };
let availability = match avail.0 {
0 => EventAvailability::Busy,
1 => EventAvailability::Free,
2 => EventAvailability::Tentative,
3 => EventAvailability::Unavailable,
_ => EventAvailability::NotSupported,
};
let stat = unsafe { event.status() };
let status = match stat.0 {
1 => EventStatus::Confirmed,
2 => EventStatus::Tentative,
3 => EventStatus::Canceled,
_ => EventStatus::None,
};
let is_detached = unsafe { event.isDetached() };
let occurrence_date = unsafe { event.occurrenceDate() }.map(|d| nsdate_to_datetime(&d));
let structured_location = unsafe { event.structuredLocation() }.map(|loc| {
let title = unsafe { loc.title() }
.map(|t| t.to_string())
.unwrap_or_default();
let radius = unsafe { loc.radius() };
let (latitude, longitude) = unsafe { loc.geoLocation() }
.map(|geo| {
let coord = unsafe { geo.coordinate() };
(coord.latitude, coord.longitude)
})
.unwrap_or((0.0, 0.0));
StructuredLocation {
title,
latitude,
longitude,
radius,
}
});
let attendees = get_item_attendees(event);
let organizer = unsafe { event.organizer() }.map(|p| participant_to_info(&p));
EventItem {
identifier,
title,
notes,
location,
start_date,
end_date,
all_day,
calendar_title,
calendar_id,
url,
availability,
status,
is_detached,
occurrence_date,
structured_location,
attendees,
organizer,
}
}
fn get_item_attendees(item: &EKCalendarItem) -> Vec<ParticipantInfo> {
let attendees = unsafe { item.attendees() };
let Some(attendees) = attendees else {
return Vec::new();
};
let mut result = Vec::new();
for i in 0..attendees.len() {
let p = attendees.objectAtIndex(i);
result.push(participant_to_info(&p));
}
result
}
fn participant_to_info(p: &objc2_event_kit::EKParticipant) -> ParticipantInfo {
let name = unsafe { p.name() }.map(|n| n.to_string());
let url = unsafe { p.URL() }.absoluteString().map(|s| s.to_string());
let role = unsafe { p.participantRole() };
let role = match role.0 {
1 => ParticipantRole::Required,
2 => ParticipantRole::Optional,
3 => ParticipantRole::Chair,
4 => ParticipantRole::NonParticipant,
_ => ParticipantRole::Unknown,
};
let status = unsafe { p.participantStatus() };
let status = match status.0 {
1 => ParticipantStatus::Pending,
2 => ParticipantStatus::Accepted,
3 => ParticipantStatus::Declined,
4 => ParticipantStatus::Tentative,
5 => ParticipantStatus::Delegated,
6 => ParticipantStatus::Completed,
7 => ParticipantStatus::InProcess,
_ => ParticipantStatus::Unknown,
};
let is_current_user = unsafe { p.isCurrentUser() };
ParticipantInfo {
name,
url,
role,
status,
is_current_user,
}
}
fn datetime_to_nsdate(dt: DateTime<Local>) -> Retained<NSDate> {
let timestamp = dt.timestamp() as f64;
NSDate::dateWithTimeIntervalSince1970(timestamp)
}
fn nsdate_to_datetime(date: &NSDate) -> DateTime<Local> {
let timestamp = date.timeIntervalSince1970();
Local.timestamp_opt(timestamp as i64, 0).unwrap()
}
fn date_components_to_datetime(components: &NSDateComponents) -> Option<DateTime<Local>> {
let calendar = NSCalendar::currentCalendar();
let date = calendar.dateFromComponents(components)?;
Some(nsdate_to_datetime(&date))
}
fn datetime_to_date_components(dt: DateTime<Local>) -> Retained<NSDateComponents> {
let components = NSDateComponents::new();
components.setYear(dt.year() as isize);
components.setMonth(dt.month() as isize);
components.setDay(dt.day() as isize);
components.setHour(dt.hour() as isize);
components.setMinute(dt.minute() as isize);
components.setSecond(dt.second() as isize);
components
}
fn get_item_alarms(item: &EKCalendarItem) -> Vec<AlarmInfo> {
let alarms = unsafe { item.alarms() };
let Some(alarms) = alarms else {
return Vec::new();
};
let mut result = Vec::new();
for i in 0..alarms.len() {
let alarm = alarms.objectAtIndex(i);
result.push(alarm_to_info(&alarm));
}
result
}
fn add_item_alarm(item: &EKCalendarItem, alarm: &AlarmInfo) {
let ek_alarm = create_ek_alarm(alarm);
unsafe { item.addAlarm(&ek_alarm) };
}
fn remove_item_alarm(item: &EKCalendarItem, index: usize) -> Result<()> {
let alarms = unsafe { item.alarms() };
let Some(alarms) = alarms else {
return Err(EventKitError::ItemNotFound("No alarms on this item".into()));
};
if index >= alarms.len() {
return Err(EventKitError::ItemNotFound(format!(
"Alarm index {} out of range ({})",
index,
alarms.len()
)));
}
let alarm = alarms.objectAtIndex(index);
unsafe { item.removeAlarm(&alarm) };
Ok(())
}
fn clear_item_alarms(item: &EKCalendarItem) {
unsafe { item.setAlarms(None) };
}
fn get_item_recurrence_rules(item: &EKCalendarItem) -> Vec<RecurrenceRule> {
let rules = unsafe { item.recurrenceRules() };
let Some(rules) = rules else {
return Vec::new();
};
let mut result = Vec::new();
for i in 0..rules.len() {
let rule = rules.objectAtIndex(i);
result.push(recurrence_rule_to_info(&rule));
}
result
}
fn set_item_recurrence_rule(item: &EKCalendarItem, rule: &RecurrenceRule) {
let ek_rule = create_ek_recurrence_rule(rule);
unsafe {
let rules = NSArray::from_retained_slice(&[ek_rule]);
item.setRecurrenceRules(Some(&rules));
}
}
fn clear_item_recurrence_rules(item: &EKCalendarItem) {
unsafe { item.setRecurrenceRules(None) };
}
fn set_item_url(item: &EKCalendarItem, url: Option<&str>) {
unsafe {
let ns_url = url.map(|u| {
let ns_str = NSString::from_str(u);
objc2_foundation::NSURL::URLWithString(&ns_str).unwrap()
});
item.setURL(ns_url.as_deref());
}
}
fn get_item_url(item: &EKCalendarItem) -> Option<String> {
unsafe { item.URL() }.map(|u| u.absoluteString().unwrap().to_string())
}
fn recurrence_rule_to_info(rule: &EKRecurrenceRule) -> RecurrenceRule {
let frequency = unsafe { rule.frequency() };
let frequency = match frequency {
EKRecurrenceFrequency::Daily => RecurrenceFrequency::Daily,
EKRecurrenceFrequency::Weekly => RecurrenceFrequency::Weekly,
EKRecurrenceFrequency::Monthly => RecurrenceFrequency::Monthly,
EKRecurrenceFrequency::Yearly => RecurrenceFrequency::Yearly,
_ => RecurrenceFrequency::Daily,
};
let interval = unsafe { rule.interval() } as usize;
let end = unsafe { rule.recurrenceEnd() }
.map(|end| {
let count = unsafe { end.occurrenceCount() };
if count > 0 {
RecurrenceEndCondition::AfterCount(count)
} else if let Some(date) = unsafe { end.endDate() } {
RecurrenceEndCondition::OnDate(nsdate_to_datetime(&date))
} else {
RecurrenceEndCondition::Never
}
})
.unwrap_or(RecurrenceEndCondition::Never);
let days_of_week = unsafe { rule.daysOfTheWeek() }.map(|days| {
let mut result = Vec::new();
for i in 0..days.len() {
let day = days.objectAtIndex(i);
let weekday = unsafe { day.dayOfTheWeek() };
result.push(weekday.0 as u8);
}
result
});
let days_of_month = unsafe { rule.daysOfTheMonth() }.map(|days| {
let mut result = Vec::new();
for i in 0..days.len() {
let num = days.objectAtIndex(i);
result.push(num.intValue());
}
result
});
RecurrenceRule {
frequency,
interval,
end,
days_of_week,
days_of_month,
}
}
fn create_ek_recurrence_rule(rule: &RecurrenceRule) -> Retained<EKRecurrenceRule> {
let frequency = match rule.frequency {
RecurrenceFrequency::Daily => EKRecurrenceFrequency::Daily,
RecurrenceFrequency::Weekly => EKRecurrenceFrequency::Weekly,
RecurrenceFrequency::Monthly => EKRecurrenceFrequency::Monthly,
RecurrenceFrequency::Yearly => EKRecurrenceFrequency::Yearly,
};
let end = match &rule.end {
RecurrenceEndCondition::Never => None,
RecurrenceEndCondition::AfterCount(count) => {
Some(unsafe { EKRecurrenceEnd::recurrenceEndWithOccurrenceCount(*count) })
}
RecurrenceEndCondition::OnDate(date) => {
let nsdate = datetime_to_nsdate(*date);
Some(unsafe { EKRecurrenceEnd::recurrenceEndWithEndDate(&nsdate) })
}
};
let days_of_week: Option<Vec<Retained<EKRecurrenceDayOfWeek>>> =
rule.days_of_week.as_ref().map(|days| {
days.iter()
.map(|&d| {
let weekday = EKWeekday(d as isize);
unsafe { EKRecurrenceDayOfWeek::dayOfWeek(weekday) }
})
.collect()
});
let days_of_month: Option<Vec<Retained<NSNumber>>> = rule
.days_of_month
.as_ref()
.map(|days| days.iter().map(|&d| NSNumber::new_i32(d)).collect());
let days_of_week_arr = days_of_week
.as_ref()
.map(|v| NSArray::from_retained_slice(v));
let days_of_month_arr = days_of_month
.as_ref()
.map(|v| NSArray::from_retained_slice(v));
unsafe {
use objc2::AnyThread;
EKRecurrenceRule::initRecurrenceWithFrequency_interval_daysOfTheWeek_daysOfTheMonth_monthsOfTheYear_weeksOfTheYear_daysOfTheYear_setPositions_end(
EKRecurrenceRule::alloc(),
frequency,
rule.interval as isize,
days_of_week_arr.as_deref(),
days_of_month_arr.as_deref(),
None, None, None, None, end.as_deref(),
)
}
}
fn alarm_to_info(alarm: &EKAlarm) -> AlarmInfo {
let relative_offset = unsafe { alarm.relativeOffset() };
let absolute_date = unsafe { alarm.absoluteDate() }.map(|d| nsdate_to_datetime(&d));
let proximity = unsafe { alarm.proximity() };
let proximity = match proximity {
EKAlarmProximity::Enter => AlarmProximity::Enter,
EKAlarmProximity::Leave => AlarmProximity::Leave,
_ => AlarmProximity::None,
};
let location = unsafe { alarm.structuredLocation() }.map(|loc| {
let title = unsafe { loc.title() }
.map(|t| t.to_string())
.unwrap_or_default();
let radius = unsafe { loc.radius() };
let (latitude, longitude) = unsafe { loc.geoLocation() }
.map(|geo| {
let coord = unsafe { geo.coordinate() };
(coord.latitude, coord.longitude)
})
.unwrap_or((0.0, 0.0));
StructuredLocation {
title,
latitude,
longitude,
radius,
}
});
AlarmInfo {
relative_offset: Some(relative_offset),
absolute_date,
proximity,
location,
}
}
fn create_ek_alarm(info: &AlarmInfo) -> Retained<EKAlarm> {
let alarm = if let Some(date) = &info.absolute_date {
let nsdate = datetime_to_nsdate(*date);
unsafe { EKAlarm::alarmWithAbsoluteDate(&nsdate) }
} else {
let offset = info.relative_offset.unwrap_or(0.0);
unsafe { EKAlarm::alarmWithRelativeOffset(offset) }
};
let prox = match info.proximity {
AlarmProximity::Enter => EKAlarmProximity::Enter,
AlarmProximity::Leave => EKAlarmProximity::Leave,
AlarmProximity::None => EKAlarmProximity::None,
};
unsafe { alarm.setProximity(prox) };
if let Some(loc) = &info.location {
let title = NSString::from_str(&loc.title);
let structured = unsafe { EKStructuredLocation::locationWithTitle(&title) };
unsafe { structured.setRadius(loc.radius) };
#[cfg(feature = "location")]
{
use objc2::AnyThread;
use objc2_core_location::CLLocation;
let cl_location = unsafe {
CLLocation::initWithLatitude_longitude(
CLLocation::alloc(),
loc.latitude,
loc.longitude,
)
};
unsafe { structured.setGeoLocation(Some(&cl_location)) };
}
unsafe { alarm.setStructuredLocation(Some(&structured)) };
}
alarm
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_authorization_status_display() {
assert_eq!(
format!("{}", AuthorizationStatus::NotDetermined),
"Not Determined"
);
assert_eq!(
format!("{}", AuthorizationStatus::FullAccess),
"Full Access"
);
}
#[test]
fn test_event_item_debug() {
let event = EventItem {
identifier: "test".to_string(),
title: "Test Event".to_string(),
notes: None,
location: None,
start_date: Local::now(),
end_date: Local::now(),
all_day: false,
calendar_title: None,
calendar_id: None,
url: None,
availability: EventAvailability::Busy,
status: EventStatus::None,
is_detached: false,
occurrence_date: None,
structured_location: None,
attendees: Vec::new(),
organizer: None,
};
assert!(format!("{:?}", event).contains("Test Event"));
}
}