aboutsummaryrefslogtreecommitdiff
path: root/src/pane.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-03-31 20:49:05 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-03-31 20:49:05 +0300
commit41e009f9b7b46d335d41b365e41f9ad57d571ecb (patch)
tree42b3be90c336ad6d4487e9f86abc56ebb40ba210 /src/pane.rs
parentf826fb78dcac89b4ba23e399ac5d141459c14ae9 (diff)
fix alt-screen scrollback handling
Diffstat (limited to 'src/pane.rs')
-rw-r--r--src/pane.rs56
1 files changed, 50 insertions, 6 deletions
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<PtyResponses>) -> usize {
max_scrollback
}
-fn recent_text_from_parser(parser: &mut vt100::Parser<PtyResponses>, lines: usize) -> String {
+fn parser_rows(parser: &mut vt100::Parser<PtyResponses>, lines: usize) -> Vec<String> {
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<PtyResponses>, 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<PtyResponses>, lines: usiz
}
}
+fn recent_text_from_parser(parser: &mut vt100::Parser<PtyResponses>, 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<ScrollMetrics> {
+ pub fn scroll_state(&self) -> Option<ScrollState> {
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<ScrollMetrics> {
+ self.scroll_state().map(|state| state.metrics)
+ }
+
pub fn visible_text(&self) -> String {
let Ok(content) = self.screen_content.read() else {
return String::new();
@@ -637,6 +659,28 @@ mod tests {
}
#[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();
let mut last_working = None;