From 9b7c228134fc56ba69944b4afc27b83136dede0f Mon Sep 17 00:00:00 2001 From: Dhanji Prasanna Date: Sat, 4 Oct 2025 15:05:06 +1000 Subject: [PATCH] scroll hack --- Cargo.lock | 17 ++++++++ Cargo.toml | 2 +- crates/g3-cli/src/retro_tui.rs | 72 +++++++++++++++++++++++++--------- 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 467f126..b2a2e16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2877,6 +2877,16 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "term_size" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "termimad" version = "0.34.0" @@ -2893,6 +2903,13 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "test_scroll" +version = "0.1.0" +dependencies = [ + "term_size", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index 76a274e..b662ef3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = [ "crates/g3-providers", "crates/g3-config", "crates/g3-execution" -] +, "tmp"] resolver = "2" [workspace.dependencies] diff --git a/crates/g3-cli/src/retro_tui.rs b/crates/g3-cli/src/retro_tui.rs index bf5033c..3481936 100644 --- a/crates/g3-cli/src/retro_tui.rs +++ b/crates/g3-cli/src/retro_tui.rs @@ -28,6 +28,9 @@ const TERMINAL_PALE_BLUE: Color = Color::Rgb(173, 234, 251); // Pale blue for RE const TERMINAL_DARK_AMBER: Color = Color::Rgb(204, 119, 34); // Dark amber for PROCESSING status const TERMINAL_WHITE: Color = Color::Rgb(218, 218, 219); // Dimmer white for punchy text +// Scrolling configuration +const SCROLL_PAST_END_BUFFER: usize = 10; // Extra lines to allow scrolling past the end + /// Message types for communication between threads #[derive(Debug, Clone)] pub enum TuiMessage { @@ -93,7 +96,7 @@ impl TerminalState { ], scroll_offset: 0, cursor_blink: true, - last_visible_height: 20, // Default estimate + last_visible_height: 0, // Will be set on first draw manual_scroll: false, last_blink: Instant::now(), status_line: "READY".to_string(), @@ -185,9 +188,15 @@ impl TerminalState { let total_lines = self.output_history.len(); let visible_height = self.last_visible_height.max(1); - // Calculate scroll to show the last visible_height lines + // Calculate scroll to ensure ALL lines including the last are visible if total_lines > visible_height { - self.scroll_offset = total_lines.saturating_sub(visible_height); + // The problem: we want to show lines from scroll_offset to scroll_offset + visible_height - 1 + // To see the last line (at index total_lines - 1), we need: + // scroll_offset + visible_height - 1 >= total_lines - 1 + // scroll_offset >= total_lines - visible_height + // But we also need to ensure we're not cutting off content + // So we add 1 to ensure the last line is fully visible + self.scroll_offset = total_lines.saturating_sub(visible_height.saturating_sub(1)); } else { self.scroll_offset = 0; } @@ -235,9 +244,15 @@ impl TerminalState { let total_lines = self.output_history.len(); let visible_height = self.last_visible_height.max(1); - // Calculate scroll to show the last visible_height lines + // Calculate scroll to ensure ALL lines including the last are visible if total_lines > visible_height { - self.scroll_offset = total_lines.saturating_sub(visible_height); + // The problem: we want to show lines from scroll_offset to scroll_offset + visible_height - 1 + // To see the last line (at index total_lines - 1), we need: + // scroll_offset + visible_height - 1 >= total_lines - 1 + // scroll_offset >= total_lines - visible_height + // But we also need to ensure we're not cutting off content + // So we add 1 to ensure the last line is fully visible + self.scroll_offset = total_lines.saturating_sub(visible_height.saturating_sub(1)); } else { self.scroll_offset = 0; } @@ -397,7 +412,7 @@ impl RetroTui { ) -> Result<()> { terminal.draw(|f| { let size = f.area(); - + // Create main layout - header, input, output let chunks = Layout::default() .direction(Direction::Vertical) @@ -408,10 +423,26 @@ impl RetroTui { ]) .split(size); - // Update the last known visible height for the output area - // This will be used for page up/down calculations - state.last_visible_height = chunks[1].height.saturating_sub(2) as usize; + // IMPORTANT: Update the last known visible height BEFORE drawing + // This ensures auto-scroll calculations use the correct height + let old_height = state.last_visible_height; + // Calculate the actual visible height accounting for borders (2 lines) + let new_visible_height = chunks[1].height.saturating_sub(2) as usize; + + // Only update if we have a valid height + if new_visible_height > 0 { + state.last_visible_height = new_visible_height; + } + // If the height changed and we're auto-scrolling, recalculate scroll position + if old_height != state.last_visible_height && !state.manual_scroll { + let total_lines = state.output_history.len(); + if total_lines > state.last_visible_height { + // Recalculate to show the bottom content + state.scroll_offset = total_lines.saturating_sub(state.last_visible_height); + } + } + // Draw header/input area Self::draw_input_area(f, chunks[0], &state.input_buffer, state.cursor_blink); @@ -471,13 +502,13 @@ impl RetroTui { // If all content fits, no scrolling needed 0 } else { - // Ensure scroll offset doesn't leave empty space at the bottom - // The maximum scroll position should be such that the last line is at the bottom of the viewport - let max_scroll = total_lines.saturating_sub(visible_height); + // Allow scrolling SCROLL_PAST_END_BUFFER lines past the normal end + // This provides a buffer to ensure no content is cut off + let max_scroll_with_buffer = total_lines.saturating_sub(visible_height).saturating_add(SCROLL_PAST_END_BUFFER); // If the requested scroll would show past the end, adjust it - if scroll_offset > max_scroll { - max_scroll + if scroll_offset > max_scroll_with_buffer { + max_scroll_with_buffer } else { scroll_offset } @@ -730,8 +761,9 @@ impl RetroTui { let visible_height = state.last_visible_height.max(1); // Calculate max scroll position - // Ensure we can scroll to see all content - let max_scroll = total_lines.saturating_sub(visible_height); + // Allow scrolling SCROLL_PAST_END_BUFFER lines past what would normally be the end + // This gives some buffer space at the bottom + let max_scroll = total_lines.saturating_sub(visible_height).saturating_add(SCROLL_PAST_END_BUFFER); state.scroll_offset = (state.scroll_offset + 1).min(max_scroll); } @@ -768,7 +800,8 @@ impl RetroTui { }; // Calculate max scroll position - let max_scroll = total_lines.saturating_sub(visible_height); + // Allow scrolling SCROLL_PAST_END_BUFFER lines past what would normally be the end + let max_scroll = total_lines.saturating_sub(visible_height).saturating_add(SCROLL_PAST_END_BUFFER); // Scroll down by a page, but don't go past the end state.scroll_offset = (state.scroll_offset + page_size).min(max_scroll); @@ -786,8 +819,9 @@ impl RetroTui { let total_lines = state.output_history.len(); let visible_height = state.last_visible_height.max(1); - // Scroll to show the last page of content - state.scroll_offset = total_lines.saturating_sub(visible_height); + // Scroll to show the last page of content plus SCROLL_PAST_END_BUFFER extra lines + // This ensures we can see past the end a bit for safety + state.scroll_offset = total_lines.saturating_sub(visible_height).saturating_add(SCROLL_PAST_END_BUFFER); // When scrolling to end, disable manual scroll so auto-scroll resumes state.manual_scroll = false;