tenseleyflow/rcal / eb06be2

Browse files

Add local event edit flow

Authored by espadonne
SHA
eb06be2af61f0259f5e0500416c894984ba75182
Parents
e19fcf6
Tree
71327ba

4 changed files

StatusFile+-
M src/agenda.rs 125 0
M src/app.rs 446 6
M src/cli.rs 22 8
M src/tui.rs 116 4
src/agenda.rsmodified
@@ -215,6 +215,10 @@ impl Event {
215
         self.timing.is_timed()
215
         self.timing.is_timed()
216
     }
216
     }
217
 
217
 
218
+    pub fn is_local(&self) -> bool {
219
+        self.source.source_id == "local"
220
+    }
221
+
218
     pub fn intersects_range(&self, range: DateRange) -> bool {
222
     pub fn intersects_range(&self, range: DateRange) -> bool {
219
         match self.timing {
223
         match self.timing {
220
             EventTiming::AllDay { date } => range.contains_date(date),
224
             EventTiming::AllDay { date } => range.contains_date(date),
@@ -443,6 +447,35 @@ impl ConfiguredAgendaSource {
443
         Ok(event)
447
         Ok(event)
444
     }
448
     }
445
 
449
 
450
+    pub fn update_event(
451
+        &mut self,
452
+        id: &str,
453
+        draft: CreateEventDraft,
454
+    ) -> Result<Event, LocalEventStoreError> {
455
+        let mut events = self.events.events().to_vec();
456
+        let Some(index) = events.iter().position(|event| event.id == id) else {
457
+            return Err(LocalEventStoreError::EventNotFound { id: id.to_string() });
458
+        };
459
+        if !events[index].is_local() {
460
+            return Err(LocalEventStoreError::EventNotEditable { id: id.to_string() });
461
+        }
462
+
463
+        let event =
464
+            draft
465
+                .into_event(id.to_string())
466
+                .map_err(|err| LocalEventStoreError::Encode {
467
+                    path: self.events_file.clone(),
468
+                    reason: err.to_string(),
469
+                })?;
470
+        events[index] = event.clone();
471
+
472
+        if let Some(path) = &self.events_file {
473
+            write_events_file(path, &events)?;
474
+        }
475
+        self.events.events = events;
476
+        Ok(event)
477
+    }
478
+
446
     fn next_local_event_id(&self, title: &str) -> String {
479
     fn next_local_event_id(&self, title: &str) -> String {
447
         let now = SystemTime::now()
480
         let now = SystemTime::now()
448
             .duration_since(UNIX_EPOCH)
481
             .duration_since(UNIX_EPOCH)
@@ -774,6 +807,12 @@ pub enum LocalEventStoreError {
774
         path: PathBuf,
807
         path: PathBuf,
775
         version: u8,
808
         version: u8,
776
     },
809
     },
810
+    EventNotFound {
811
+        id: String,
812
+    },
813
+    EventNotEditable {
814
+        id: String,
815
+    },
777
     Encode {
816
     Encode {
778
         path: Option<PathBuf>,
817
         path: Option<PathBuf>,
779
         reason: String,
818
         reason: String,
@@ -798,6 +837,8 @@ impl fmt::Display for LocalEventStoreError {
798
                 "unsupported local events file version {version} in {}",
837
                 "unsupported local events file version {version} in {}",
799
                 path.display()
838
                 path.display()
800
             ),
839
             ),
840
+            Self::EventNotFound { id } => write!(f, "local event '{id}' was not found"),
841
+            Self::EventNotEditable { id } => write!(f, "event '{id}' is not editable locally"),
801
             Self::Encode { path, reason } => {
842
             Self::Encode { path, reason } => {
802
                 if let Some(path) = path {
843
                 if let Some(path) = path {
803
                     write!(f, "failed to encode {}: {reason}", path.display())
844
                     write!(f, "failed to encode {}: {reason}", path.display())
@@ -1680,6 +1721,90 @@ mod tests {
1680
         );
1721
         );
1681
     }
1722
     }
1682
 
1723
 
1724
+    #[test]
1725
+    fn local_event_store_updates_existing_event_id_and_persists() {
1726
+        let path = temp_events_path("update");
1727
+        let _ = std::fs::remove_dir_all(path.parent().expect("path has parent"));
1728
+        let day = date(23);
1729
+        let mut source = ConfiguredAgendaSource::from_events_file(&path, HolidayProvider::off())
1730
+            .expect("missing event file is empty");
1731
+        let event = source
1732
+            .create_event(CreateEventDraft {
1733
+                title: "Planning".to_string(),
1734
+                timing: CreateEventTiming::Timed {
1735
+                    start: at(day, 9, 0),
1736
+                    end: at(day, 10, 0),
1737
+                },
1738
+                location: None,
1739
+                notes: None,
1740
+                reminders: Vec::new(),
1741
+            })
1742
+            .expect("event saves");
1743
+
1744
+        let updated = source
1745
+            .update_event(
1746
+                &event.id,
1747
+                CreateEventDraft {
1748
+                    title: "Updated planning".to_string(),
1749
+                    timing: CreateEventTiming::AllDay { date: day },
1750
+                    location: Some("Room 2".to_string()),
1751
+                    notes: Some("Moved".to_string()),
1752
+                    reminders: vec![Reminder::minutes_before(5)],
1753
+                },
1754
+            )
1755
+            .expect("event updates");
1756
+
1757
+        assert_eq!(updated.id, event.id);
1758
+        assert!(updated.is_local());
1759
+
1760
+        let reloaded = ConfiguredAgendaSource::from_events_file(&path, HolidayProvider::off())
1761
+            .expect("saved file reloads");
1762
+        let agenda = DayAgenda::from_source(day, &reloaded);
1763
+
1764
+        let _ = std::fs::remove_dir_all(path.parent().expect("test dir exists"));
1765
+
1766
+        assert!(agenda.timed_events.is_empty());
1767
+        assert_eq!(agenda.all_day_events.len(), 1);
1768
+        assert_eq!(agenda.all_day_events[0].id, event.id);
1769
+        assert_eq!(agenda.all_day_events[0].title, "Updated planning");
1770
+        assert_eq!(agenda.all_day_events[0].location.as_deref(), Some("Room 2"));
1771
+    }
1772
+
1773
+    #[test]
1774
+    fn local_event_store_rejects_missing_and_non_local_updates() {
1775
+        let day = date(23);
1776
+        let mut source = ConfiguredAgendaSource::new(
1777
+            InMemoryAgendaSource::with_events_and_holidays(
1778
+                vec![timed("fixture", "Fixture", at(day, 8, 0), at(day, 9, 0))],
1779
+                Vec::new(),
1780
+            ),
1781
+            HolidayProvider::off(),
1782
+        );
1783
+        let draft = CreateEventDraft {
1784
+            title: "Updated".to_string(),
1785
+            timing: CreateEventTiming::Timed {
1786
+                start: at(day, 10, 0),
1787
+                end: at(day, 11, 0),
1788
+            },
1789
+            location: None,
1790
+            notes: None,
1791
+            reminders: Vec::new(),
1792
+        };
1793
+
1794
+        assert!(matches!(
1795
+            source
1796
+                .update_event("missing", draft.clone())
1797
+                .expect_err("missing event fails"),
1798
+            LocalEventStoreError::EventNotFound { .. }
1799
+        ));
1800
+        assert!(matches!(
1801
+            source
1802
+                .update_event("fixture", draft)
1803
+                .expect_err("fixture event fails"),
1804
+            LocalEventStoreError::EventNotEditable { .. }
1805
+        ));
1806
+    }
1807
+
1683
     #[test]
1808
     #[test]
1684
     fn local_event_store_rejects_malformed_json() {
1809
     fn local_event_store_rejects_malformed_json() {
1685
         let path = temp_events_path("malformed");
1810
         let path = temp_events_path("malformed");
src/app.rsmodified
@@ -5,7 +5,8 @@ use time::{Month, Time, Weekday};
5
 
5
 
6
 use crate::{
6
 use crate::{
7
     agenda::{
7
     agenda::{
8
-        AgendaSource, CreateEventDraft, CreateEventTiming, DayAgenda, EventDateTime, Reminder,
8
+        AgendaSource, CreateEventDraft, CreateEventTiming, DayAgenda, Event, EventDateTime,
9
+        EventTiming, Reminder,
9
     },
10
     },
10
     calendar::{CalendarDate, CalendarMonth, DAYS_PER_WEEK},
11
     calendar::{CalendarDate, CalendarMonth, DAYS_PER_WEEK},
11
 };
12
 };
@@ -22,6 +23,7 @@ pub struct AppState {
22
     today: CalendarDate,
23
     today: CalendarDate,
23
     view_mode: ViewMode,
24
     view_mode: ViewMode,
24
     create_form: Option<CreateEventForm>,
25
     create_form: Option<CreateEventForm>,
26
+    selected_day_event_id: Option<String>,
25
     should_quit: bool,
27
     should_quit: bool,
26
 }
28
 }
27
 
29
 
@@ -36,6 +38,7 @@ impl AppState {
36
             today,
38
             today,
37
             view_mode: ViewMode::Month,
39
             view_mode: ViewMode::Month,
38
             create_form: None,
40
             create_form: None,
41
+            selected_day_event_id: None,
39
             should_quit: false,
42
             should_quit: false,
40
         }
43
         }
41
     }
44
     }
@@ -60,6 +63,10 @@ impl AppState {
60
         self.create_form.as_ref()
63
         self.create_form.as_ref()
61
     }
64
     }
62
 
65
 
66
+    pub fn selected_day_event_id(&self) -> Option<&str> {
67
+        self.selected_day_event_id.as_deref()
68
+    }
69
+
63
     pub const fn is_creating_event(&self) -> bool {
70
     pub const fn is_creating_event(&self) -> bool {
64
         self.create_form.is_some()
71
         self.create_form.is_some()
65
     }
72
     }
@@ -93,12 +100,49 @@ impl AppState {
93
         DayAgenda::from_source(self.selected_date, source)
100
         DayAgenda::from_source(self.selected_date, source)
94
     }
101
     }
95
 
102
 
103
+    pub fn reconcile_day_event_selection(&mut self, source: &dyn AgendaSource) {
104
+        if self.view_mode != ViewMode::Day {
105
+            self.selected_day_event_id = None;
106
+            return;
107
+        }
108
+
109
+        let events = selectable_day_events(self.selected_date, source);
110
+        if let Some(selected_id) = &self.selected_day_event_id
111
+            && events.iter().any(|event| &event.id == selected_id)
112
+        {
113
+            return;
114
+        }
115
+
116
+        self.selected_day_event_id = events.first().map(|event| event.id.clone());
117
+    }
118
+
96
     pub fn apply(&mut self, action: AppAction) {
119
     pub fn apply(&mut self, action: AppAction) {
120
+        self.apply_resolved(action, None);
121
+    }
122
+
123
+    pub fn apply_with_agenda_source(&mut self, action: AppAction, source: &dyn AgendaSource) {
124
+        self.apply_resolved(action, Some(source));
125
+    }
126
+
127
+    fn apply_resolved(&mut self, action: AppAction, source: Option<&dyn AgendaSource>) {
97
         match action {
128
         match action {
98
             AppAction::Noop => {}
129
             AppAction::Noop => {}
99
             AppAction::Quit => self.should_quit = true,
130
             AppAction::Quit => self.should_quit = true,
100
-            AppAction::OpenDay => self.view_mode = ViewMode::Day,
131
+            AppAction::OpenDay if self.view_mode == ViewMode::Day => {
101
-            AppAction::CloseDay => self.view_mode = ViewMode::Month,
132
+                if let Some(source) = source {
133
+                    self.open_selected_event_for_edit(source);
134
+                }
135
+            }
136
+            AppAction::OpenDay => {
137
+                self.view_mode = ViewMode::Day;
138
+                if let Some(source) = source {
139
+                    self.reconcile_day_event_selection(source);
140
+                }
141
+            }
142
+            AppAction::CloseDay => {
143
+                self.view_mode = ViewMode::Month;
144
+                self.selected_day_event_id = None;
145
+            }
102
             AppAction::OpenCreate => {
146
             AppAction::OpenCreate => {
103
                 if self.create_form.is_none() {
147
                 if self.create_form.is_none() {
104
                     let context = match self.view_mode {
148
                     let context = match self.view_mode {
@@ -115,6 +159,21 @@ impl AppState {
115
                 if self.view_mode == ViewMode::Day && matches!(days, -1 | 1) =>
159
                 if self.view_mode == ViewMode::Day && matches!(days, -1 | 1) =>
116
             {
160
             {
117
                 self.selected_date = self.selected_date.add_days(days);
161
                 self.selected_date = self.selected_date.add_days(days);
162
+                if let Some(source) = source {
163
+                    self.reconcile_day_event_selection(source);
164
+                } else {
165
+                    self.selected_day_event_id = None;
166
+                }
167
+            }
168
+            AppAction::MoveDays(-7) if self.view_mode == ViewMode::Day => {
169
+                if let Some(source) = source {
170
+                    self.move_day_event_selection(source, -1);
171
+                }
172
+            }
173
+            AppAction::MoveDays(7) if self.view_mode == ViewMode::Day => {
174
+                if let Some(source) = source {
175
+                    self.move_day_event_selection(source, 1);
176
+                }
118
             }
177
             }
119
             AppAction::SelectDate(date) if self.view_mode == ViewMode::Month => {
178
             AppAction::SelectDate(date) if self.view_mode == ViewMode::Month => {
120
                 self.selected_date = date;
179
                 self.selected_date = date;
@@ -136,6 +195,40 @@ impl AppState {
136
         }
195
         }
137
     }
196
     }
138
 
197
 
198
+    fn move_day_event_selection(&mut self, source: &dyn AgendaSource, delta: i32) {
199
+        let events = selectable_day_events(self.selected_date, source);
200
+        if events.is_empty() {
201
+            self.selected_day_event_id = None;
202
+            return;
203
+        }
204
+
205
+        let current_index = self
206
+            .selected_day_event_id
207
+            .as_ref()
208
+            .and_then(|id| events.iter().position(|event| &event.id == id))
209
+            .unwrap_or(0);
210
+        let len = events.len();
211
+        let next_index = if delta < 0 {
212
+            (current_index + len - 1) % len
213
+        } else {
214
+            (current_index + 1) % len
215
+        };
216
+        self.selected_day_event_id = Some(events[next_index].id.clone());
217
+    }
218
+
219
+    fn open_selected_event_for_edit(&mut self, source: &dyn AgendaSource) {
220
+        self.reconcile_day_event_selection(source);
221
+        let Some(selected_id) = self.selected_day_event_id.as_deref() else {
222
+            return;
223
+        };
224
+        if let Some(event) = selectable_day_events(self.selected_date, source)
225
+            .into_iter()
226
+            .find(|event| event.id == selected_id)
227
+        {
228
+            self.create_form = Some(CreateEventForm::edit(&event));
229
+        }
230
+    }
231
+
139
     fn weekday_in_selected_week(&self, weekday: Weekday) -> Option<CalendarDate> {
232
     fn weekday_in_selected_week(&self, weekday: Weekday) -> Option<CalendarDate> {
140
         let month = self.calendar_month();
233
         let month = self.calendar_month();
141
         let selected = month.selected_cell()?;
234
         let selected = month.selected_cell()?;
@@ -168,8 +261,15 @@ pub enum CreateEventContext {
168
     FixedDate,
261
     FixedDate,
169
 }
262
 }
170
 
263
 
264
+#[derive(Debug, Clone, PartialEq, Eq)]
265
+pub enum EventFormMode {
266
+    Create,
267
+    Edit { event_id: String },
268
+}
269
+
171
 #[derive(Debug, Clone, PartialEq, Eq)]
270
 #[derive(Debug, Clone, PartialEq, Eq)]
172
 pub struct CreateEventForm {
271
 pub struct CreateEventForm {
272
+    mode: EventFormMode,
173
     context: CreateEventContext,
273
     context: CreateEventContext,
174
     selected_date: CalendarDate,
274
     selected_date: CalendarDate,
175
     title: String,
275
     title: String,
@@ -188,6 +288,7 @@ pub struct CreateEventForm {
188
 impl CreateEventForm {
288
 impl CreateEventForm {
189
     pub fn new(selected_date: CalendarDate, context: CreateEventContext) -> Self {
289
     pub fn new(selected_date: CalendarDate, context: CreateEventContext) -> Self {
190
         Self {
290
         Self {
291
+            mode: EventFormMode::Create,
191
             context,
292
             context,
192
             selected_date,
293
             selected_date,
193
             title: String::new(),
294
             title: String::new(),
@@ -204,6 +305,67 @@ impl CreateEventForm {
204
         }
305
         }
205
     }
306
     }
206
 
307
 
308
+    pub fn edit(event: &Event) -> Self {
309
+        let (all_day, start_date, start_time, end_date, end_time, selected_date) =
310
+            match event.timing {
311
+                EventTiming::AllDay { date } => (
312
+                    true,
313
+                    date.to_string(),
314
+                    "09:00".to_string(),
315
+                    date.to_string(),
316
+                    "10:00".to_string(),
317
+                    date,
318
+                ),
319
+                EventTiming::Timed { start, end } => (
320
+                    false,
321
+                    start.date.to_string(),
322
+                    format_time_field(start.time),
323
+                    end.date.to_string(),
324
+                    format_time_field(end.time),
325
+                    start.date,
326
+                ),
327
+            };
328
+        let mut reminders = [false; REMINDER_PRESETS.len()];
329
+        for reminder in &event.reminders {
330
+            if let Some(index) = REMINDER_PRESETS
331
+                .iter()
332
+                .position(|preset| preset.minutes == reminder.minutes_before)
333
+            {
334
+                reminders[index] = true;
335
+            }
336
+        }
337
+
338
+        Self {
339
+            mode: EventFormMode::Edit {
340
+                event_id: event.id.clone(),
341
+            },
342
+            context: CreateEventContext::EditableDate,
343
+            selected_date,
344
+            title: event.title.clone(),
345
+            all_day,
346
+            start_date,
347
+            start_time,
348
+            end_date,
349
+            end_time,
350
+            location: event.location.clone().unwrap_or_default(),
351
+            notes: event.notes.clone().unwrap_or_default(),
352
+            reminders,
353
+            focused: 0,
354
+            error: None,
355
+        }
356
+    }
357
+
358
+    pub fn mode(&self) -> &EventFormMode {
359
+        &self.mode
360
+    }
361
+
362
+    pub fn heading(&self) -> &'static str {
363
+        match &self.mode {
364
+            EventFormMode::Create => "Create",
365
+            EventFormMode::Edit { .. } => "Edit",
366
+        }
367
+    }
368
+
207
     pub const fn context(&self) -> CreateEventContext {
369
     pub const fn context(&self) -> CreateEventContext {
208
         self.context
370
         self.context
209
     }
371
     }
@@ -232,7 +394,10 @@ impl CreateEventForm {
232
 
394
 
233
         if ctrl_s(key) {
395
         if ctrl_s(key) {
234
             return match self.submit() {
396
             return match self.submit() {
235
-                Ok(draft) => CreateEventInputResult::Submit(draft),
397
+                Ok(draft) => CreateEventInputResult::Submit(EventFormSubmission {
398
+                    mode: self.mode.clone(),
399
+                    draft,
400
+                }),
236
                 Err(err) => {
401
                 Err(err) => {
237
                     self.error = Some(err.to_string());
402
                     self.error = Some(err.to_string());
238
                     CreateEventInputResult::Continue
403
                     CreateEventInputResult::Continue
@@ -436,11 +601,17 @@ pub enum CreateEventFormRowKind {
436
     Toggle,
601
     Toggle,
437
 }
602
 }
438
 
603
 
604
+#[derive(Debug, Clone, PartialEq, Eq)]
605
+pub struct EventFormSubmission {
606
+    pub mode: EventFormMode,
607
+    pub draft: CreateEventDraft,
608
+}
609
+
439
 #[derive(Debug, Clone, PartialEq, Eq)]
610
 #[derive(Debug, Clone, PartialEq, Eq)]
440
 pub enum CreateEventInputResult {
611
 pub enum CreateEventInputResult {
441
     Continue,
612
     Continue,
442
     Cancel,
613
     Cancel,
443
-    Submit(CreateEventDraft),
614
+    Submit(EventFormSubmission),
444
 }
615
 }
445
 
616
 
446
 #[derive(Debug, Clone, PartialEq, Eq)]
617
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -614,6 +785,25 @@ fn parse_time_field(value: &str, field: &'static str) -> Result<Time, CreateEven
614
     })
785
     })
615
 }
786
 }
616
 
787
 
788
+fn format_time_field(time: Time) -> String {
789
+    format!("{:02}:{:02}", time.hour(), time.minute())
790
+}
791
+
792
+fn selectable_day_events(date: CalendarDate, source: &dyn AgendaSource) -> Vec<Event> {
793
+    let agenda = DayAgenda::from_source(date, source);
794
+    agenda
795
+        .all_day_events
796
+        .into_iter()
797
+        .chain(
798
+            agenda
799
+                .timed_events
800
+                .into_iter()
801
+                .map(|agenda_event| agenda_event.event),
802
+        )
803
+        .filter(Event::is_local)
804
+        .collect()
805
+}
806
+
617
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
807
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
618
 pub struct KeyboardInput {
808
 pub struct KeyboardInput {
619
     pending: PendingKey,
809
     pending: PendingKey,
@@ -822,7 +1012,7 @@ fn ctrl_s(key: KeyEvent) -> bool {
822
 mod tests {
1012
 mod tests {
823
     use super::*;
1013
     use super::*;
824
     use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
1014
     use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
825
-    use time::Month;
1015
+    use time::{Month, Time};
826
 
1016
 
827
     use crate::agenda::{Holiday, InMemoryAgendaSource, SourceMetadata};
1017
     use crate::agenda::{Holiday, InMemoryAgendaSource, SourceMetadata};
828
 
1018
 
@@ -830,6 +1020,32 @@ mod tests {
830
         CalendarDate::from_ymd(year, month, day).expect("valid test date")
1020
         CalendarDate::from_ymd(year, month, day).expect("valid test date")
831
     }
1021
     }
832
 
1022
 
1023
+    fn at(date: CalendarDate, hour: u8, minute: u8) -> EventDateTime {
1024
+        EventDateTime::new(
1025
+            date,
1026
+            Time::from_hms(hour, minute, 0).expect("valid test time"),
1027
+        )
1028
+    }
1029
+
1030
+    fn local_timed_event(id: &str, title: &str, start: EventDateTime, end: EventDateTime) -> Event {
1031
+        Event::timed(id, title, start, end, SourceMetadata::local())
1032
+            .expect("valid local timed event")
1033
+    }
1034
+
1035
+    fn local_all_day_event(id: &str, title: &str, date: CalendarDate) -> Event {
1036
+        Event::all_day(id, title, date, SourceMetadata::local())
1037
+    }
1038
+
1039
+    fn fixture_timed_event(
1040
+        id: &str,
1041
+        title: &str,
1042
+        start: EventDateTime,
1043
+        end: EventDateTime,
1044
+    ) -> Event {
1045
+        Event::timed(id, title, start, end, SourceMetadata::fixture())
1046
+            .expect("valid fixture timed event")
1047
+    }
1048
+
833
     fn key(code: KeyCode) -> KeyEvent {
1049
     fn key(code: KeyCode) -> KeyEvent {
834
         KeyEvent::new(code, KeyModifiers::empty())
1050
         KeyEvent::new(code, KeyModifiers::empty())
835
     }
1051
     }
@@ -866,6 +1082,18 @@ mod tests {
866
         }
1082
         }
867
     }
1083
     }
868
 
1084
 
1085
+    fn apply_keys_with_source(
1086
+        app: &mut AppState,
1087
+        input: &mut KeyboardInput,
1088
+        source: &dyn AgendaSource,
1089
+        keys: impl IntoIterator<Item = KeyEvent>,
1090
+    ) {
1091
+        for key in keys {
1092
+            let action = input.translate(key);
1093
+            app.apply_with_agenda_source(action, source);
1094
+        }
1095
+    }
1096
+
869
     #[test]
1097
     #[test]
870
     fn arrow_keys_move_within_month() {
1098
     fn arrow_keys_move_within_month() {
871
         let mut app = AppState::new(date(2026, Month::April, 23));
1099
         let mut app = AppState::new(date(2026, Month::April, 23));
@@ -1055,6 +1283,152 @@ mod tests {
1055
         assert_eq!(app.selected_date(), date(2026, Month::April, 23));
1283
         assert_eq!(app.selected_date(), date(2026, Month::April, 23));
1056
     }
1284
     }
1057
 
1285
 
1286
+    #[test]
1287
+    fn day_view_selects_first_local_event_and_skips_non_local_items() {
1288
+        let day = date(2026, Month::April, 23);
1289
+        let source = InMemoryAgendaSource::with_events_and_holidays(
1290
+            vec![
1291
+                Event::all_day("fixture-all", "A Fixture", day, SourceMetadata::fixture()),
1292
+                local_all_day_event("local-all", "Release", day),
1293
+                fixture_timed_event("fixture-time", "Fixture", at(day, 8, 0), at(day, 9, 0)),
1294
+                local_timed_event("local-time", "Standup", at(day, 9, 0), at(day, 9, 30)),
1295
+            ],
1296
+            vec![Holiday::new(
1297
+                "holiday",
1298
+                "Holiday",
1299
+                day,
1300
+                SourceMetadata::fixture(),
1301
+            )],
1302
+        );
1303
+        let mut app = AppState::new(day);
1304
+        let mut input = KeyboardInput::default();
1305
+
1306
+        apply_keys_with_source(&mut app, &mut input, &source, [key(KeyCode::Enter)]);
1307
+
1308
+        assert_eq!(app.view_mode(), ViewMode::Day);
1309
+        assert_eq!(app.selected_day_event_id(), Some("local-all"));
1310
+    }
1311
+
1312
+    #[test]
1313
+    fn day_view_up_and_down_cycle_local_event_selection() {
1314
+        let day = date(2026, Month::April, 23);
1315
+        let source = InMemoryAgendaSource::with_events_and_holidays(
1316
+            vec![
1317
+                local_all_day_event("local-all", "Release", day),
1318
+                local_timed_event("local-time", "Standup", at(day, 9, 0), at(day, 9, 30)),
1319
+            ],
1320
+            Vec::new(),
1321
+        );
1322
+        let mut app = AppState::new(day);
1323
+        let mut input = KeyboardInput::default();
1324
+
1325
+        apply_keys_with_source(
1326
+            &mut app,
1327
+            &mut input,
1328
+            &source,
1329
+            [key(KeyCode::Enter), key(KeyCode::Down)],
1330
+        );
1331
+
1332
+        assert_eq!(app.selected_date(), day);
1333
+        assert_eq!(app.selected_day_event_id(), Some("local-time"));
1334
+
1335
+        apply_keys_with_source(&mut app, &mut input, &source, [key(KeyCode::Down)]);
1336
+        assert_eq!(app.selected_day_event_id(), Some("local-all"));
1337
+
1338
+        apply_keys_with_source(&mut app, &mut input, &source, [key(KeyCode::Up)]);
1339
+        assert_eq!(app.selected_day_event_id(), Some("local-time"));
1340
+    }
1341
+
1342
+    #[test]
1343
+    fn day_view_enter_opens_edit_for_selected_local_event() {
1344
+        let day = date(2026, Month::April, 23);
1345
+        let source = InMemoryAgendaSource::with_events_and_holidays(
1346
+            vec![local_timed_event(
1347
+                "local-time",
1348
+                "Standup",
1349
+                at(day, 9, 0),
1350
+                at(day, 9, 30),
1351
+            )],
1352
+            Vec::new(),
1353
+        );
1354
+        let mut app = AppState::new(day);
1355
+        let mut input = KeyboardInput::default();
1356
+
1357
+        apply_keys_with_source(
1358
+            &mut app,
1359
+            &mut input,
1360
+            &source,
1361
+            [key(KeyCode::Enter), key(KeyCode::Enter)],
1362
+        );
1363
+
1364
+        let form = app.create_form().expect("edit form opens");
1365
+        assert_eq!(
1366
+            form.mode(),
1367
+            &EventFormMode::Edit {
1368
+                event_id: "local-time".to_string()
1369
+            }
1370
+        );
1371
+        assert_eq!(form.rows()[0].value, "Standup");
1372
+    }
1373
+
1374
+    #[test]
1375
+    fn day_view_reconciles_selection_when_event_leaves_day() {
1376
+        let day = date(2026, Month::April, 23);
1377
+        let next_day = date(2026, Month::April, 24);
1378
+        let source = InMemoryAgendaSource::with_events_and_holidays(
1379
+            vec![
1380
+                local_timed_event("move", "Move", at(day, 8, 0), at(day, 9, 0)),
1381
+                local_timed_event("stay", "Stay", at(day, 10, 0), at(day, 11, 0)),
1382
+            ],
1383
+            Vec::new(),
1384
+        );
1385
+        let moved_source = InMemoryAgendaSource::with_events_and_holidays(
1386
+            vec![
1387
+                local_timed_event("move", "Move", at(next_day, 8, 0), at(next_day, 9, 0)),
1388
+                local_timed_event("stay", "Stay", at(day, 10, 0), at(day, 11, 0)),
1389
+            ],
1390
+            Vec::new(),
1391
+        );
1392
+        let mut app = AppState::new(day);
1393
+        app.apply_with_agenda_source(AppAction::OpenDay, &source);
1394
+
1395
+        assert_eq!(app.selected_day_event_id(), Some("move"));
1396
+
1397
+        app.reconcile_day_event_selection(&moved_source);
1398
+
1399
+        assert_eq!(app.selected_date(), day);
1400
+        assert_eq!(app.selected_day_event_id(), Some("stay"));
1401
+    }
1402
+
1403
+    #[test]
1404
+    fn edit_form_up_and_down_keep_day_event_selection() {
1405
+        let day = date(2026, Month::April, 23);
1406
+        let source = InMemoryAgendaSource::with_events_and_holidays(
1407
+            vec![local_timed_event(
1408
+                "local-time",
1409
+                "Standup",
1410
+                at(day, 9, 0),
1411
+                at(day, 9, 30),
1412
+            )],
1413
+            Vec::new(),
1414
+        );
1415
+        let mut app = AppState::new(day);
1416
+        let mut input = KeyboardInput::default();
1417
+        apply_keys_with_source(
1418
+            &mut app,
1419
+            &mut input,
1420
+            &source,
1421
+            [key(KeyCode::Enter), key(KeyCode::Enter)],
1422
+        );
1423
+
1424
+        assert_eq!(
1425
+            app.handle_create_key(key(KeyCode::Down)),
1426
+            CreateEventInputResult::Continue
1427
+        );
1428
+        assert_eq!(app.selected_day_event_id(), Some("local-time"));
1429
+        assert!(app.create_form().expect("form stays open").rows()[1].focused);
1430
+    }
1431
+
1058
     #[test]
1432
     #[test]
1059
     fn quit_action_marks_app_done() {
1433
     fn quit_action_marks_app_done() {
1060
         let mut app = AppState::new(date(2026, Month::April, 23));
1434
         let mut app = AppState::new(date(2026, Month::April, 23));
@@ -1143,6 +1517,72 @@ mod tests {
1143
         assert_eq!(form.error(), Some("title is required"));
1517
         assert_eq!(form.error(), Some("title is required"));
1144
     }
1518
     }
1145
 
1519
 
1520
+    #[test]
1521
+    fn edit_form_preloads_timed_event_fields() {
1522
+        let day = date(2026, Month::April, 23);
1523
+        let event = local_timed_event("local-time", "Standup", at(day, 9, 0), at(day, 9, 30))
1524
+            .with_location("Room 1")
1525
+            .with_notes("Bring notes")
1526
+            .with_reminders(vec![
1527
+                Reminder::minutes_before(10),
1528
+                Reminder::minutes_before(60),
1529
+            ]);
1530
+
1531
+        let form = CreateEventForm::edit(&event);
1532
+
1533
+        assert_eq!(
1534
+            form.mode(),
1535
+            &EventFormMode::Edit {
1536
+                event_id: "local-time".to_string()
1537
+            }
1538
+        );
1539
+        assert_eq!(form.title, "Standup");
1540
+        assert!(!form.all_day);
1541
+        assert_eq!(form.start_date, "2026-04-23");
1542
+        assert_eq!(form.start_time, "09:00");
1543
+        assert_eq!(form.end_date, "2026-04-23");
1544
+        assert_eq!(form.end_time, "09:30");
1545
+        assert_eq!(form.location, "Room 1");
1546
+        assert_eq!(form.notes, "Bring notes");
1547
+        assert!(form.reminders[1]);
1548
+        assert!(form.reminders[4]);
1549
+    }
1550
+
1551
+    #[test]
1552
+    fn edit_form_preloads_all_day_event_and_can_switch_to_timed() {
1553
+        let day = date(2026, Month::April, 23);
1554
+        let event = local_all_day_event("local-all", "Release", day);
1555
+        let mut form = CreateEventForm::edit(&event);
1556
+
1557
+        assert!(form.all_day);
1558
+        assert_eq!(form.start_date, "2026-04-23");
1559
+        assert_eq!(form.start_time, "09:00");
1560
+        assert_eq!(form.end_time, "10:00");
1561
+
1562
+        form.all_day = false;
1563
+        let draft = form.submit().expect("all-day edit can become timed");
1564
+
1565
+        assert_eq!(
1566
+            draft.timing,
1567
+            CreateEventTiming::Timed {
1568
+                start: EventDateTime::new(day, Time::from_hms(9, 0, 0).expect("valid time")),
1569
+                end: EventDateTime::new(day, Time::from_hms(10, 0, 0).expect("valid time")),
1570
+            }
1571
+        );
1572
+    }
1573
+
1574
+    #[test]
1575
+    fn edit_form_can_switch_timed_event_to_all_day() {
1576
+        let day = date(2026, Month::April, 23);
1577
+        let event = local_timed_event("local-time", "Standup", at(day, 9, 0), at(day, 9, 30));
1578
+        let mut form = CreateEventForm::edit(&event);
1579
+
1580
+        form.all_day = true;
1581
+        let draft = form.submit().expect("timed edit can become all day");
1582
+
1583
+        assert_eq!(draft.timing, CreateEventTiming::AllDay { date: day });
1584
+    }
1585
+
1146
     #[test]
1586
     #[test]
1147
     fn create_form_submits_day_view_cross_midnight_event() {
1587
     fn create_form_submits_day_view_cross_midnight_event() {
1148
         let day = date(2026, Month::April, 23);
1588
         let day = date(2026, Month::April, 23);
src/cli.rsmodified
@@ -16,7 +16,7 @@ use time::{Date, OffsetDateTime, format_description};
16
 
16
 
17
 use crate::{
17
 use crate::{
18
     agenda::{ConfiguredAgendaSource, HolidayProvider, LocalEventStoreError, default_events_file},
18
     agenda::{ConfiguredAgendaSource, HolidayProvider, LocalEventStoreError, default_events_file},
19
-    app::{AppState, CreateEventInputResult, KeyboardInput, MouseInput},
19
+    app::{AppState, CreateEventInputResult, EventFormMode, KeyboardInput, MouseInput},
20
     calendar::CalendarDate,
20
     calendar::CalendarDate,
21
     tui::{
21
     tui::{
22
         AppView, DEFAULT_RENDER_HEIGHT, DEFAULT_RENDER_WIDTH, hit_test_app_date,
22
         AppView, DEFAULT_RENDER_HEIGHT, DEFAULT_RENDER_WIDTH, hit_test_app_date,
@@ -457,16 +457,30 @@ where
457
                     match app.handle_create_key(key) {
457
                     match app.handle_create_key(key) {
458
                         CreateEventInputResult::Continue => {}
458
                         CreateEventInputResult::Continue => {}
459
                         CreateEventInputResult::Cancel => app.close_create_form(),
459
                         CreateEventInputResult::Cancel => app.close_create_form(),
460
-                        CreateEventInputResult::Submit(draft) => {
460
+                        CreateEventInputResult::Submit(submission) => match submission.mode {
461
-                            match agenda_source.create_event(draft) {
461
+                            EventFormMode::Create => {
462
-                                Ok(_) => app.close_create_form(),
462
+                                match agenda_source.create_event(submission.draft) {
463
-                                Err(err) => app.set_create_form_error(err.to_string()),
463
+                                    Ok(_) => {
464
+                                        app.close_create_form();
465
+                                        app.reconcile_day_event_selection(&agenda_source);
466
+                                    }
467
+                                    Err(err) => app.set_create_form_error(err.to_string()),
468
+                                }
464
                             }
469
                             }
465
-                        }
470
+                            EventFormMode::Edit { event_id } => {
471
+                                match agenda_source.update_event(&event_id, submission.draft) {
472
+                                    Ok(_) => {
473
+                                        app.close_create_form();
474
+                                        app.reconcile_day_event_selection(&agenda_source);
475
+                                    }
476
+                                    Err(err) => app.set_create_form_error(err.to_string()),
477
+                                }
478
+                            }
479
+                        },
466
                     }
480
                     }
467
                 } else {
481
                 } else {
468
                     let action = keyboard.translate(key);
482
                     let action = keyboard.translate(key);
469
-                    app.apply(action);
483
+                    app.apply_with_agenda_source(action, &agenda_source);
470
                 }
484
                 }
471
             }
485
             }
472
             Event::Mouse(mouse_event) => {
486
             Event::Mouse(mouse_event) => {
@@ -479,7 +493,7 @@ where
479
                 let target_date =
493
                 let target_date =
480
                     hit_test_app_date(&app, area, mouse_event.column, mouse_event.row);
494
                     hit_test_app_date(&app, area, mouse_event.column, mouse_event.row);
481
                 let action = mouse.translate(mouse_event, target_date, app.selected_date());
495
                 let action = mouse.translate(mouse_event, target_date, app.selected_date());
482
-                app.apply(action);
496
+                app.apply_with_agenda_source(action, &agenda_source);
483
             }
497
             }
484
             Event::Resize(_, _) => {
498
             Event::Resize(_, _) => {
485
                 keyboard.clear();
499
                 keyboard.clear();
src/tui.rsmodified
@@ -452,6 +452,7 @@ struct DayViewStyles {
452
     timeline_event: Style,
452
     timeline_event: Style,
453
     holiday: Style,
453
     holiday: Style,
454
     event: Style,
454
     event: Style,
455
+    selected_event: Style,
455
 }
456
 }
456
 
457
 
457
 impl DayViewStyles {
458
 impl DayViewStyles {
@@ -467,6 +468,10 @@ impl DayViewStyles {
467
             timeline_event: Style::new().fg(Color::White).bg(Color::Blue),
468
             timeline_event: Style::new().fg(Color::White).bg(Color::Blue),
468
             holiday: Style::new().fg(Color::Yellow),
469
             holiday: Style::new().fg(Color::Yellow),
469
             event: Style::new().fg(Color::White),
470
             event: Style::new().fg(Color::White),
471
+            selected_event: Style::new()
472
+                .fg(Color::White)
473
+                .bg(Color::Blue)
474
+                .add_modifier(Modifier::BOLD),
470
         }
475
         }
471
     }
476
     }
472
 }
477
 }
@@ -743,7 +748,13 @@ fn render_day_view(
743
     match layout.mode {
748
     match layout.mode {
744
         DayViewLayoutMode::Split | DayViewLayoutMode::Stacked => {
749
         DayViewLayoutMode::Split | DayViewLayoutMode::Stacked => {
745
             if let Some(agenda_area) = layout.agenda_area {
750
             if let Some(agenda_area) = layout.agenda_area {
746
-                render_agenda_panel(&agenda, agenda_area, buf, styles);
751
+                render_agenda_panel(
752
+                    &agenda,
753
+                    app.selected_day_event_id(),
754
+                    agenda_area,
755
+                    buf,
756
+                    styles,
757
+                );
747
             }
758
             }
748
 
759
 
749
             if let Some(timeline_area) = layout.timeline_area {
760
             if let Some(timeline_area) = layout.timeline_area {
@@ -821,7 +832,7 @@ fn render_create_event_modal(
821
         content.y,
832
         content.y,
822
         content.x,
833
         content.x,
823
         content.width,
834
         content.width,
824
-        "Create",
835
+        form.heading(),
825
         styles.title,
836
         styles.title,
826
     );
837
     );
827
     let label_width = 12.min(content.width.saturating_sub(1));
838
     let label_width = 12.min(content.width.saturating_sub(1));
@@ -942,7 +953,13 @@ fn create_modal_value_lines(
942
     }
953
     }
943
 }
954
 }
944
 
955
 
945
-fn render_agenda_panel(agenda: &DayAgenda, area: Rect, buf: &mut Buffer, styles: DayViewStyles) {
956
+fn render_agenda_panel(
957
+    agenda: &DayAgenda,
958
+    selected_event_id: Option<&str>,
959
+    area: Rect,
960
+    buf: &mut Buffer,
961
+    styles: DayViewStyles,
962
+) {
946
     render_panel(area, "Agenda", buf, styles);
963
     render_panel(area, "Agenda", buf, styles);
947
 
964
 
948
     let content = inset_rect(area);
965
     let content = inset_rect(area);
@@ -1001,6 +1018,7 @@ fn render_agenda_panel(agenda: &DayAgenda, area: Rect, buf: &mut Buffer, styles:
1001
             y = render_event_detail_lines(
1018
             y = render_event_detail_lines(
1002
                 event,
1019
                 event,
1003
                 &format!("- {}", event.title),
1020
                 &format!("- {}", event.title),
1021
+                selected_event_id == Some(event.id.as_str()),
1004
                 content,
1022
                 content,
1005
                 y,
1023
                 y,
1006
                 buf,
1024
                 buf,
@@ -1041,6 +1059,7 @@ fn render_agenda_panel(agenda: &DayAgenda, area: Rect, buf: &mut Buffer, styles:
1041
                 y = render_event_detail_lines(
1059
                 y = render_event_detail_lines(
1042
                     &agenda_event.event,
1060
                     &agenda_event.event,
1043
                     &agenda_event_line(agenda_event),
1061
                     &agenda_event_line(agenda_event),
1062
+                    selected_event_id == Some(agenda_event.event.id.as_str()),
1044
                     content,
1063
                     content,
1045
                     y,
1064
                     y,
1046
                     buf,
1065
                     buf,
@@ -1054,12 +1073,23 @@ fn render_agenda_panel(agenda: &DayAgenda, area: Rect, buf: &mut Buffer, styles:
1054
 fn render_event_detail_lines(
1073
 fn render_event_detail_lines(
1055
     event: &Event,
1074
     event: &Event,
1056
     first_line: &str,
1075
     first_line: &str,
1076
+    selected: bool,
1057
     content: Rect,
1077
     content: Rect,
1058
     mut y: u16,
1078
     mut y: u16,
1059
     buf: &mut Buffer,
1079
     buf: &mut Buffer,
1060
     styles: DayViewStyles,
1080
     styles: DayViewStyles,
1061
 ) -> u16 {
1081
 ) -> u16 {
1062
-    write_left(buf, y, content.x, content.width, first_line, styles.event);
1082
+    let first_line = if selected {
1083
+        format!("> {first_line}")
1084
+    } else {
1085
+        format!("  {first_line}")
1086
+    };
1087
+    let style = if selected {
1088
+        styles.selected_event
1089
+    } else {
1090
+        styles.event
1091
+    };
1092
+    write_left(buf, y, content.x, content.width, &first_line, style);
1063
     y += 1;
1093
     y += 1;
1064
 
1094
 
1065
     if let Some(location) = &event.location {
1095
     if let Some(location) = &event.location {
@@ -1819,6 +1849,21 @@ mod tests {
1819
         buffer
1849
         buffer
1820
     }
1850
     }
1821
 
1851
 
1852
+    fn render_app_buffer_with_agenda_source<S>(
1853
+        app: &AppState,
1854
+        width: u16,
1855
+        height: u16,
1856
+        agenda_source: &S,
1857
+    ) -> Buffer
1858
+    where
1859
+        S: AgendaSource,
1860
+    {
1861
+        let area = Rect::new(0, 0, width, height);
1862
+        let mut buffer = Buffer::empty(area);
1863
+        AppView::with_agenda_source(app, agenda_source).render(area, &mut buffer);
1864
+        buffer
1865
+    }
1866
+
1822
     fn buffer_lines(buffer: &Buffer) -> Vec<String> {
1867
     fn buffer_lines(buffer: &Buffer) -> Vec<String> {
1823
         buffer_to_string(buffer)
1868
         buffer_to_string(buffer)
1824
             .lines()
1869
             .lines()
@@ -1890,6 +1935,10 @@ mod tests {
1890
         SourceMetadata::fixture()
1935
         SourceMetadata::fixture()
1891
     }
1936
     }
1892
 
1937
 
1938
+    fn local_source_metadata() -> SourceMetadata {
1939
+        SourceMetadata::local()
1940
+    }
1941
+
1893
     fn at(date: CalendarDate, hour: u8, minute: u8) -> EventDateTime {
1942
     fn at(date: CalendarDate, hour: u8, minute: u8) -> EventDateTime {
1894
         EventDateTime::new(
1943
         EventDateTime::new(
1895
             date,
1944
             date,
@@ -1901,6 +1950,11 @@ mod tests {
1901
         Event::timed(id, title, start, end, source_metadata()).expect("valid test event")
1950
         Event::timed(id, title, start, end, source_metadata()).expect("valid test event")
1902
     }
1951
     }
1903
 
1952
 
1953
+    fn local_timed_event(id: &str, title: &str, start: EventDateTime, end: EventDateTime) -> Event {
1954
+        Event::timed(id, title, start, end, local_source_metadata())
1955
+            .expect("valid local timed event")
1956
+    }
1957
+
1904
     fn agenda_source(events: Vec<Event>, holidays: Vec<Holiday>) -> InMemoryAgendaSource {
1958
     fn agenda_source(events: Vec<Event>, holidays: Vec<Holiday>) -> InMemoryAgendaSource {
1905
         InMemoryAgendaSource::with_events_and_holidays(events, holidays)
1959
         InMemoryAgendaSource::with_events_and_holidays(events, holidays)
1906
     }
1960
     }
@@ -2070,6 +2124,28 @@ mod tests {
2070
         assert!(rendered.contains("Ctrl-S save"));
2124
         assert!(rendered.contains("Ctrl-S save"));
2071
     }
2125
     }
2072
 
2126
 
2127
+    #[test]
2128
+    fn edit_modal_uses_edit_title() {
2129
+        let day = date(2026, Month::April, 23);
2130
+        let source = agenda_source(
2131
+            vec![local_timed_event(
2132
+                "planning",
2133
+                "Planning",
2134
+                at(day, 9, 0),
2135
+                at(day, 10, 0),
2136
+            )],
2137
+            Vec::new(),
2138
+        );
2139
+        let mut app = AppState::new(day);
2140
+        app.apply_with_agenda_source(AppAction::OpenDay, &source);
2141
+        app.apply_with_agenda_source(AppAction::OpenDay, &source);
2142
+
2143
+        let rendered = render_app_to_string_with_agenda_source(&app, 84, 26, &source);
2144
+
2145
+        assert!(rendered.contains("Edit"));
2146
+        assert!(rendered.contains("Planning"));
2147
+    }
2148
+
2073
     #[test]
2149
     #[test]
2074
     fn create_modal_clears_background_content() {
2150
     fn create_modal_clears_background_content() {
2075
         let selected = date(2026, Month::April, 23);
2151
         let selected = date(2026, Month::April, 23);
@@ -2419,6 +2495,42 @@ mod tests {
2419
         assert!(rendered.contains("Bring notes"));
2495
         assert!(rendered.contains("Bring notes"));
2420
     }
2496
     }
2421
 
2497
 
2498
+    #[test]
2499
+    fn day_view_highlights_selected_local_event() {
2500
+        let day = date(2026, Month::April, 23);
2501
+        let source = agenda_source(
2502
+            vec![local_timed_event(
2503
+                "planning",
2504
+                "Planning",
2505
+                at(day, 9, 0),
2506
+                at(day, 10, 0),
2507
+            )],
2508
+            Vec::new(),
2509
+        );
2510
+        let mut app = AppState::new(day);
2511
+        app.apply_with_agenda_source(AppAction::OpenDay, &source);
2512
+
2513
+        let buffer = render_app_buffer_with_agenda_source(&app, 84, 18, &source);
2514
+        let rendered = buffer_to_string(&buffer);
2515
+        let selected_position = rendered
2516
+            .lines()
2517
+            .enumerate()
2518
+            .find_map(|(row, line)| {
2519
+                line.find("> 09:00-10:00 Planning")
2520
+                    .map(|column| (column, row))
2521
+            })
2522
+            .expect("selected event is rendered");
2523
+        let selected_cell = buffer
2524
+            .cell((
2525
+                u16::try_from(selected_position.0).expect("column fits"),
2526
+                u16::try_from(selected_position.1).expect("row fits"),
2527
+            ))
2528
+            .expect("selected cell exists");
2529
+
2530
+        assert_eq!(selected_cell.bg, Color::Blue);
2531
+        assert!(selected_cell.modifier.contains(Modifier::BOLD));
2532
+    }
2533
+
2422
     #[test]
2534
     #[test]
2423
     fn overlapping_timeline_events_keep_distinct_labels() {
2535
     fn overlapping_timeline_events_keep_distinct_labels() {
2424
         let day = date(2026, Month::April, 23);
2536
         let day = date(2026, Month::April, 23);