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 }) }