diff options
Diffstat (limited to 'src/protocol')
| -rw-r--r-- | src/protocol/render_ansi.rs | 69 |
1 files changed, 64 insertions, 5 deletions
diff --git a/src/protocol/render_ansi.rs b/src/protocol/render_ansi.rs index b1f0806d..f02ec2bc 100644 --- a/src/protocol/render_ansi.rs +++ b/src/protocol/render_ansi.rs @@ -682,8 +682,7 @@ fn close_hyperlink(writer: &mut impl Write, active: &mut Option<String>) { fn write_cell( writer: &mut impl Write, - row: u16, - col: u16, + cursor_position: Option<(u16, u16)>, cell: &CellData, last_sgr: &mut String, active_hyperlink: &mut Option<String>, @@ -693,7 +692,9 @@ fn write_cell( return; } - let _ = write!(writer, "\x1b[{};{}H", row + 1, col + 1); + if let Some(position) = cursor_position { + write_cursor_position(writer, position); + } let sgr = build_sgr(cell.fg, cell.bg, cell.modifier); if sgr != *last_sgr { @@ -730,6 +731,9 @@ fn write_changed_cells(writer: &mut impl Write, frame: &FrameData, prev: &FrameD for row in 0..frame.height { let mut invalidated = 0usize; let mut to_skip = 0usize; + // Herdr clients disable host autowrap, so safe cells can advance inline + // without spilling into adjacent rows during a resize race. + let mut next_inline_col = None; for col in 0..frame.width { let idx = (row as usize) * (frame.width as usize) + (col as usize); @@ -745,15 +749,18 @@ fn write_changed_cells(writer: &mut impl Write, frame: &FrameData, prev: &FrameD ) || invalidated > 0) && to_skip == 0 { + let cursor_position = + (next_inline_col != Some(col) || invalidated > 0).then_some((col, row)); write_cell( writer, - row, - col, + cursor_position, cell, &mut last_sgr, &mut active_hyperlink, frame, ); + next_inline_col = (cell.symbol.is_ascii() && cell_width(cell) == 1) + .then_some(col.saturating_add(1)); } to_skip = cell_width(cell).saturating_sub(1); @@ -1400,6 +1407,58 @@ mod tests { } #[test] + fn scroll_sized_ascii_shift_batches_changed_cells_by_row() { + const WIDTH: u16 = 140; + const HEIGHT: u16 = 50; + let prev = make_frame( + WIDTH, + HEIGHT, + vec![make_cell("A", 0, 0, 0); usize::from(WIDTH) * usize::from(HEIGHT)], + ); + let curr = make_frame( + WIDTH, + HEIGHT, + vec![make_cell("B", 0, 0, 0); usize::from(WIDTH) * usize::from(HEIGHT)], + ); + + let mut output = Vec::new(); + blit_frame_to(&mut output, &curr, Some(&prev)); + + let cup_count = output.iter().filter(|&&byte| byte == b'H').count(); + assert!( + cup_count <= usize::from(HEIGHT) + 2, + "one dense scroll frame should need at most one CUP per row plus cursor anchors, got {cup_count}" + ); + assert!( + output.len() <= 16_290, + "one dense scroll frame should stay below 25% of the 65,161-byte live baseline, got {} bytes", + output.len() + ); + } + + #[test] + fn batched_ascii_diff_replays_to_current_frame() { + let prev = make_frame(4, 3, vec![make_cell("A", 0, 0, 0); 12]); + let curr = make_frame(4, 3, vec![make_cell("B", 0, 0, 0); 12]); + let mut terminal = crate::ghostty::Terminal::new(4, 3, 0).unwrap(); + + let mut initial = Vec::new(); + blit_frame_to(&mut initial, &prev, None); + terminal.write(&initial); + + let mut diff = Vec::new(); + blit_frame_to(&mut diff, &curr, Some(&prev)); + terminal.write(&diff); + + for row in 0..3 { + for col in 0..4 { + let (_, graphemes) = terminal.screen_cell(col, row).unwrap(); + assert_eq!(graphemes, vec![u32::from('B')]); + } + } + } + + #[test] fn blit_frame_size_change_triggers_full_redraw() { let prev = make_frame(2, 2, vec![make_cell("A", 0, 0, 0); 4]); |
