From 41e009f9b7b46d335d41b365e41f9ad57d571ecb Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Tue, 31 Mar 2026 20:49:05 +0300 Subject: fix alt-screen scrollback handling --- src/pane.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 6 deletions(-) (limited to 'src/pane.rs') diff --git a/src/pane.rs b/src/pane.rs index c6b8e175..f25c6a42 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -100,6 +100,12 @@ pub struct ScrollMetrics { pub viewport_rows: usize, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ScrollState { + pub metrics: ScrollMetrics, + pub alternate_screen: bool, +} + impl Drop for PaneRuntime { fn drop(&mut self) { // Abort detection task immediately. @@ -124,7 +130,7 @@ fn max_scrollback(parser: &mut vt100::Parser) -> usize { max_scrollback } -fn recent_text_from_parser(parser: &mut vt100::Parser, lines: usize) -> String { +fn parser_rows(parser: &mut vt100::Parser, lines: usize) -> Vec { let max_scrollback = max_scrollback(parser); let screen = parser.screen_mut(); let original_scrollback = screen.scrollback(); @@ -150,7 +156,10 @@ fn recent_text_from_parser(parser: &mut vt100::Parser, lines: usiz screen.set_scrollback(original_scrollback); rows.extend(visible_rows); trim_trailing_blank_rows(&mut rows); + rows +} +fn recent_text_from_rows(rows: &[String], lines: usize) -> String { let start = rows.len().saturating_sub(lines); let text = rows[start..].join("\n"); if text.is_empty() { @@ -160,6 +169,11 @@ fn recent_text_from_parser(parser: &mut vt100::Parser, lines: usiz } } +fn recent_text_from_parser(parser: &mut vt100::Parser, lines: usize) -> String { + let rows = parser_rows(parser, lines); + recent_text_from_rows(&rows, lines) +} + fn wait_for_processes_to_exit(pids: &[u32], timeout: std::time::Duration) -> bool { let deadline = std::time::Instant::now() + timeout; loop { @@ -324,6 +338,7 @@ impl PaneRuntime { Ok(n) => { if let Ok(mut p) = parser.write() { p.process(&buf[..n]); + // Snapshot live screen content for detection. // Always reads at scrollback 0 (current view), // without touching the user's scroll position. @@ -557,20 +572,27 @@ impl PaneRuntime { } } - pub fn scroll_metrics(&self) -> Option { + pub fn scroll_state(&self) -> Option { let Ok(mut parser) = self.parser.write() else { return None; }; let max_offset_from_bottom = max_scrollback(&mut parser); let screen = parser.screen(); let (viewport_rows, _) = screen.size(); - Some(ScrollMetrics { - offset_from_bottom: screen.scrollback(), - max_offset_from_bottom, - viewport_rows: viewport_rows as usize, + Some(ScrollState { + metrics: ScrollMetrics { + offset_from_bottom: screen.scrollback(), + max_offset_from_bottom, + viewport_rows: viewport_rows as usize, + }, + alternate_screen: screen.alternate_screen(), }) } + pub fn scroll_metrics(&self) -> Option { + self.scroll_state().map(|state| state.metrics) + } + pub fn visible_text(&self) -> String { let Ok(content) = self.screen_content.read() else { return String::new(); @@ -636,6 +658,28 @@ mod tests { assert_eq!(rows, vec!["hello".to_string()]); } + #[test] + fn alternate_screen_accumulates_its_own_scrollback() { + let responses = PtyResponses::new(); + let mut parser = vt100::Parser::new_with_callbacks(2, 10, 100, responses); + parser.process(b"\x1b[?1049h1\r\n2\r\n3"); + + assert!(parser.screen().alternate_screen()); + assert_eq!(max_scrollback(&mut parser), 1); + assert_eq!(recent_text_from_parser(&mut parser, 3), "1\n2\n3\n"); + } + + #[test] + fn top_anchored_scroll_regions_feed_scrollback() { + let responses = PtyResponses::new(); + let mut parser = vt100::Parser::new_with_callbacks(5, 10, 100, responses); + parser.process(b"\x1b[?1049h1\r\n2\r\n3\r\n4\r\n5"); + parser.process(b"\x1b[1;3r\x1b[3;1H\r\nX"); + + assert!(parser.screen().alternate_screen()); + assert_eq!(max_scrollback(&mut parser), 1); + } + #[test] fn claude_working_is_sticky_for_short_gap() { let now = std::time::Instant::now(); -- cgit v1.2.3