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 <Tab> and <BS>, and a regression test adds
work both from the Days list and by typing another date.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
skkeyeandClaude Opus 5.5 committed 2026-09-25 00:01:40 -04:00
1 parent 38f14fa453
commit 9257181a77
2 files changed
+60 -24

No files matched your search

+29
View File
@@ -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<jiff::civil::Date> {
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:00<Tab>19:00<CR>");
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:00<Tab>19:00<Tab>{}{other_day}<CR>", "<BS>".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");
+31 -24
View File
@@ -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<Date, String> {
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<Resolved, String> {
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 })
}