From f3dcd5acbc414fdb774351a9fc9113f5ab55eff0 Mon Sep 17 00:00:00 2001 From: Ian Keane Date: Mon, 1 Dec 2025 13:31:53 -0500 Subject: [PATCH] Fifteen-minute rounding --- src/app.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++-------- src/ui.rs | 9 ++++++++- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/app.rs b/src/app.rs index ddf686d..732b94a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -68,6 +68,7 @@ pub struct App { pub log_view_frame_indices: Vec, // Maps display order to frame index pub log_view_scroll: usize, pub log_view_selected: usize, + pub log_view_rounded: bool, // Toggle for 15-minute rounding display pub help_scroll: usize, pub clipboard: Option, pub status_message: Option<(String, std::time::Instant)>, @@ -81,6 +82,24 @@ pub enum NewEntryMode { } impl App { + // Helper to round a time to the nearest 15-minute interval + fn round_to_15min(dt: &chrono::DateTime) -> chrono::DateTime { + use chrono::{Timelike, Duration}; + + let minutes = dt.minute(); + let rounded_minutes = ((minutes + 7) / 15) * 15; // Round to nearest 15 + + let mut rounded = dt.with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap(); + rounded = rounded + Duration::minutes(rounded_minutes as i64); + + // Handle hour overflow (e.g., 50 minutes rounds to 60) + if rounded_minutes >= 60 { + rounded = rounded.with_minute(0).unwrap(); + } + + rounded + } + // Helper to determine if a line is an entry line based on grouping mode fn is_entry_line(&self, line: &str) -> bool { // Exclude separator lines (time gap markers) @@ -130,6 +149,7 @@ impl App { log_view_frame_indices: Vec::new(), log_view_scroll: 0, log_view_selected: 0, + log_view_rounded: false, help_scroll: 0, clipboard: arboard::Clipboard::new().ok(), status_message: None, @@ -560,6 +580,12 @@ impl App { self.format_log_entries(); self.needs_clear = true; } + KeyCode::Char('R') => { + // Toggle 15-minute rounding for display + self.log_view_rounded = !self.log_view_rounded; + self.format_log_entries(); + self.needs_clear = true; + } KeyCode::PageDown => { self.log_view_selected = (self.log_view_selected + 10) .min(self.log_view_frame_indices.len().saturating_sub(1)); @@ -644,6 +670,13 @@ impl App { let start_local: DateTime = start_dt.into(); let stop_local: DateTime = stop_dt.into(); + // Apply rounding if enabled + let (display_start, display_stop) = if self.log_view_rounded { + (Self::round_to_15min(&start_local), Self::round_to_15min(&stop_local)) + } else { + (start_local, stop_local) + }; + // Check for time gap and add separator line if enabled if self.config.show_time_gaps && frame_idx > 0 { if let Some(prev) = prev_stop { @@ -655,11 +688,11 @@ impl App { } } - let start_time = format!("{:02}:{:02}", start_local.hour(), start_local.minute()); - let stop_time = format!("{:02}:{:02}", stop_local.hour(), stop_local.minute()); + let start_time = format!("{:02}:{:02}", display_start.hour(), display_start.minute()); + let stop_time = format!("{:02}:{:02}", display_stop.hour(), display_stop.minute()); - // Calculate duration - let duration = stop_local.signed_duration_since(start_local); + // Calculate duration (use rounded times if enabled) + let duration = display_stop.signed_duration_since(display_start); let hours = duration.num_hours(); let minutes = duration.num_minutes() % 60; let duration_str = if hours > 0 { @@ -755,11 +788,18 @@ impl App { let start_local: DateTime = start_dt.into(); let stop_local: DateTime = stop_dt.into(); - let start_time = format!("{:02}:{:02}", start_local.hour(), start_local.minute()); - let stop_time = format!("{:02}:{:02}", stop_local.hour(), stop_local.minute()); + // Apply rounding if enabled + let (display_start, display_stop) = if self.log_view_rounded { + (Self::round_to_15min(&start_local), Self::round_to_15min(&stop_local)) + } else { + (start_local, stop_local) + }; - // Calculate duration - let duration = stop_local.signed_duration_since(start_local); + let start_time = format!("{:02}:{:02}", display_start.hour(), display_start.minute()); + let stop_time = format!("{:02}:{:02}", display_stop.hour(), display_stop.minute()); + + // Calculate duration (use rounded times if enabled) + let duration = display_stop.signed_duration_since(display_start); let hours = duration.num_hours(); let minutes = duration.num_minutes() % 60; let duration_str = if hours > 0 { diff --git a/src/ui.rs b/src/ui.rs index 9dcab36..1f71251 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -692,7 +692,8 @@ fn render_log_view(frame: &mut Frame, app: &App) { LogViewDayOrder::ReverseChronological => "↓", }; - let title = format!("Watson Log - {} View ({}) [{}]", period_str, grouping_str, order_str); + let rounded_indicator = if app.log_view_rounded { " [ROUNDED]" } else { "" }; + let title = format!("Watson Log - {} View ({}) [{}]{}", period_str, grouping_str, order_str, rounded_indicator); let block = Block::default() .title(title) @@ -939,6 +940,12 @@ fn render_log_view_help(frame: &mut Frame, app: &App) { " - Days are shown in the chosen order", " - Entries within each day are always chronological (earliest to latest)", "", + "Display Mode:", + "- R: Toggle 15-minute rounding (for estimates/reports)", + " - When enabled, all times are rounded to nearest 15-minute interval", + " - Shows [ROUNDED] indicator in title bar", + " - Affects display and clipboard copy, does not modify Watson data", + "", "Navigation:", "- j/k or ↑/↓: Navigate selection", " - At Entry level: Move to next/previous entry",