wip(tui): avoid blank prefill during resize replay

This commit is contained in:
Felipe Coury
2026-04-23 18:41:25 -03:00
parent 946870ea25
commit 29e6d132d8
2 changed files with 100 additions and 77 deletions
+99 -76
View File
@@ -46,7 +46,10 @@ use ratatui::text::Span;
pub enum InsertHistoryMode {
Standard,
Zellij,
FullScreenReplay,
FullScreenReplayPrefill,
// Kept available while validating terminal-specific replay behavior.
#[allow(dead_code)]
FullScreenReplayDirect,
}
impl InsertHistoryMode {
@@ -78,9 +81,10 @@ where
/// emits newlines at the screen bottom to create space (since Zellij ignores scroll
/// region escapes) and writes lines at computed absolute positions. Both modes
/// update `terminal.viewport_area` so subsequent draw passes know where the
/// viewport moved to. In `FullScreenReplay` mode every line is replayed through
/// normal full-screen scrolling so terminals that drop scroll-region output from
/// scrollback can still rebuild deep history after a resize.
/// viewport moved to. In `FullScreenReplayPrefill` mode blank rows are used to
/// create scrollback space before replaying content, matching xterm.js behavior.
/// `FullScreenReplayDirect` keeps the no-prefill strategy isolated for terminals
/// where the prefilled blank rows become user-visible scrollback.
pub fn insert_history_lines_with_mode<B>(
terminal: &mut crate::custom_terminal::Terminal<B>,
lines: Vec<Line>,
@@ -126,88 +130,103 @@ where
}
let wrapped_lines = wrapped_rows as u16;
if matches!(
mode,
InsertHistoryMode::Zellij | InsertHistoryMode::FullScreenReplay
) {
let space_below = screen_size.height.saturating_sub(area.bottom());
let shift_down = if matches!(mode, InsertHistoryMode::Zellij) {
wrapped_lines.min(space_below)
} else {
0
};
let scroll_up_amount = wrapped_lines.saturating_sub(shift_down);
match mode {
InsertHistoryMode::Zellij | InsertHistoryMode::FullScreenReplayPrefill => {
let space_below = screen_size.height.saturating_sub(area.bottom());
let shift_down = if matches!(mode, InsertHistoryMode::Zellij) {
wrapped_lines.min(space_below)
} else {
0
};
let scroll_up_amount = wrapped_lines.saturating_sub(shift_down);
if scroll_up_amount > 0 {
// Scroll the entire screen up by emitting \n at the bottom
queue!(writer, MoveTo(0, screen_size.height.saturating_sub(1)))?;
for _ in 0..scroll_up_amount {
queue!(writer, Print("\n"))?;
if scroll_up_amount > 0 {
// Scroll the entire screen up by emitting \n at the bottom
queue!(
writer,
MoveTo(/*x*/ 0, screen_size.height.saturating_sub(1))
)?;
for _ in 0..scroll_up_amount {
queue!(writer, Print("\n"))?;
}
}
if shift_down > 0 {
area.y += shift_down;
should_update_area = true;
}
let cursor_top = area.top().saturating_sub(scroll_up_amount + shift_down);
queue!(writer, MoveTo(/*x*/ 0, cursor_top))?;
for (i, line) in wrapped.iter().enumerate() {
if i > 0 {
queue!(writer, Print("\r\n"))?;
}
write_history_line(writer, line, wrap_width)?;
}
}
InsertHistoryMode::FullScreenReplayDirect => {
// Rebuild scrollback with transcript content itself. Pre-scrolling with blank
// newlines creates visible empty scrollback in terminals such as Terminal.app.
queue!(writer, MoveTo(/*x*/ 0, /*y*/ 0))?;
if shift_down > 0 {
area.y += shift_down;
should_update_area = true;
for (i, line) in wrapped.iter().enumerate() {
if i > 0 {
queue!(writer, Print("\r\n"))?;
}
write_history_line(writer, line, wrap_width)?;
}
}
InsertHistoryMode::Standard => {
let cursor_top = if area.bottom() < screen_size.height {
let scroll_amount = wrapped_lines.min(screen_size.height - area.bottom());
let cursor_top = area.top().saturating_sub(scroll_up_amount + shift_down);
queue!(writer, MoveTo(0, cursor_top))?;
let top_1based = area.top() + 1;
queue!(writer, SetScrollRegion(top_1based..screen_size.height))?;
queue!(writer, MoveTo(/*x*/ 0, area.top()))?;
for _ in 0..scroll_amount {
queue!(writer, Print("\x1bM"))?;
}
queue!(writer, ResetScrollRegion)?;
for (i, line) in wrapped.iter().enumerate() {
if i > 0 {
let cursor_top = area.top().saturating_sub(1);
area.y += scroll_amount;
should_update_area = true;
cursor_top
} else {
area.top().saturating_sub(1)
};
// Limit the scroll region to the lines from the top of the screen to the
// top of the viewport. With this in place, when we add lines inside this
// area, only the lines in this area will be scrolled. We place the cursor
// at the end of the scroll region, and add lines starting there.
//
// ┌─Screen───────────────────────┐
// │┌╌Scroll region╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐│
// │┆ ┆│
// │┆ ┆│
// │┆ ┆│
// │█╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘│
// │╭─Viewport───────────────────╮│
// ││ ││
// │╰────────────────────────────╯│
// └──────────────────────────────┘
queue!(writer, SetScrollRegion(1..area.top()))?;
// NB: we are using MoveTo instead of set_cursor_position here to avoid messing with the
// terminal's last_known_cursor_position, which hopefully will still be accurate after we
// fetch/restore the cursor position. insert_history_lines should be cursor-position-neutral :)
queue!(writer, MoveTo(/*x*/ 0, cursor_top))?;
for line in &wrapped {
queue!(writer, Print("\r\n"))?;
write_history_line(writer, line, wrap_width)?;
}
write_history_line(writer, line, wrap_width)?;
}
} else {
let cursor_top = if area.bottom() < screen_size.height {
let scroll_amount = wrapped_lines.min(screen_size.height - area.bottom());
let top_1based = area.top() + 1;
queue!(writer, SetScrollRegion(top_1based..screen_size.height))?;
queue!(writer, MoveTo(0, area.top()))?;
for _ in 0..scroll_amount {
queue!(writer, Print("\x1bM"))?;
}
queue!(writer, ResetScrollRegion)?;
let cursor_top = area.top().saturating_sub(1);
area.y += scroll_amount;
should_update_area = true;
cursor_top
} else {
area.top().saturating_sub(1)
};
// Limit the scroll region to the lines from the top of the screen to the
// top of the viewport. With this in place, when we add lines inside this
// area, only the lines in this area will be scrolled. We place the cursor
// at the end of the scroll region, and add lines starting there.
//
// ┌─Screen───────────────────────┐
// │┌╌Scroll region╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐│
// │┆ ┆│
// │┆ ┆│
// │┆ ┆│
// │█╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘│
// │╭─Viewport───────────────────╮│
// ││ ││
// │╰────────────────────────────╯│
// └──────────────────────────────┘
queue!(writer, SetScrollRegion(1..area.top()))?;
// NB: we are using MoveTo instead of set_cursor_position here to avoid messing with the
// terminal's last_known_cursor_position, which hopefully will still be accurate after we
// fetch/restore the cursor position. insert_history_lines should be cursor-position-neutral :)
queue!(writer, MoveTo(0, cursor_top))?;
for line in &wrapped {
queue!(writer, Print("\r\n"))?;
write_history_line(writer, line, wrap_width)?;
}
queue!(writer, ResetScrollRegion)?;
}
// Restore the cursor position to where it was before we started.
@@ -857,8 +876,12 @@ mod tests {
let lines: Vec<Line<'static>> = (0..12)
.map(|idx| Line::from(format!("reflow line {idx:02}")))
.collect();
insert_history_lines_with_mode(&mut term, lines, InsertHistoryMode::FullScreenReplay)
.expect("insert reflow history");
insert_history_lines_with_mode(
&mut term,
lines,
InsertHistoryMode::FullScreenReplayPrefill,
)
.expect("insert reflow history");
let start_row = 0;
let rows: Vec<String> = term
+1 -1
View File
@@ -833,7 +833,7 @@ impl Tui {
crate::insert_history::insert_history_lines_with_mode(
terminal,
pending_reflow_history_lines.clone(),
crate::insert_history::InsertHistoryMode::FullScreenReplay,
crate::insert_history::InsertHistoryMode::FullScreenReplayPrefill,
)?;
pending_reflow_history_lines.clear();
Ok(true)