···33pub mod event;
44pub mod source;
5566-pub use event::{CalendarEvent, EventTime};
66+pub use event::{CalendarEvent, EventTime, RecurrencePattern};
77pub use source::CalendarSource;
+121
crates/alarma-eds/src/ffi/mod.rs
···263263 Ok(components)
264264 }
265265 }
266266+267267+ /// Generate instances of recurring events with actual occurrence times
268268+ pub fn generate_instances(
269269+ &self,
270270+ start_time: i64,
271271+ end_time: i64,
272272+ ) -> Result<Vec<(ICalComponent, ICalTime, ICalTime)>> {
273273+ unsafe {
274274+ // Collection structure to be passed to callback
275275+ struct InstanceCollector {
276276+ instances: Vec<(ICalComponent, ICalTime, ICalTime)>,
277277+ }
278278+279279+ // Callback that collects each instance
280280+ unsafe extern "C" fn collect_instance(
281281+ icomp: *mut bindings::ICalComponent,
282282+ instance_start: *mut bindings::ICalTime,
283283+ instance_end: *mut bindings::ICalTime,
284284+ user_data: bindings::gpointer,
285285+ _cancellable: *mut bindings::GCancellable,
286286+ _error: *mut *mut bindings::GError,
287287+ ) -> bindings::gboolean {
288288+ let collector = &mut *(user_data as *mut InstanceCollector);
289289+290290+ // Ref all objects before taking ownership
291291+ glib::gobject_ffi::g_object_ref(icomp as *mut _);
292292+ glib::gobject_ffi::g_object_ref(instance_start as *mut _);
293293+ glib::gobject_ffi::g_object_ref(instance_end as *mut _);
294294+295295+ collector.instances.push((
296296+ ICalComponent { ptr: icomp },
297297+ ICalTime {
298298+ ptr: instance_start,
299299+ },
300300+ ICalTime { ptr: instance_end },
301301+ ));
302302+303303+ // Return 1 (TRUE) to continue generating instances
304304+ 1
305305+ }
306306+307307+ let mut collector = InstanceCollector {
308308+ instances: Vec::new(),
309309+ };
310310+311311+ // Call the FFI function
312312+ bindings::e_cal_client_generate_instances_sync(
313313+ self.ptr as *mut bindings::ECalClient,
314314+ start_time,
315315+ end_time,
316316+ ptr::null_mut(), // No cancellable
317317+ Some(collect_instance),
318318+ &mut collector as *mut _ as bindings::gpointer,
319319+ );
320320+321321+ Ok(collector.instances)
322322+ }
323323+ }
266324}
267325268326impl Drop for CalClient {
···349407 unsafe {
350408 let ptr = bindings::i_cal_component_get_dtend(self.ptr);
351409 ICalTime { ptr }
410410+ }
411411+ }
412412+413413+ /// Check if this component has a RECURRENCE-ID property
414414+ /// (indicates it's an instance of a recurring event)
415415+ pub fn has_recurrence_id(&self) -> bool {
416416+ unsafe {
417417+ let ptr = bindings::i_cal_component_get_recurrenceid(self.ptr);
418418+ let has_id = !ptr.is_null() && bindings::i_cal_time_is_null_time(ptr) == 0;
419419+ if !ptr.is_null() {
420420+ glib::gobject_ffi::g_object_unref(ptr as *mut _);
421421+ }
422422+ has_id
423423+ }
424424+ }
425425+426426+ /// Check if this component has an RRULE property
427427+ /// (indicates it's a master recurring event)
428428+ pub fn has_rrule(&self) -> bool {
429429+ unsafe {
430430+ let prop = bindings::i_cal_component_get_first_property(
431431+ self.ptr,
432432+ bindings::ICalPropertyKind_I_CAL_RRULE_PROPERTY,
433433+ );
434434+ let has_rrule = !prop.is_null();
435435+ if !prop.is_null() {
436436+ glib::gobject_ffi::g_object_unref(prop as *mut _);
437437+ }
438438+ has_rrule
439439+ }
440440+ }
441441+442442+ /// Get the recurrence frequency (periodicity) from RRULE
443443+ pub fn recurrence_frequency(&self) -> Option<String> {
444444+ unsafe {
445445+ let prop = bindings::i_cal_component_get_first_property(
446446+ self.ptr,
447447+ bindings::ICalPropertyKind_I_CAL_RRULE_PROPERTY,
448448+ );
449449+ if prop.is_null() {
450450+ return None;
451451+ }
452452+453453+ let rrule = bindings::i_cal_property_get_rrule(prop);
454454+ let freq_str = if !rrule.is_null() {
455455+ let freq = bindings::i_cal_recurrence_get_freq(rrule);
456456+ let freq_cstr = bindings::i_cal_recurrence_frequency_to_string(freq);
457457+ if !freq_cstr.is_null() {
458458+ let result = CStr::from_ptr(freq_cstr).to_string_lossy().to_lowercase();
459459+ Some(result)
460460+ } else {
461461+ None
462462+ }
463463+ } else {
464464+ None
465465+ };
466466+467467+ if !rrule.is_null() {
468468+ glib::gobject_ffi::g_object_unref(rrule as *mut _);
469469+ }
470470+ glib::gobject_ffi::g_object_unref(prop as *mut _);
471471+472472+ freq_str
352473 }
353474 }
354475}
+105-25
crates/alarma-eds/src/repository.rs
···33//! This module implements the CalendarRepository trait using EDS as the backend.
4455use alarma_core::{
66- domain::{CalendarEvent, CalendarSource, EventTime},
66+ domain::{CalendarEvent, CalendarSource, EventTime, RecurrencePattern},
77 repository::{CalendarRepository, DateRange},
88 AlarmaError, Result,
99};
···2323 Ok(Self { registry })
2424 }
25252626- /// Converts an ICalComponent to a CalendarEvent
2727- fn ical_to_event(
2626+ /// Converts an ICalComponent instance to a CalendarEvent using provided times
2727+ fn ical_to_event_instance(
2828 component: &crate::ffi::ICalComponent,
2929+ instance_start: &crate::ffi::ICalTime,
3030+ instance_end: &crate::ffi::ICalTime,
2931 source_uid: String,
3032 ) -> Option<CalendarEvent> {
3133 let summary = component
···3537 let location = component.location();
3638 let uid = component.uid();
37393838- let start_time = component.dtstart();
3939- let end_time = component.dtend();
4040-4141- let is_all_day = start_time.is_date();
4040+ let is_all_day = instance_start.is_date();
42414342 let (start_event_time, end_event_time) = if is_all_day {
4443 // All-day event - use date only
4545- let start_timestamp = start_time.as_timet();
4646- let end_timestamp = end_time.as_timet();
4444+ let start_timestamp = instance_start.as_timet();
4545+ let end_timestamp = instance_end.as_timet();
47464847 let start_dt = Local.timestamp_opt(start_timestamp, 0).single()?;
4948 let end_dt = Local.timestamp_opt(end_timestamp, 0).single()?;
···5453 )
5554 } else {
5655 // Timed event - use full datetime
5757- let start_timestamp = start_time.as_timet();
5858- let end_timestamp = end_time.as_timet();
5656+ let start_timestamp = instance_start.as_timet();
5757+ let end_timestamp = instance_end.as_timet();
59586059 (
6160 EventTime::DateTime(Local.timestamp_opt(start_timestamp, 0).single()?),
···6362 )
6463 };
65646565+ // Extract recurrence pattern if this is a master
6666+ let recurrence_pattern = if component.has_rrule() {
6767+ component
6868+ .recurrence_frequency()
6969+ .map(|periodicity| {
7070+ // For first_occurrence, use the master's original DTSTART, not the instance time
7171+ // Note: This will be corrected later from pre-fetched master data
7272+ let master_start = component.dtstart();
7373+ let master_is_all_day = master_start.is_date();
7474+7575+ let first_occurrence = if master_is_all_day {
7676+ let start_timestamp = master_start.as_timet();
7777+ let start_dt = Local.timestamp_opt(start_timestamp, 0).single()?;
7878+ EventTime::Date(start_dt.date_naive())
7979+ } else {
8080+ let start_timestamp = master_start.as_timet();
8181+ EventTime::DateTime(Local.timestamp_opt(start_timestamp, 0).single()?)
8282+ };
8383+8484+ Some(RecurrencePattern {
8585+ periodicity,
8686+ first_occurrence,
8787+ })
8888+ })
8989+ .flatten()
9090+ } else {
9191+ None
9292+ };
9393+6694 Some(
6795 CalendarEvent::new(uid, summary, start_event_time, end_event_time, source_uid)
6896 .with_description(description)
6969- .with_location(location),
9797+ .with_location(location)
9898+ .with_recurrence_pattern(recurrence_pattern),
7099 )
71100 }
72101}
···95124 // Connect to the calendar
96125 let client = CalClient::connect(source)?;
971269898- // Query events
9999- let components = client.list_events(range.start_timestamp(), range.end_timestamp())?;
127127+ // First, get master events to extract their original first occurrence dates
128128+ use std::collections::HashMap;
129129+ let master_components =
130130+ client.list_events(range.start_timestamp(), range.end_timestamp())?;
131131+ let mut master_first_occurrences: HashMap<String, EventTime> = HashMap::new();
100132101101- // Convert to domain events and filter by actual start time
102102- // EDS may return recurring event masters that are outside the range,
103103- // so we need to filter by the actual event start time
104104- let range_start = range.start_timestamp();
105105- let range_end = range.end_timestamp();
133133+ for comp in &master_components {
134134+ if comp.has_rrule() {
135135+ let uid = comp.uid();
136136+ let dtstart = comp.dtstart();
137137+ let is_all_day = dtstart.is_date();
138138+139139+ let first_occurrence = if is_all_day {
140140+ let timestamp = dtstart.as_timet();
141141+ let dt = Local.timestamp_opt(timestamp, 0).single();
142142+ dt.map(|d| EventTime::Date(d.date_naive()))
143143+ } else {
144144+ let timestamp = dtstart.as_timet();
145145+ Local
146146+ .timestamp_opt(timestamp, 0)
147147+ .single()
148148+ .map(EventTime::DateTime)
149149+ };
106150107107- Ok(components
151151+ if let Some(first_occ) = first_occurrence {
152152+ // Extract base UID (the part before _R suffix for recurring instances)
153153+ let base_uid = uid.split('_').next().unwrap_or(&uid).to_string();
154154+ master_first_occurrences.insert(base_uid, first_occ);
155155+ }
156156+ }
157157+ }
158158+159159+ // Now use generate_instances to get all instances with correct occurrence times
160160+ let instances =
161161+ client.generate_instances(range.start_timestamp(), range.end_timestamp())?;
162162+163163+ // Convert instances to events
164164+ let mut events: Vec<CalendarEvent> = instances
108165 .iter()
109109- .filter_map(|comp| Self::ical_to_event(comp, source_uid.to_string()))
110110- .filter(|event| {
111111- let event_start = event.start.timestamp();
112112- event_start >= range_start && event_start < range_end
166166+ .filter_map(|(component, instance_start, instance_end)| {
167167+ let mut event = Self::ical_to_event_instance(
168168+ component,
169169+ instance_start,
170170+ instance_end,
171171+ source_uid.to_string(),
172172+ )?;
173173+174174+ // Fix the first_occurrence for recurring events using pre-fetched master data
175175+ if let Some(ref mut pattern) = event.recurrence_pattern {
176176+ let base_uid = event
177177+ .uid
178178+ .split('_')
179179+ .next()
180180+ .unwrap_or(&event.uid)
181181+ .to_string();
182182+ if let Some(first_occ) = master_first_occurrences.get(&base_uid) {
183183+ pattern.first_occurrence = first_occ.clone();
184184+ }
185185+ }
186186+187187+ Some(event)
113188 })
114114- .collect())
189189+ .collect();
190190+191191+ // Sort by start time
192192+ events.sort_by_key(|e| e.start.timestamp());
193193+194194+ Ok(events)
115195 }
116196}
117197