aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-18 03:56:51 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-18 03:56:51 +0300
commit02a6e874f67800891b5a549297219ed6f3ce0f2f (patch)
tree3f24e9d7f57f8b72576cca057111a34f65181f59 /src
parentc1f7b6f331501a784bfad896310eae1d064bd9b9 (diff)
perf: batch contiguous terminal diff writes
refs #283
Diffstat (limited to 'src')
-rw-r--r--src/client/mod.rs4
-rw-r--r--src/protocol/render_ansi.rs69
2 files changed, 68 insertions, 5 deletions
diff --git a/src/client/mod.rs b/src/client/mod.rs
index 90a73ec8..011de937 100644
--- a/src/client/mod.rs
+++ b/src/client/mod.rs
@@ -30,6 +30,7 @@ use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers, MouseEventKind};
#[cfg(not(windows))]
use crossterm::event::{PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags};
use crossterm::execute;
+use crossterm::terminal::{DisableLineWrap, EnableLineWrap};
use interprocess::local_socket::traits::Stream as _;
use interprocess::TryClone as _;
use tracing::{debug, info, warn};
@@ -404,6 +405,8 @@ fn setup_terminal_with_capabilities(
io::stdout().flush()?;
}
+ execute!(io::stdout(), DisableLineWrap)?;
+
Ok(TerminalGuard {
reset_modify_other_keys: modify_other_keys_mode.is_some(),
reset_host_color_scheme_reports: host_color_scheme_reports,
@@ -576,6 +579,7 @@ fn restore_terminal_state(
let _ = execute!(
io::stdout(),
+ EnableLineWrap,
DisableFocusChange,
DisableBracketedPaste,
DisableMouseCapture
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]);