From 9257181a7727664f39cb7c0f86bd5527668141fc Mon Sep 17 00:00:00 2001 From: Skkeye Date: Fri, 25 Sep 2026 00:01:40 -0400 Subject: [PATCH] Fix added work landing on the wrong day When another date was typed in the segment editor's Work day field, the segment was filed under that date but its times were still placed around the original day (usually today). A segment filed under Sep 14 could run on Sep 24. The editor now reads the Work day field first and places the typed times on that day: next to its existing work, or around noon on an empty day. Moving an existing segment to another work day moves its times with it, keeping the same clock times. The test helper also learns and , and a regression test adds work both from the Days list and by typing another date. Co-Authored-By: Claude Opus 5.5 --- src/app.rs | 29 +++++++++++++++++ src/ui/popups/segment_editor.rs | 55 +++++++++++++++++++-------------- 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/src/app.rs b/src/app.rs index 6b55ca6..0296365 100644 --- a/src/app.rs +++ b/src/app.rs @@ -832,6 +832,8 @@ mod tests { Some("CR") => (KeyCode::Enter, 4), Some("Esc") => (KeyCode::Esc, 5), Some("Space") => (KeyCode::Char(' '), 7), + Some("Tab") => (KeyCode::Tab, 5), + Some("BS") => (KeyCode::Backspace, 4), _ => (KeyCode::Char(c), c.len_utf8()), }; app.handle(Event::Key(KeyEvent::new(code, KeyModifiers::NONE))); @@ -877,6 +879,33 @@ mod tests { } } + /// The dates of the segments starting at `hour` on the work day `day`. + fn dates_of_segments_at(app: &App, day: jiff::civil::Date, hour: i8) -> Vec { + let segments = app.model.journal.day(day).map(|day| day.segments().to_vec()).unwrap_or_default(); + segments + .iter() + .map(|segment| app.model.clock.local(segment.start())) + .filter(|start| start.hour() == hour) + .map(|start| start.date()) + .collect() + } + + #[test] + fn added_work_lands_on_the_chosen_day() { + // Choosing the day in the Days list. + let mut app = sample_app("add-from-list"); + // The list is newest first; `j` selects the second day. + let second_day = app.model.journal.days().rev().nth(1).unwrap().date(); + press(&mut app, "gdjo18:0019:00"); + assert_eq!(dates_of_segments_at(&app, second_day, 18), vec![second_day]); + + // Typing another date in the Work day field: the times follow it. + let mut app = sample_app("add-typed-day"); + let other_day = app.model.clock.now().date().saturating_sub(jiff::ToSpan::days(10)); + press(&mut app, &format!("o18:0019:00{}{other_day}", "".repeat(10))); + assert_eq!(dates_of_segments_at(&app, other_day, 18), vec![other_day]); + } + #[test] fn editing_through_the_keys() { let mut app = sample_app("editing"); diff --git a/src/ui/popups/segment_editor.rs b/src/ui/popups/segment_editor.rs index ded48d3..55ed925 100644 --- a/src/ui/popups/segment_editor.rs +++ b/src/ui/popups/segment_editor.rs @@ -5,7 +5,7 @@ //! "22:00 to 01:30" visibly ends the next day. use jiff::civil::Date; -use jiff::{Timestamp, Zoned}; +use jiff::{Timestamp, ToSpan, Zoned}; use ratatui::Frame; use ratatui::crossterm::event::KeyEvent; use ratatui::layout::Rect; @@ -88,27 +88,43 @@ impl SegmentEditor { } } - /// Where typed start times are placed: close to this moment. - fn reference(&self, ctx: &Context) -> Zoned { - let day_edge = |day: Date, after: bool| { - let work_day = ctx.day(day); - let edge = if after { - work_day.and_then(WorkDay::last_end) - } else { - work_day.and_then(WorkDay::first_start) - }; - edge.map_or_else(|| ctx.noon(day), |moment| ctx.local(moment)) - }; + /// The work day the segment is filed under (the Work day field). + fn work_day(&self) -> Result { match self.target { - Target::New { day, after } => day_edge(day, after), - Target::Existing { original, .. } => ctx.local(original.start()), + Target::Running { day, .. } => Ok(day), + _ => self.day.text().trim().parse().map_err(|_| "work day: write it like 2026-09-24".to_string()), + } + } + + /// Where typed start times are placed: close to this moment, on the + /// work day `day` (the one in the Work day field). + fn reference(&self, day: Date, ctx: &Context) -> Zoned { + match self.target { + // Next to the day's existing work, or around noon on an empty day. + Target::New { after, .. } => { + let work_day = ctx.day(day); + let edge = if after { + work_day.and_then(WorkDay::last_end) + } else { + work_day.and_then(WorkDay::first_start) + }; + edge.map_or_else(|| ctx.noon(day), |moment| ctx.local(moment)) + } + // Moving a segment to another work day moves its times with it, + // keeping the same clock times. + Target::Existing { day: original_day, original, .. } => { + let moved_days = day.duration_since(original_day).as_hours() / 24; + let start = ctx.local(original.start()); + start.checked_add(moved_days.days()).unwrap_or(start) + } Target::Running { since, .. } => ctx.local(since), } } fn resolve(&self, ctx: &Context) -> Result { + let day = self.work_day()?; let start_time = parse_time(self.start.text()).map_err(|error| format!("start: {error}"))?; - let start = nearest(start_time, &self.reference(ctx)); + let start = nearest(start_time, &self.reference(day, ctx)); let end = match self.end.text().trim() { "" if self.is_running() => None, "" => return Err("end: when did it end?".into()), @@ -117,15 +133,6 @@ impl SegmentEditor { Some(next_after(end_time, &start)) } }; - let day = match self.target { - Target::Running { day, .. } => day, - _ => self - .day - .text() - .trim() - .parse() - .map_err(|_| "work day: write it like 2026-09-24".to_string())?, - }; Ok(Resolved { start, end, day }) }