Skip to content

Commit 887947d

Browse files
committed
fix(tui): stream VTE history rows into scrollback
1 parent 47fafe6 commit 887947d

4 files changed

Lines changed: 478 additions & 84 deletions

File tree

codex-rs/tui/src/custom_terminal.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,62 @@ where
452452
Ok(())
453453
}
454454

455+
/// Insert fully-rendered rows above the inline viewport without using scroll regions.
456+
pub(crate) fn insert_buffer_before_viewport_without_scroll_region(
457+
&mut self,
458+
buffer: Buffer,
459+
) -> io::Result<()> {
460+
let height = buffer.area.height;
461+
if height == 0 {
462+
return Ok(());
463+
}
464+
465+
let screen_height = self.size()?.height;
466+
if screen_height == 0 {
467+
return Ok(());
468+
}
469+
470+
if self.viewport_area.bottom().saturating_add(height) <= screen_height {
471+
self.draw_buffer_lines(
472+
self.viewport_area.top(),
473+
height,
474+
buffer.area.width,
475+
buffer.content.as_slice(),
476+
)?;
477+
self.set_viewport_area(Rect {
478+
y: self.viewport_area.y.saturating_add(height),
479+
..self.viewport_area
480+
});
481+
self.clear()?;
482+
return Ok(());
483+
}
484+
485+
let mut cells = buffer.content.as_slice();
486+
let bottom = screen_height.saturating_sub(1);
487+
// VTE records scrollback when a row scrolls offscreen, not when that row is later
488+
// repainted by absolute cursor movement. Stream each rendered row through the bottom
489+
// line so the terminal saves the row's actual cells into history.
490+
while !cells.is_empty() {
491+
cells =
492+
self.draw_buffer_lines(bottom, /*lines_to_draw*/ 1, buffer.area.width, cells)?;
493+
self.scroll_up_with_append_lines(/*lines_to_scroll*/ 1)?;
494+
}
495+
496+
let viewport_height = self.viewport_area.height.min(screen_height);
497+
for _ in 1..viewport_height {
498+
self.scroll_up_with_append_lines(/*lines_to_scroll*/ 1)?;
499+
}
500+
501+
self.set_viewport_area(Rect {
502+
y: screen_height.saturating_sub(viewport_height),
503+
height: viewport_height,
504+
..self.viewport_area
505+
});
506+
self.clear()?;
507+
508+
Ok(())
509+
}
510+
455511
/// Clear the entire visible screen (not just the viewport) and force a full redraw.
456512
pub fn clear_visible_screen(&mut self) -> io::Result<()> {
457513
let home = Position { x: 0, y: 0 };
@@ -507,6 +563,37 @@ where
507563
pub fn size(&self) -> io::Result<Size> {
508564
self.backend.size()
509565
}
566+
567+
fn draw_buffer_lines<'a>(
568+
&mut self,
569+
y_offset: u16,
570+
lines_to_draw: u16,
571+
width: u16,
572+
cells: &'a [Cell],
573+
) -> io::Result<&'a [Cell]> {
574+
let width = usize::from(width);
575+
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
576+
if lines_to_draw > 0 {
577+
let iter = to_draw
578+
.iter()
579+
.enumerate()
580+
.map(|(i, cell)| ((i % width) as u16, y_offset + (i / width) as u16, cell));
581+
self.backend.draw(iter)?;
582+
Backend::flush(&mut self.backend)?;
583+
}
584+
Ok(remainder)
585+
}
586+
587+
fn scroll_up_with_append_lines(&mut self, lines_to_scroll: u16) -> io::Result<()> {
588+
if lines_to_scroll > 0 {
589+
self.backend.set_cursor_position(Position::new(
590+
/*x*/ 0,
591+
/*y*/ self.size()?.height.saturating_sub(1),
592+
))?;
593+
self.backend.append_lines(lines_to_scroll)?;
594+
}
595+
Ok(())
596+
}
510597
}
511598

512599
use ratatui::buffer::Cell;

0 commit comments

Comments
 (0)