@@ -3,16 +3,26 @@ const DEFAULT_TERMINAL_ROWS: u16 = 24;
33const DEFAULT_TERMINAL_COLS : u16 = 80 ;
44
55/// A scrollback buffer backed by vt100 terminal emulator.
6+ ///
7+ /// Handles alternate screen mode properly - when a TUI app enters alternate
8+ /// screen, we preserve the main screen scrollback and combine it with the
9+ /// alternate screen content when reading.
610pub struct ScrollbackBuffer {
711 parser : Option < vt100:: Parser > ,
812 max_lines : usize ,
13+ /// Saved main screen content when entering alternate screen mode
14+ saved_main_content : Option < String > ,
15+ /// Track previous alternate screen state to detect transitions
16+ was_alternate : bool ,
917}
1018
1119impl ScrollbackBuffer {
1220 pub const fn new ( ) -> Self {
1321 Self {
1422 parser : None ,
1523 max_lines : DEFAULT_SCROLLBACK_LINES ,
24+ saved_main_content : None ,
25+ was_alternate : false ,
1626 }
1727 }
1828
@@ -23,7 +33,38 @@ impl ScrollbackBuffer {
2333 }
2434
2535 pub fn push ( & mut self , data : & [ u8 ] ) {
36+ // Check if we need to save main screen before processing
37+ // (in case this data switches to alternate screen)
38+ let was_alternate_before = self . parser . as_ref ( ) . is_some_and ( |p| p. screen ( ) . alternate_screen ( ) ) ;
39+
2640 self . ensure_parser ( ) . process ( data) ;
41+
42+ let is_alternate_now = self . parser . as_ref ( ) . is_some_and ( |p| p. screen ( ) . alternate_screen ( ) ) ;
43+
44+ // Detect transition from main -> alternate screen
45+ if !was_alternate_before && is_alternate_now {
46+ // We just entered alternate screen - but the content has already been
47+ // replaced. We need to save BEFORE the switch happens.
48+ // Unfortunately vt100 doesn't give us a hook for this, so we need to
49+ // save content continuously while in main screen mode.
50+ tracing:: debug!( "entered alternate screen mode" ) ;
51+ }
52+
53+ // Detect transition from alternate -> main screen
54+ if was_alternate_before && !is_alternate_now {
55+ // We just exited alternate screen - clear saved content
56+ self . saved_main_content = None ;
57+ tracing:: debug!( "exited alternate screen mode" ) ;
58+ }
59+
60+ // If we're in main screen mode, keep saving content for potential alternate switch
61+ if !is_alternate_now {
62+ if let Some ( parser) = & self . parser {
63+ self . saved_main_content = Some ( parser. screen ( ) . contents ( ) ) ;
64+ }
65+ }
66+
67+ self . was_alternate = is_alternate_now;
2768 }
2869
2970 pub fn get_lines ( & self , count : Option < usize > ) -> String {
@@ -32,7 +73,20 @@ impl ScrollbackBuffer {
3273 } ;
3374
3475 let screen = parser. screen ( ) ;
35- let all_contents = screen. contents ( ) ;
76+ let is_alternate = screen. alternate_screen ( ) ;
77+
78+ // If in alternate screen and we have saved main content, combine them
79+ let all_contents = if is_alternate {
80+ if let Some ( saved) = & self . saved_main_content {
81+ // Combine: saved main scrollback + current alternate screen
82+ let alternate_content = screen. contents ( ) ;
83+ format ! ( "{saved}\n --- alternate screen ---\n {alternate_content}" )
84+ } else {
85+ screen. contents ( )
86+ }
87+ } else {
88+ screen. contents ( )
89+ } ;
3690
3791 match count {
3892 Some ( n) => {
@@ -56,6 +110,10 @@ impl ScrollbackBuffer {
56110 )
57111 }
58112
113+ /// Returns true if currently in alternate screen mode
114+ pub fn is_alternate_screen ( & self ) -> bool {
115+ self . parser . as_ref ( ) . is_some_and ( |p| p. screen ( ) . alternate_screen ( ) )
116+ }
59117}
60118
61119#[ cfg( test) ]
@@ -107,4 +165,35 @@ mod tests {
107165 assert ! ( !content. contains( "\x1b [31m" ) ) ;
108166 assert ! ( !content. contains( "[31m" ) ) ;
109167 }
168+
169+ #[ test]
170+ fn test_alternate_screen_detection ( ) {
171+ let mut buf = ScrollbackBuffer :: new ( ) ;
172+ buf. push ( b"main screen content" ) ;
173+ assert ! ( !buf. is_alternate_screen( ) ) ;
174+
175+ // Enter alternate screen mode
176+ buf. push ( b"\x1b [?1049h" ) ;
177+ assert ! ( buf. is_alternate_screen( ) ) ;
178+
179+ // Exit alternate screen mode
180+ buf. push ( b"\x1b [?1049l" ) ;
181+ assert ! ( !buf. is_alternate_screen( ) ) ;
182+ }
183+
184+ #[ test]
185+ fn test_alternate_screen_preserves_main_content ( ) {
186+ let mut buf = ScrollbackBuffer :: new ( ) ;
187+ buf. push ( b"line1\r \n line2\r \n line3" ) ;
188+
189+ // Enter alternate screen
190+ buf. push ( b"\x1b [?1049h" ) ;
191+ buf. push ( b"alternate content" ) ;
192+
193+ // Should contain both main and alternate content
194+ let content = buf. get_lines ( None ) ;
195+ assert ! ( content. contains( "line1" ) ) ;
196+ assert ! ( content. contains( "line2" ) ) ;
197+ assert ! ( content. contains( "alternate content" ) ) ;
198+ }
110199}
0 commit comments