tenseleyflow/rcal / 027ce51

Browse files

Add calendar target selector

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
027ce5189ab5349784cd617150f71dd3c7ece796
Parents
816267c
Tree
3abe913

6 changed files

StatusFile+-
M README.md 6 3
M src/agenda.rs 143 2
M src/app.rs 312 10
M src/cli.rs 9 2
M src/providers.rs 199 8
M src/tui.rs 41 23
README.mdmodified
@@ -170,9 +170,12 @@ daemon does not sync remote calendars itself.
170170
 - Left click selects a visible date; double-click a visible date to open day
171171
   view.
172172
 
173
-Created events are stored locally as JSON and are shown immediately in month,
174
-week, and day views. The create/edit modal supports timed events, single-day
175
-all-day events, recurrence, location, notes, and multiple reminder offsets.
173
+The create/edit modal supports timed events, single-day all-day events,
174
+recurrence, location, notes, and multiple reminder offsets. Its `Calendar`
175
+field controls where the event is saved; use Left/Right on that field to cycle
176
+between local storage and configured editable provider calendars. Local events
177
+are stored as JSON, while Microsoft events are written through Graph and then
178
+shown immediately from the provider cache.
176179
 Reminder notifications are delivered by a user-level background service. Use
177180
 `rcal reminders install` to install it, `rcal reminders status` to inspect it,
178181
 and `rcal reminders test` to send a test notification. On macOS, notification
src/agenda.rsmodified
@@ -133,6 +133,64 @@ impl SourceMetadata {
133133
     }
134134
 }
135135
 
136
+#[derive(Debug, Clone, PartialEq, Eq)]
137
+pub struct EventWriteTarget {
138
+    pub id: EventWriteTargetId,
139
+    pub label: String,
140
+}
141
+
142
+impl EventWriteTarget {
143
+    pub fn local() -> Self {
144
+        Self {
145
+            id: EventWriteTargetId::Local,
146
+            label: "Local".to_string(),
147
+        }
148
+    }
149
+
150
+    pub fn microsoft(
151
+        account_id: impl Into<String>,
152
+        calendar_id: impl Into<String>,
153
+        label: impl Into<String>,
154
+    ) -> Self {
155
+        Self {
156
+            id: EventWriteTargetId::Microsoft {
157
+                account_id: account_id.into(),
158
+                calendar_id: calendar_id.into(),
159
+            },
160
+            label: label.into(),
161
+        }
162
+    }
163
+}
164
+
165
+#[derive(Debug, Clone, PartialEq, Eq)]
166
+pub enum EventWriteTargetId {
167
+    Local,
168
+    Microsoft {
169
+        account_id: String,
170
+        calendar_id: String,
171
+    },
172
+}
173
+
174
+impl EventWriteTargetId {
175
+    pub const fn is_local(&self) -> bool {
176
+        matches!(self, Self::Local)
177
+    }
178
+
179
+    pub fn from_event(event: &Event) -> Option<Self> {
180
+        if event.is_local() {
181
+            return Some(Self::Local);
182
+        }
183
+        let mut parts = event.source.source_id.splitn(3, ':');
184
+        match (parts.next(), parts.next(), parts.next()) {
185
+            (Some("microsoft"), Some(account_id), Some(calendar_id)) => Some(Self::Microsoft {
186
+                account_id: account_id.to_string(),
187
+                calendar_id: calendar_id.to_string(),
188
+            }),
189
+            _ => None,
190
+        }
191
+    }
192
+}
193
+
136194
 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
137195
 pub struct Reminder {
138196
     pub minutes_before: u16,
@@ -592,6 +650,14 @@ pub trait AgendaSource {
592650
 
593651
     fn holidays_in(&self, range: DateRange) -> Vec<Holiday>;
594652
 
653
+    fn event_write_targets(&self) -> Vec<EventWriteTarget> {
654
+        vec![EventWriteTarget::local()]
655
+    }
656
+
657
+    fn default_event_write_target(&self) -> EventWriteTargetId {
658
+        EventWriteTargetId::Local
659
+    }
660
+
595661
     fn local_event_by_id(&self, _id: &str) -> Option<Event> {
596662
         None
597663
     }
@@ -653,16 +719,37 @@ impl ConfiguredAgendaSource {
653719
     }
654720
 
655721
     pub fn create_event(&mut self, draft: CreateEventDraft) -> Result<Event, LocalEventStoreError> {
656
-        if self.create_target == ProviderCreateTarget::Microsoft
722
+        let target = self.default_event_write_target();
723
+        self.create_event_with_target(draft, &target)
724
+    }
725
+
726
+    pub fn create_event_with_target(
727
+        &mut self,
728
+        draft: CreateEventDraft,
729
+        target: &EventWriteTargetId,
730
+    ) -> Result<Event, LocalEventStoreError> {
731
+        if let EventWriteTargetId::Microsoft { .. } = target
657732
             && let Some(microsoft) = &mut self.microsoft
658733
         {
659734
             let http = ReqwestMicrosoftHttpClient;
660735
             let token_store = KeyringMicrosoftTokenStore;
661736
             return microsoft
662
-                .create_event(draft, &http, &token_store)
737
+                .create_event_in_target(draft, target, &http, &token_store)
663738
                 .map_err(provider_error);
664739
         }
740
+        if matches!(target, EventWriteTargetId::Microsoft { .. }) {
741
+            return Err(LocalEventStoreError::Provider {
742
+                reason: "Microsoft provider is not configured".to_string(),
743
+            });
744
+        }
665745
 
746
+        self.create_local_event(draft)
747
+    }
748
+
749
+    fn create_local_event(
750
+        &mut self,
751
+        draft: CreateEventDraft,
752
+    ) -> Result<Event, LocalEventStoreError> {
666753
         let id = self.next_local_event_id(&draft.title);
667754
         let event = draft
668755
             .into_event(id)
@@ -684,6 +771,28 @@ impl ConfiguredAgendaSource {
684771
         id: &str,
685772
         draft: CreateEventDraft,
686773
     ) -> Result<Event, LocalEventStoreError> {
774
+        let target = self
775
+            .event_target_for_id(id)
776
+            .unwrap_or(EventWriteTargetId::Local);
777
+        self.update_event_with_target(id, draft, &target)
778
+    }
779
+
780
+    pub fn update_event_with_target(
781
+        &mut self,
782
+        id: &str,
783
+        draft: CreateEventDraft,
784
+        target: &EventWriteTargetId,
785
+    ) -> Result<Event, LocalEventStoreError> {
786
+        let current_target = self.event_target_for_id(id);
787
+        if current_target
788
+            .as_ref()
789
+            .is_some_and(|current| current != target)
790
+        {
791
+            let created = self.create_event_with_target(draft, target)?;
792
+            self.delete_event(id)?;
793
+            return Ok(created);
794
+        }
795
+
687796
         if self.events.local_event_by_id(id).is_none()
688797
             && id.starts_with("microsoft:")
689798
             && let Some(microsoft) = &mut self.microsoft
@@ -944,6 +1053,18 @@ impl ConfiguredAgendaSource {
9441053
         Ok(event)
9451054
     }
9461055
 
1056
+    fn event_target_for_id(&self, id: &str) -> Option<EventWriteTargetId> {
1057
+        self.events
1058
+            .local_event_by_id(id)
1059
+            .as_ref()
1060
+            .and_then(EventWriteTargetId::from_event)
1061
+            .or_else(|| {
1062
+                let microsoft = self.microsoft.as_ref()?;
1063
+                let event = microsoft.agenda_source().editable_event_by_id(id)?;
1064
+                EventWriteTargetId::from_event(&event)
1065
+            })
1066
+    }
1067
+
9471068
     fn next_local_event_id(&self, title: &str) -> String {
9481069
         let now = SystemTime::now()
9491070
             .duration_since(UNIX_EPOCH)
@@ -977,6 +1098,26 @@ impl AgendaSource for ConfiguredAgendaSource {
9771098
         self.holidays.holidays_in(range)
9781099
     }
9791100
 
1101
+    fn event_write_targets(&self) -> Vec<EventWriteTarget> {
1102
+        let mut targets = vec![EventWriteTarget::local()];
1103
+        if let Some(microsoft) = &self.microsoft {
1104
+            targets.extend(microsoft.write_targets());
1105
+        }
1106
+        targets
1107
+    }
1108
+
1109
+    fn default_event_write_target(&self) -> EventWriteTargetId {
1110
+        if self.create_target == ProviderCreateTarget::Microsoft
1111
+            && let Some(target) = self
1112
+                .microsoft
1113
+                .as_ref()
1114
+                .and_then(MicrosoftProviderRuntime::default_write_target)
1115
+        {
1116
+            return target;
1117
+        }
1118
+        EventWriteTargetId::Local
1119
+    }
1120
+
9801121
     fn local_event_by_id(&self, id: &str) -> Option<Event> {
9811122
         self.events.local_event_by_id(id)
9821123
     }
src/app.rsmodified
@@ -12,8 +12,9 @@ use time::{Month, Time, Weekday};
1212
 use crate::{
1313
     agenda::{
1414
         AgendaSource, CreateEventDraft, CreateEventTiming, DayAgenda, Event, EventDateTime,
15
-        EventTiming, OccurrenceAnchor, RecurrenceEnd, RecurrenceFrequency, RecurrenceMonthlyRule,
16
-        RecurrenceRule, RecurrenceYearlyRule, Reminder, recurrence_ordinal_for_date,
15
+        EventTiming, EventWriteTarget, EventWriteTargetId, OccurrenceAnchor, RecurrenceEnd,
16
+        RecurrenceFrequency, RecurrenceMonthlyRule, RecurrenceRule, RecurrenceYearlyRule, Reminder,
17
+        recurrence_ordinal_for_date,
1718
     },
1819
     calendar::{CalendarDate, CalendarMonth, DAYS_PER_WEEK},
1920
 };
@@ -201,7 +202,10 @@ impl AppState {
201202
                                 .unwrap_or(false)
202203
                         })
203204
                     {
204
-                        self.create_form = Some(CreateEventForm::edit_occurrence(&event));
205
+                        self.create_form = Some(CreateEventForm::edit_occurrence_with_targets(
206
+                            &event,
207
+                            source.event_write_targets(),
208
+                        ));
205209
                     }
206210
                     RecurrenceChoiceInputResult::Continue
207211
                 }
@@ -209,7 +213,10 @@ impl AppState {
209213
                     let series_id = choice.series_id.clone();
210214
                     self.recurrence_choice = None;
211215
                     if let Some(event) = source.editable_event_by_id(&series_id) {
212
-                        self.create_form = Some(CreateEventForm::edit(&event));
216
+                        self.create_form = Some(CreateEventForm::edit_with_targets(
217
+                            &event,
218
+                            source.event_write_targets(),
219
+                        ));
213220
                     }
214221
                     RecurrenceChoiceInputResult::Continue
215222
                 }
@@ -415,7 +422,18 @@ impl AppState {
415422
                         ViewMode::Month => CreateEventContext::EditableDate,
416423
                         ViewMode::Day => CreateEventContext::FixedDate,
417424
                     };
418
-                    self.create_form = Some(CreateEventForm::new(self.selected_date, context));
425
+                    let targets = source
426
+                        .map(AgendaSource::event_write_targets)
427
+                        .unwrap_or_else(|| vec![EventWriteTarget::local()]);
428
+                    let selected_target = source
429
+                        .map(AgendaSource::default_event_write_target)
430
+                        .unwrap_or(EventWriteTargetId::Local);
431
+                    self.create_form = Some(CreateEventForm::new_with_targets(
432
+                        self.selected_date,
433
+                        context,
434
+                        targets,
435
+                        selected_target,
436
+                    ));
419437
                 }
420438
             }
421439
             AppAction::OpenDelete if self.view_mode == ViewMode::Day => {
@@ -522,7 +540,10 @@ impl AppState {
522540
                     occurrence.anchor,
523541
                 ));
524542
             } else {
525
-                self.create_form = Some(CreateEventForm::edit(&event));
543
+                self.create_form = Some(CreateEventForm::edit_with_targets(
544
+                    &event,
545
+                    source.event_write_targets(),
546
+                ));
526547
             }
527548
         }
528549
     }
@@ -993,6 +1014,8 @@ pub enum HelpInputResult {
9931014
 pub struct CreateEventForm {
9941015
     mode: EventFormMode,
9951016
     context: CreateEventContext,
1017
+    targets: Vec<EventWriteTarget>,
1018
+    selected_target: usize,
9961019
     selected_date: CalendarDate,
9971020
     title: String,
9981021
     all_day: bool,
@@ -1015,11 +1038,55 @@ pub struct CreateEventForm {
10151038
     error: Option<String>,
10161039
 }
10171040
 
1041
+fn normalize_write_targets(
1042
+    targets: Vec<EventWriteTarget>,
1043
+    current: Option<EventWriteTarget>,
1044
+) -> Vec<EventWriteTarget> {
1045
+    let mut normalized = Vec::new();
1046
+    for target in targets.into_iter().chain(current) {
1047
+        if !normalized
1048
+            .iter()
1049
+            .any(|existing: &EventWriteTarget| existing.id == target.id)
1050
+        {
1051
+            normalized.push(target);
1052
+        }
1053
+    }
1054
+    if normalized.is_empty() {
1055
+        normalized.push(EventWriteTarget::local());
1056
+    }
1057
+    normalized
1058
+}
1059
+
1060
+fn selected_target_index(targets: &[EventWriteTarget], selected: &EventWriteTargetId) -> usize {
1061
+    targets
1062
+        .iter()
1063
+        .position(|target| &target.id == selected)
1064
+        .unwrap_or_default()
1065
+}
1066
+
10181067
 impl CreateEventForm {
10191068
     pub fn new(selected_date: CalendarDate, context: CreateEventContext) -> Self {
1069
+        Self::new_with_targets(
1070
+            selected_date,
1071
+            context,
1072
+            vec![EventWriteTarget::local()],
1073
+            EventWriteTargetId::Local,
1074
+        )
1075
+    }
1076
+
1077
+    pub fn new_with_targets(
1078
+        selected_date: CalendarDate,
1079
+        context: CreateEventContext,
1080
+        targets: Vec<EventWriteTarget>,
1081
+        selected_target: EventWriteTargetId,
1082
+    ) -> Self {
1083
+        let targets = normalize_write_targets(targets, None);
1084
+        let selected_target = selected_target_index(&targets, &selected_target);
10201085
         Self {
10211086
             mode: EventFormMode::Create,
10221087
             context,
1088
+            targets,
1089
+            selected_target,
10231090
             selected_date,
10241091
             title: String::new(),
10251092
             all_day: false,
@@ -1044,6 +1111,10 @@ impl CreateEventForm {
10441111
     }
10451112
 
10461113
     pub fn edit(event: &Event) -> Self {
1114
+        Self::edit_with_targets(event, vec![EventWriteTarget::local()])
1115
+    }
1116
+
1117
+    pub fn edit_with_targets(event: &Event, targets: Vec<EventWriteTarget>) -> Self {
10471118
         let (all_day, start_date, start_time, end_date, end_time, selected_date) =
10481119
             match event.timing {
10491120
                 EventTiming::AllDay { date } => (
@@ -1072,12 +1143,26 @@ impl CreateEventForm {
10721143
                 reminders[index] = true;
10731144
             }
10741145
         }
1146
+        let current_target = EventWriteTargetId::from_event(event);
1147
+        let targets = normalize_write_targets(
1148
+            targets,
1149
+            current_target.as_ref().map(|target| EventWriteTarget {
1150
+                id: target.clone(),
1151
+                label: event.source.source_name.clone(),
1152
+            }),
1153
+        );
1154
+        let selected_target = current_target
1155
+            .as_ref()
1156
+            .map(|target| selected_target_index(&targets, target))
1157
+            .unwrap_or_default();
10751158
 
10761159
         let mut form = Self {
10771160
             mode: EventFormMode::Edit {
10781161
                 event_id: event.id.clone(),
10791162
             },
10801163
             context: CreateEventContext::EditableDate,
1164
+            targets,
1165
+            selected_target,
10811166
             selected_date,
10821167
             title: event.title.clone(),
10831168
             all_day,
@@ -1104,10 +1189,14 @@ impl CreateEventForm {
11041189
     }
11051190
 
11061191
     pub fn edit_occurrence(event: &Event) -> Self {
1192
+        Self::edit_occurrence_with_targets(event, vec![EventWriteTarget::local()])
1193
+    }
1194
+
1195
+    pub fn edit_occurrence_with_targets(event: &Event, targets: Vec<EventWriteTarget>) -> Self {
11071196
         let Some(occurrence) = event.occurrence() else {
1108
-            return Self::edit(event);
1197
+            return Self::edit_with_targets(event, targets);
11091198
         };
1110
-        let mut form = Self::edit(event);
1199
+        let mut form = Self::edit_with_targets(event, targets);
11111200
         form.mode = EventFormMode::EditOccurrence {
11121201
             series_id: occurrence.series_id.clone(),
11131202
             anchor: occurrence.anchor,
@@ -1158,6 +1247,7 @@ impl CreateEventForm {
11581247
                 Ok(draft) => CreateEventInputResult::Submit(Box::new(EventFormSubmission {
11591248
                     mode: self.mode.clone(),
11601249
                     draft,
1250
+                    target: self.selected_target_id(),
11611251
                 })),
11621252
                 Err(err) => {
11631253
                     self.error = Some(err.to_string());
@@ -1184,6 +1274,14 @@ impl CreateEventForm {
11841274
                 self.focus_next();
11851275
                 CreateEventInputResult::Continue
11861276
             }
1277
+            KeyCode::Left => {
1278
+                self.cycle_focused_field(-1);
1279
+                CreateEventInputResult::Continue
1280
+            }
1281
+            KeyCode::Right => {
1282
+                self.cycle_focused_field(1);
1283
+                CreateEventInputResult::Continue
1284
+            }
11871285
             KeyCode::Backspace => {
11881286
                 self.edit_text_field(|value| {
11891287
                     value.pop();
@@ -1378,7 +1476,11 @@ impl CreateEventForm {
13781476
     }
13791477
 
13801478
     fn visible_fields(&self) -> Vec<CreateEventField> {
1381
-        let mut fields = vec![CreateEventField::Title, CreateEventField::AllDay];
1479
+        let mut fields = vec![CreateEventField::Title];
1480
+        if !matches!(self.mode, EventFormMode::EditOccurrence { .. }) {
1481
+            fields.push(CreateEventField::Calendar);
1482
+        }
1483
+        fields.push(CreateEventField::AllDay);
13821484
         if self.context == CreateEventContext::EditableDate {
13831485
             fields.push(CreateEventField::StartDate);
13841486
         }
@@ -1418,6 +1520,7 @@ impl CreateEventForm {
14181520
     fn field_value(&self, field: CreateEventField) -> String {
14191521
         match field {
14201522
             CreateEventField::Title => self.title.clone(),
1523
+            CreateEventField::Calendar => self.targets[self.selected_target].label.clone(),
14211524
             CreateEventField::AllDay => checkbox(self.all_day).to_string(),
14221525
             CreateEventField::StartDate => self.start_date.clone(),
14231526
             CreateEventField::StartTime => self.start_time.clone(),
@@ -1469,6 +1572,7 @@ impl CreateEventForm {
14691572
     fn activate_focused_field(&mut self) {
14701573
         match self.focused_field() {
14711574
             CreateEventField::AllDay => self.all_day = !self.all_day,
1575
+            CreateEventField::Calendar => self.cycle_target(1),
14721576
             CreateEventField::Reminder(index) => self.reminders[index] = !self.reminders[index],
14731577
             CreateEventField::Notes => self.notes.push('\n'),
14741578
             CreateEventField::Repeat => self.repeat = self.repeat.next(),
@@ -1483,6 +1587,61 @@ impl CreateEventForm {
14831587
         self.error = None;
14841588
     }
14851589
 
1590
+    fn cycle_focused_field(&mut self, delta: i32) {
1591
+        match self.focused_field() {
1592
+            CreateEventField::Calendar => self.cycle_target(delta),
1593
+            CreateEventField::Repeat => {
1594
+                self.repeat = if delta < 0 {
1595
+                    self.repeat.previous()
1596
+                } else {
1597
+                    self.repeat.next()
1598
+                };
1599
+            }
1600
+            CreateEventField::MonthlyMode => {
1601
+                self.monthly_mode = if delta < 0 {
1602
+                    self.monthly_mode.previous()
1603
+                } else {
1604
+                    self.monthly_mode.next()
1605
+                };
1606
+            }
1607
+            CreateEventField::YearlyMode => {
1608
+                self.yearly_mode = if delta < 0 {
1609
+                    self.yearly_mode.previous()
1610
+                } else {
1611
+                    self.yearly_mode.next()
1612
+                };
1613
+            }
1614
+            CreateEventField::RecurrenceEnd => {
1615
+                self.recurrence_end = if delta < 0 {
1616
+                    self.recurrence_end.previous()
1617
+                } else {
1618
+                    self.recurrence_end.next()
1619
+                };
1620
+            }
1621
+            _ => {}
1622
+        }
1623
+        self.error = None;
1624
+    }
1625
+
1626
+    fn cycle_target(&mut self, delta: i32) {
1627
+        let len = self.targets.len();
1628
+        if len <= 1 {
1629
+            return;
1630
+        }
1631
+        self.selected_target = if delta < 0 {
1632
+            (self.selected_target + len - 1) % len
1633
+        } else {
1634
+            (self.selected_target + 1) % len
1635
+        };
1636
+    }
1637
+
1638
+    fn selected_target_id(&self) -> EventWriteTargetId {
1639
+        self.targets
1640
+            .get(self.selected_target)
1641
+            .map(|target| target.id.clone())
1642
+            .unwrap_or(EventWriteTargetId::Local)
1643
+    }
1644
+
14861645
     fn edit_text_field(&mut self, edit: impl FnOnce(&mut String)) {
14871646
         let field = self.focused_field();
14881647
         let target = match field {
@@ -1497,6 +1656,7 @@ impl CreateEventForm {
14971656
             CreateEventField::UntilDate => Some(&mut self.recurrence_until_date),
14981657
             CreateEventField::OccurrenceCount => Some(&mut self.recurrence_count),
14991658
             CreateEventField::AllDay
1659
+            | CreateEventField::Calendar
15001660
             | CreateEventField::Reminder(_)
15011661
             | CreateEventField::Repeat
15021662
             | CreateEventField::WeeklyDay(_)
@@ -1525,12 +1685,14 @@ pub enum CreateEventFormRowKind {
15251685
     Text,
15261686
     Multiline,
15271687
     Toggle,
1688
+    Selector,
15281689
 }
15291690
 
15301691
 #[derive(Debug, Clone, PartialEq, Eq)]
15311692
 pub struct EventFormSubmission {
15321693
     pub mode: EventFormMode,
15331694
     pub draft: CreateEventDraft,
1695
+    pub target: EventWriteTargetId,
15341696
 }
15351697
 
15361698
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -1626,6 +1788,16 @@ impl RepeatFrequency {
16261788
         }
16271789
     }
16281790
 
1791
+    const fn previous(self) -> Self {
1792
+        match self {
1793
+            Self::None => Self::Yearly,
1794
+            Self::Daily => Self::None,
1795
+            Self::Weekly => Self::Daily,
1796
+            Self::Monthly => Self::Weekly,
1797
+            Self::Yearly => Self::Monthly,
1798
+        }
1799
+    }
1800
+
16291801
     const fn frequency(self) -> Option<RecurrenceFrequency> {
16301802
         match self {
16311803
             Self::None => None,
@@ -1666,6 +1838,10 @@ impl RecurrenceMonthlyFormMode {
16661838
             Self::WeekdayOrdinal => Self::DayOfMonth,
16671839
         }
16681840
     }
1841
+
1842
+    const fn previous(self) -> Self {
1843
+        self.next()
1844
+    }
16691845
 }
16701846
 
16711847
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -1688,6 +1864,10 @@ impl RecurrenceYearlyFormMode {
16881864
             Self::WeekdayOrdinal => Self::Date,
16891865
         }
16901866
     }
1867
+
1868
+    const fn previous(self) -> Self {
1869
+        self.next()
1870
+    }
16911871
 }
16921872
 
16931873
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -1713,11 +1893,20 @@ impl RecurrenceEndFormMode {
17131893
             Self::Count => Self::Never,
17141894
         }
17151895
     }
1896
+
1897
+    const fn previous(self) -> Self {
1898
+        match self {
1899
+            Self::Never => Self::Count,
1900
+            Self::Until => Self::Never,
1901
+            Self::Count => Self::Until,
1902
+        }
1903
+    }
17161904
 }
17171905
 
17181906
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
17191907
 enum CreateEventField {
17201908
     Title,
1909
+    Calendar,
17211910
     AllDay,
17221911
     StartDate,
17231912
     StartTime,
@@ -1740,6 +1929,7 @@ impl CreateEventField {
17401929
     const fn label(self) -> &'static str {
17411930
         match self {
17421931
             Self::Title => "Title",
1932
+            Self::Calendar => "Calendar",
17431933
             Self::AllDay => "All day",
17441934
             Self::StartDate => "Start date",
17451935
             Self::StartTime => "Start time",
@@ -1763,6 +1953,11 @@ impl CreateEventField {
17631953
         match self {
17641954
             Self::Notes => CreateEventFormRowKind::Multiline,
17651955
             Self::AllDay | Self::Reminder(_) | Self::WeeklyDay(_) => CreateEventFormRowKind::Toggle,
1956
+            Self::Calendar
1957
+            | Self::Repeat
1958
+            | Self::MonthlyMode
1959
+            | Self::YearlyMode
1960
+            | Self::RecurrenceEnd => CreateEventFormRowKind::Selector,
17661961
             _ => CreateEventFormRowKind::Text,
17671962
         }
17681963
     }
@@ -2678,7 +2873,9 @@ mod tests {
26782873
     use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
26792874
     use time::{Month, Time};
26802875
 
2681
-    use crate::agenda::{Holiday, InMemoryAgendaSource, RecurrenceOrdinal, SourceMetadata};
2876
+    use crate::agenda::{
2877
+        DateRange, Holiday, InMemoryAgendaSource, RecurrenceOrdinal, SourceMetadata,
2878
+    };
26822879
 
26832880
     fn date(year: i32, month: Month, day: u8) -> CalendarDate {
26842881
         CalendarDate::from_ymd(year, month, day).expect("valid test date")
@@ -2758,6 +2955,37 @@ mod tests {
27582955
         }
27592956
     }
27602957
 
2958
+    struct WriteTargetSource {
2959
+        targets: Vec<EventWriteTarget>,
2960
+        default: EventWriteTargetId,
2961
+    }
2962
+
2963
+    impl AgendaSource for WriteTargetSource {
2964
+        fn events_intersecting(&self, _range: DateRange) -> Vec<Event> {
2965
+            Vec::new()
2966
+        }
2967
+
2968
+        fn holidays_in(&self, _range: DateRange) -> Vec<Holiday> {
2969
+            Vec::new()
2970
+        }
2971
+
2972
+        fn event_write_targets(&self) -> Vec<EventWriteTarget> {
2973
+            self.targets.clone()
2974
+        }
2975
+
2976
+        fn default_event_write_target(&self) -> EventWriteTargetId {
2977
+            self.default.clone()
2978
+        }
2979
+    }
2980
+
2981
+    fn form_value(form: &CreateEventForm, label: &str) -> String {
2982
+        form.rows()
2983
+            .into_iter()
2984
+            .find(|row| row.label == label)
2985
+            .map(|row| row.value)
2986
+            .expect("form row exists")
2987
+    }
2988
+
27612989
     #[test]
27622990
     fn configured_keybindings_replace_default_normal_mode_commands() {
27632991
         let bindings = KeyBindings::with_overrides(KeyBindingOverrides {
@@ -3448,6 +3676,24 @@ mod tests {
34483676
         );
34493677
     }
34503678
 
3679
+    #[test]
3680
+    fn plus_opens_create_form_with_agenda_source_default_calendar_target() {
3681
+        let day = date(2026, Month::April, 23);
3682
+        let target = EventWriteTarget::microsoft("work", "cal", "Microsoft work: Calendar");
3683
+        let source = WriteTargetSource {
3684
+            targets: vec![EventWriteTarget::local(), target.clone()],
3685
+            default: target.id.clone(),
3686
+        };
3687
+        let mut app = AppState::new(day);
3688
+        let mut input = KeyboardInput::default();
3689
+
3690
+        app.apply_with_agenda_source(input.translate(char_key('+')), &source);
3691
+
3692
+        let form = app.create_form().expect("form opens");
3693
+        assert_eq!(form.context(), CreateEventContext::EditableDate);
3694
+        assert_eq!(form_value(form, "Calendar"), "Microsoft work: Calendar");
3695
+    }
3696
+
34513697
     #[test]
34523698
     fn question_mark_opens_help_and_esc_closes_it() {
34533699
         let day = date(2026, Month::April, 23);
@@ -3524,6 +3770,43 @@ mod tests {
35243770
         assert_eq!(app.selected_date(), day);
35253771
     }
35263772
 
3773
+    #[test]
3774
+    fn create_form_calendar_selector_cycles_and_submits_target() {
3775
+        let day = date(2026, Month::April, 23);
3776
+        let calendar = EventWriteTarget::microsoft("work", "cal", "Microsoft work: Calendar");
3777
+        let personal = EventWriteTarget::microsoft("work", "personal", "Microsoft work: Personal");
3778
+        let mut form = CreateEventForm::new_with_targets(
3779
+            day,
3780
+            CreateEventContext::EditableDate,
3781
+            vec![
3782
+                EventWriteTarget::local(),
3783
+                calendar.clone(),
3784
+                personal.clone(),
3785
+            ],
3786
+            calendar.id.clone(),
3787
+        );
3788
+
3789
+        form.title = "Planning".to_string();
3790
+        let _ = form.handle_key(key(KeyCode::Tab));
3791
+        assert_eq!(form_value(&form, "Calendar"), "Microsoft work: Calendar");
3792
+
3793
+        let _ = form.handle_key(key(KeyCode::Right));
3794
+        assert_eq!(form_value(&form, "Calendar"), "Microsoft work: Personal");
3795
+        let _ = form.handle_key(key(KeyCode::Left));
3796
+        assert_eq!(form_value(&form, "Calendar"), "Microsoft work: Calendar");
3797
+        let _ = form.handle_key(key(KeyCode::Left));
3798
+        assert_eq!(form_value(&form, "Calendar"), "Local");
3799
+        let _ = form.handle_key(key(KeyCode::Right));
3800
+        let _ = form.handle_key(key(KeyCode::Right));
3801
+
3802
+        let result = form.handle_key(ctrl_char_key('s'));
3803
+
3804
+        let CreateEventInputResult::Submit(submission) = result else {
3805
+            panic!("form should submit");
3806
+        };
3807
+        assert_eq!(submission.target, personal.id);
3808
+    }
3809
+
35273810
     #[test]
35283811
     fn create_form_validates_required_title() {
35293812
         let mut form = CreateEventForm::new(
@@ -3568,6 +3851,25 @@ mod tests {
35683851
         assert!(form.reminders[4]);
35693852
     }
35703853
 
3854
+    #[test]
3855
+    fn edit_form_preloads_provider_calendar_target() {
3856
+        let day = date(2026, Month::April, 23);
3857
+        let target = EventWriteTarget::microsoft("work", "cal", "Microsoft work: Calendar");
3858
+        let event = Event::timed(
3859
+            "microsoft:work:cal:remote-1",
3860
+            "Sync",
3861
+            at(day, 9, 0),
3862
+            at(day, 9, 30),
3863
+            SourceMetadata::new("microsoft:work:cal", "Microsoft work: Calendar"),
3864
+        )
3865
+        .expect("valid provider event");
3866
+
3867
+        let form =
3868
+            CreateEventForm::edit_with_targets(&event, vec![EventWriteTarget::local(), target]);
3869
+
3870
+        assert_eq!(form_value(&form, "Calendar"), "Microsoft work: Calendar");
3871
+    }
3872
+
35713873
     #[test]
35723874
     fn edit_form_preloads_all_day_event_and_can_switch_to_timed() {
35733875
         let day = date(2026, Month::April, 23);
src/cli.rsmodified
@@ -1631,7 +1631,10 @@ where
16311631
                             let submission = *submission;
16321632
                             match submission.mode {
16331633
                                 EventFormMode::Create => {
1634
-                                    match agenda_source.create_event(submission.draft) {
1634
+                                    match agenda_source.create_event_with_target(
1635
+                                        submission.draft,
1636
+                                        &submission.target,
1637
+                                    ) {
16351638
                                         Ok(_) => {
16361639
                                             app.close_create_form();
16371640
                                             app.reconcile_day_event_selection(&agenda_source);
@@ -1640,7 +1643,11 @@ where
16401643
                                     }
16411644
                                 }
16421645
                                 EventFormMode::Edit { event_id } => {
1643
-                                    match agenda_source.update_event(&event_id, submission.draft) {
1646
+                                    match agenda_source.update_event_with_target(
1647
+                                        &event_id,
1648
+                                        submission.draft,
1649
+                                        &submission.target,
1650
+                                    ) {
16441651
                                         Ok(_) => {
16451652
                                             app.close_create_form();
16461653
                                             app.reconcile_day_event_selection(&agenda_source);
src/providers.rsmodified
@@ -19,9 +19,10 @@ use time::{Month, Time, Weekday};
1919
 use crate::{
2020
     agenda::{
2121
         AgendaError, AgendaSource, CreateEventDraft, CreateEventTiming, DateRange, Event,
22
-        EventDateTime, Holiday, InMemoryAgendaSource, OccurrenceAnchor, OccurrenceMetadata,
23
-        RecurrenceEnd, RecurrenceFrequency, RecurrenceMonthlyRule, RecurrenceOrdinal,
24
-        RecurrenceRule, RecurrenceYearlyRule, Reminder, SourceMetadata,
22
+        EventDateTime, EventWriteTarget, EventWriteTargetId, Holiday, InMemoryAgendaSource,
23
+        OccurrenceAnchor, OccurrenceMetadata, RecurrenceEnd, RecurrenceFrequency,
24
+        RecurrenceMonthlyRule, RecurrenceOrdinal, RecurrenceRule, RecurrenceYearlyRule, Reminder,
25
+        SourceMetadata,
2526
     },
2627
     calendar::CalendarDate,
2728
 };
@@ -326,6 +327,46 @@ impl MicrosoftProviderRuntime {
326327
         }
327328
     }
328329
 
330
+    pub fn write_targets(&self) -> Vec<EventWriteTarget> {
331
+        self.config
332
+            .accounts
333
+            .iter()
334
+            .flat_map(|account| {
335
+                account.calendars.iter().filter_map(|calendar_id| {
336
+                    let record = self.cache.calendar_record(&account.id, calendar_id);
337
+                    if record.as_ref().is_some_and(|calendar| !calendar.can_edit) {
338
+                        return None;
339
+                    }
340
+                    let label = record
341
+                        .as_ref()
342
+                        .map(|calendar| calendar.name.as_str())
343
+                        .filter(|name| !name.trim().is_empty())
344
+                        .map(|name| format!("Microsoft {}: {name}", account.id))
345
+                        .unwrap_or_else(|| {
346
+                            format!(
347
+                                "Microsoft {}: {}",
348
+                                account.id,
349
+                                short_calendar_label(calendar_id)
350
+                            )
351
+                        });
352
+                    Some(EventWriteTarget::microsoft(
353
+                        account.id.clone(),
354
+                        calendar_id.clone(),
355
+                        label,
356
+                    ))
357
+                })
358
+            })
359
+            .collect()
360
+    }
361
+
362
+    pub fn default_write_target(&self) -> Option<EventWriteTargetId> {
363
+        let (account, calendar_id) = self.config.default_calendar()?;
364
+        Some(EventWriteTargetId::Microsoft {
365
+            account_id: account.id.clone(),
366
+            calendar_id: calendar_id.to_string(),
367
+        })
368
+    }
369
+
329370
     pub fn status(&self, token_store: &dyn MicrosoftTokenStore) -> MicrosoftProviderStatus {
330371
         let accounts = self
331372
             .config
@@ -400,11 +441,44 @@ impl MicrosoftProviderRuntime {
400441
         http: &dyn MicrosoftHttpClient,
401442
         token_store: &dyn MicrosoftTokenStore,
402443
     ) -> Result<Event, ProviderError> {
403
-        let (account, calendar_id) = self.config.default_calendar().ok_or_else(|| {
444
+        let target = self.default_write_target().ok_or_else(|| {
404445
             ProviderError::Config("no Microsoft default calendar configured".to_string())
405446
         })?;
406
-        let account = account.clone();
407
-        let calendar_id = calendar_id.to_string();
447
+        self.create_event_in_target(draft, &target, http, token_store)
448
+    }
449
+
450
+    pub fn create_event_in_target(
451
+        &mut self,
452
+        draft: CreateEventDraft,
453
+        target: &EventWriteTargetId,
454
+        http: &dyn MicrosoftHttpClient,
455
+        token_store: &dyn MicrosoftTokenStore,
456
+    ) -> Result<Event, ProviderError> {
457
+        let EventWriteTargetId::Microsoft {
458
+            account_id,
459
+            calendar_id,
460
+        } = target
461
+        else {
462
+            return Err(ProviderError::Config(
463
+                "Microsoft provider requires a Microsoft calendar target".to_string(),
464
+            ));
465
+        };
466
+        let account = self
467
+            .config
468
+            .account(account_id)
469
+            .ok_or_else(|| {
470
+                ProviderError::Config(format!("account '{account_id}' is not configured"))
471
+            })?
472
+            .clone();
473
+        if !account
474
+            .calendars
475
+            .iter()
476
+            .any(|calendar| calendar == calendar_id)
477
+        {
478
+            return Err(ProviderError::Config(format!(
479
+                "calendar '{calendar_id}' is not configured for account '{account_id}'"
480
+            )));
481
+        }
408482
         let token = access_token(&account, http, token_store)?;
409483
         let body = graph_event_payload(&draft, false)?;
410484
         let response = graph_request(
@@ -412,14 +486,14 @@ impl MicrosoftProviderRuntime {
412486
             "POST",
413487
             &format!(
414488
                 "{GRAPH_BASE_URL}/me/calendars/{}/events",
415
-                percent_encode(&calendar_id)
489
+                percent_encode(calendar_id)
416490
             ),
417491
             &token,
418492
             Some(body.to_string()),
419493
         )?;
420494
         let value = parse_graph_success_json(response)?;
421495
         let calendar =
422
-            fetch_calendar(http, &token, &calendar_id).unwrap_or(MicrosoftCalendarRecord {
496
+            fetch_calendar(http, &token, calendar_id).unwrap_or(MicrosoftCalendarRecord {
423497
                 id: calendar_id.clone(),
424498
                 name: calendar_id.clone(),
425499
                 can_edit: true,
@@ -2331,6 +2405,16 @@ fn microsoft_event_app_id(account_id: &str, calendar_id: &str, graph_id: &str) -
23312405
     format!("microsoft:{account_id}:{calendar_id}:{graph_id}")
23322406
 }
23332407
 
2408
+fn short_calendar_label(calendar_id: &str) -> String {
2409
+    const MAX: usize = 18;
2410
+    let label = calendar_id.chars().take(MAX).collect::<String>();
2411
+    if calendar_id.chars().count() > MAX {
2412
+        format!("{label}...")
2413
+    } else {
2414
+        label
2415
+    }
2416
+}
2417
+
23342418
 fn anchor_label(anchor: OccurrenceAnchor) -> String {
23352419
     match anchor {
23362420
         OccurrenceAnchor::AllDay { date } => date.to_string(),
@@ -2632,6 +2716,15 @@ mod tests {
26322716
         }
26332717
     }
26342718
 
2719
+    fn calendar_record(id: &str, name: &str, can_edit: bool) -> MicrosoftCalendarRecord {
2720
+        MicrosoftCalendarRecord {
2721
+            id: id.to_string(),
2722
+            name: name.to_string(),
2723
+            can_edit,
2724
+            is_default: false,
2725
+        }
2726
+    }
2727
+
26352728
     #[derive(Default)]
26362729
     struct MemoryTokenStore {
26372730
         tokens: RefCell<HashMap<String, MicrosoftToken>>,
@@ -2749,6 +2842,104 @@ mod tests {
27492842
         assert!(event.source.source_id.starts_with("microsoft:work:cal"));
27502843
     }
27512844
 
2845
+    #[test]
2846
+    fn provider_write_targets_use_configured_editable_calendars() {
2847
+        let cache_file = temp_path("targets/microsoft-cache.json");
2848
+        let mut config = provider_config(cache_file);
2849
+        config.default_calendar = Some("team".to_string());
2850
+        config.accounts[0].calendars = vec![
2851
+            "cal".to_string(),
2852
+            "team".to_string(),
2853
+            "holidays".to_string(),
2854
+        ];
2855
+        let mut runtime = MicrosoftProviderRuntime::load(config).expect("load");
2856
+        runtime
2857
+            .cache
2858
+            .replace_calendar("work", calendar_record("cal", "Work", true), Vec::new(), 0);
2859
+        runtime.cache.replace_calendar(
2860
+            "work",
2861
+            calendar_record("holidays", "Holidays", false),
2862
+            Vec::new(),
2863
+            0,
2864
+        );
2865
+
2866
+        let targets = runtime.write_targets();
2867
+
2868
+        assert_eq!(targets.len(), 2);
2869
+        assert_eq!(targets[0].label, "Microsoft work: Work");
2870
+        assert_eq!(targets[1].label, "Microsoft work: team");
2871
+        assert_eq!(
2872
+            runtime.default_write_target(),
2873
+            Some(EventWriteTargetId::Microsoft {
2874
+                account_id: "work".to_string(),
2875
+                calendar_id: "team".to_string(),
2876
+            })
2877
+        );
2878
+    }
2879
+
2880
+    #[test]
2881
+    fn create_event_uses_explicit_calendar_target() {
2882
+        let cache_file = temp_path("create-target/microsoft-cache.json");
2883
+        let mut config = provider_config(cache_file.clone());
2884
+        config.accounts[0].calendars = vec!["cal".to_string(), "personal".to_string()];
2885
+        let store = MemoryTokenStore::with_token("work");
2886
+        let http = RecordingHttpClient::new(vec![
2887
+            RecordingHttpClient::json(
2888
+                201,
2889
+                json!({
2890
+                    "id": "evt",
2891
+                    "subject": "Planning",
2892
+                    "type": "singleInstance",
2893
+                    "isAllDay": false,
2894
+                    "start": {"dateTime": "2026-04-23T09:00:00", "timeZone": "UTC"},
2895
+                    "end": {"dateTime": "2026-04-23T10:00:00", "timeZone": "UTC"}
2896
+                }),
2897
+            ),
2898
+            RecordingHttpClient::json(
2899
+                200,
2900
+                json!({
2901
+                    "id": "personal",
2902
+                    "name": "Personal",
2903
+                    "canEdit": true,
2904
+                    "isDefaultCalendar": false
2905
+                }),
2906
+            ),
2907
+        ]);
2908
+        let mut runtime = MicrosoftProviderRuntime::load(config).expect("load");
2909
+        let day = date(2026, Month::April, 23);
2910
+        let draft = CreateEventDraft {
2911
+            title: "Planning".to_string(),
2912
+            timing: CreateEventTiming::Timed {
2913
+                start: at(day, 9, 0),
2914
+                end: at(day, 10, 0),
2915
+            },
2916
+            location: None,
2917
+            notes: None,
2918
+            reminders: Vec::new(),
2919
+            recurrence: None,
2920
+        };
2921
+        let target = EventWriteTargetId::Microsoft {
2922
+            account_id: "work".to_string(),
2923
+            calendar_id: "personal".to_string(),
2924
+        };
2925
+
2926
+        let event = runtime
2927
+            .create_event_in_target(draft, &target, &http, &store)
2928
+            .expect("create succeeds");
2929
+
2930
+        let _ = fs::remove_dir_all(
2931
+            cache_file
2932
+                .parent()
2933
+                .and_then(Path::parent)
2934
+                .expect("test root"),
2935
+        );
2936
+        assert_eq!(event.id, "microsoft:work:personal:evt");
2937
+        assert_eq!(
2938
+            http.requests.borrow()[0].url,
2939
+            format!("{GRAPH_BASE_URL}/me/calendars/personal/events")
2940
+        );
2941
+    }
2942
+
27522943
     #[test]
27532944
     fn graph_all_day_recurring_occurrence_maps_anchor() {
27542945
         let calendar = MicrosoftCalendarRecord {
src/tui.rsmodified
@@ -1011,7 +1011,9 @@ fn render_create_event_modal(
10111011
 
10121012
             if value_x < content.right() {
10131013
                 match row.kind {
1014
-                    CreateEventFormRowKind::Text | CreateEventFormRowKind::Multiline => {
1014
+                    CreateEventFormRowKind::Text
1015
+                    | CreateEventFormRowKind::Multiline
1016
+                    | CreateEventFormRowKind::Selector => {
10151017
                         write_padded_left(
10161018
                             buf,
10171019
                             line_y,
@@ -1042,7 +1044,7 @@ fn render_create_event_modal(
10421044
         footer_y,
10431045
         content.x,
10441046
         content.width,
1045
-        "Tab/Up/Down fields | Ctrl-S save | Esc cancel",
1047
+        create_modal_footer(content.width),
10461048
         styles.footer,
10471049
     );
10481050
 }
@@ -1410,19 +1412,19 @@ fn help_rows(view_mode: ViewMode, keybindings: &KeyBindings) -> Vec<(String, &'s
14101412
                     keybindings.display_for(KeyCommand::MoveUp),
14111413
                     keybindings.display_for(KeyCommand::MoveDown)
14121414
                 ),
1413
-                "Select a local event",
1415
+                "Select an editable event",
14141416
             ),
14151417
             (
14161418
                 keybindings.display_for(KeyCommand::OpenDayOrEdit),
1417
-                "Edit the selected local event",
1419
+                "Edit the selected event",
14181420
             ),
14191421
             (
14201422
                 keybindings.display_for(KeyCommand::CopyEvent),
1421
-                "Copy the selected local event",
1423
+                "Copy the selected event",
14221424
             ),
14231425
             (
14241426
                 keybindings.display_for(KeyCommand::DeleteEvent),
1425
-                "Delete the selected local event",
1427
+                "Delete the selected event",
14261428
             ),
14271429
             (
14281430
                 keybindings.display_for(KeyCommand::CreateEvent),
@@ -1474,6 +1476,16 @@ fn create_modal_row_height(kind: CreateEventFormRowKind, value: &str, value_widt
14741476
     u16::try_from(create_modal_value_lines(kind, value, value_width).len()).unwrap_or(u16::MAX)
14751477
 }
14761478
 
1479
+fn create_modal_footer(width: u16) -> &'static str {
1480
+    let full = "Tab/Up/Down fields | Left/Right change | Ctrl-S save | Esc cancel";
1481
+    let compact = "Left/Right | Ctrl-S save | Esc";
1482
+    if usize::from(width) >= full.chars().count() {
1483
+        full
1484
+    } else {
1485
+        compact
1486
+    }
1487
+}
1488
+
14771489
 fn create_modal_value_lines(
14781490
     kind: CreateEventFormRowKind,
14791491
     value: &str,
@@ -1481,7 +1493,9 @@ fn create_modal_value_lines(
14811493
 ) -> Vec<String> {
14821494
     match kind {
14831495
         CreateEventFormRowKind::Multiline => wrap_text_lines(value, value_width),
1484
-        CreateEventFormRowKind::Text | CreateEventFormRowKind::Toggle => {
1496
+        CreateEventFormRowKind::Text
1497
+        | CreateEventFormRowKind::Toggle
1498
+        | CreateEventFormRowKind::Selector => {
14851499
             vec![value.to_string()]
14861500
         }
14871501
     }
@@ -2834,8 +2848,8 @@ mod tests {
28342848
         let rendered = render_app_to_string(&app, 84, 26);
28352849
 
28362850
         assert!(rendered.contains("Day keys"));
2837
-        assert!(rendered.contains("Copy the selected local event"));
2838
-        assert!(rendered.contains("Delete the selected local event"));
2851
+        assert!(rendered.contains("Copy the selected event"));
2852
+        assert!(rendered.contains("Delete the selected event"));
28392853
         assert!(rendered.contains("Move to the previous or next day"));
28402854
     }
28412855
 
@@ -2891,6 +2905,7 @@ mod tests {
28912905
         app.apply(AppAction::OpenCreate);
28922906
         let _ = app.handle_create_key(key(KeyCode::Char('A')));
28932907
         let _ = app.handle_create_key(key(KeyCode::Tab));
2908
+        let _ = app.handle_create_key(key(KeyCode::Tab));
28942909
         let _ = app.handle_create_key(key(KeyCode::Enter));
28952910
 
28962911
         let area = Rect::new(0, 0, 84, 26);
@@ -2902,20 +2917,22 @@ mod tests {
29022917
         let label_width = 12.min(content.width.saturating_sub(1));
29032918
         let value_x = label_x.saturating_add(label_width).saturating_add(1);
29042919
 
2905
-        assert_styled_text(&buffer, content.x, row_y + 1, ">", Color::White);
2920
+        assert_styled_text(&buffer, content.x, row_y + 2, ">", Color::White);
29062921
         assert_styled_text(&buffer, label_x, row_y, "Title", Color::White);
2907
-        assert_styled_text(&buffer, label_x, row_y + 1, "All day", Color::White);
2908
-        assert_styled_text(&buffer, label_x, row_y + 2, "Start date", Color::White);
2922
+        assert_styled_text(&buffer, label_x, row_y + 1, "Calendar", Color::White);
2923
+        assert_styled_text(&buffer, label_x, row_y + 2, "All day", Color::White);
2924
+        assert_styled_text(&buffer, label_x, row_y + 3, "Start date", Color::White);
29092925
         assert_styled_text(&buffer, value_x, row_y, "A", Color::Gray);
2910
-        assert_styled_text(&buffer, value_x, row_y + 2, "2026-04-23", Color::Gray);
2911
-        assert_styled_text(&buffer, value_x, row_y + 3, "09:00", Color::Gray);
2912
-        assert_styled_cell(&buffer, value_x, row_y + 1, "[", Color::Yellow);
2913
-        assert_styled_cell(&buffer, value_x + 1, row_y + 1, "x", Color::White);
2914
-        assert_styled_cell(&buffer, value_x + 2, row_y + 1, "]", Color::Yellow);
2915
-        assert_styled_cell(&buffer, value_x, row_y + 8, "[", Color::Yellow);
2916
-        assert_styled_cell(&buffer, value_x + 1, row_y + 8, " ", Color::White);
2917
-        assert_styled_cell(&buffer, value_x + 2, row_y + 8, "]", Color::Yellow);
2918
-        assert_styled_text(&buffer, value_x + 4, row_y + 8, "5m", Color::Gray);
2926
+        assert_styled_text(&buffer, value_x, row_y + 1, "Local", Color::Gray);
2927
+        assert_styled_text(&buffer, value_x, row_y + 3, "2026-04-23", Color::Gray);
2928
+        assert_styled_text(&buffer, value_x, row_y + 4, "09:00", Color::Gray);
2929
+        assert_styled_cell(&buffer, value_x, row_y + 2, "[", Color::Yellow);
2930
+        assert_styled_cell(&buffer, value_x + 1, row_y + 2, "x", Color::White);
2931
+        assert_styled_cell(&buffer, value_x + 2, row_y + 2, "]", Color::Yellow);
2932
+        assert_styled_cell(&buffer, value_x, row_y + 9, "[", Color::Yellow);
2933
+        assert_styled_cell(&buffer, value_x + 1, row_y + 9, " ", Color::White);
2934
+        assert_styled_cell(&buffer, value_x + 2, row_y + 9, "]", Color::Yellow);
2935
+        assert_styled_text(&buffer, value_x + 4, row_y + 9, "5m", Color::Gray);
29192936
     }
29202937
 
29212938
     #[test]
@@ -2923,7 +2940,7 @@ mod tests {
29232940
         let selected = date(2026, Month::April, 23);
29242941
         let mut app = AppState::new(selected);
29252942
         app.apply(AppAction::OpenCreate);
2926
-        for _ in 0..7 {
2943
+        for _ in 0..8 {
29272944
             let _ = app.handle_create_key(key(KeyCode::Tab));
29282945
         }
29292946
         let notes = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
@@ -2936,7 +2953,7 @@ mod tests {
29362953
         let modal = create_modal_area(area, app.create_form().expect("form stays open"));
29372954
         let content = inset_rect(modal);
29382955
         let row_y = content.y.saturating_add(2);
2939
-        let notes_y = row_y.saturating_add(7);
2956
+        let notes_y = row_y.saturating_add(8);
29402957
         let label_x = content.x.saturating_add(2);
29412958
         let label_width = 12.min(content.width.saturating_sub(1));
29422959
         let value_x = label_x.saturating_add(label_width).saturating_add(1);
@@ -2971,6 +2988,7 @@ mod tests {
29712988
         assert_eq!(lines.len(), 10);
29722989
         assert!(lines[1].contains("Create"));
29732990
         assert!(rendered.contains("Title"));
2991
+        assert!(rendered.contains("Left/Right"));
29742992
         assert!(rendered.contains("Ctrl-S save"));
29752993
     }
29762994