aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-08-11 22:55:49 +0300
committerGitHub <noreply@github.com>2026-08-11 22:55:49 +0300
commit3607453087fdc870b7c6aff5cf64b202f630c513 (patch)
tree50c3cae497b21874188081b43a66e013d0376a18 /src
parent06ca0baa12f4203c5bbad9ecadf53f9a475a52b2 (diff)
fix(render): compact large terminal redraws (#2675)
refs #2670 Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Diffstat (limited to 'src')
-rw-r--r--src/protocol/render_ansi.rs26
-rw-r--r--src/server/headless.rs74
2 files changed, 87 insertions, 13 deletions
diff --git a/src/protocol/render_ansi.rs b/src/protocol/render_ansi.rs
index 1e6b9aa4..97685d4d 100644
--- a/src/protocol/render_ansi.rs
+++ b/src/protocol/render_ansi.rs
@@ -633,9 +633,11 @@ fn write_ime_anchor_cursor_state(writer: &mut impl Write, cursor: HostCursorStat
}
fn write_all_cells(writer: &mut impl Write, frame: &FrameData) {
+ let mut last_sgr = String::new();
let mut active_hyperlink = None;
for row in 0..frame.height {
let mut to_skip = 0usize;
+ let mut next_inline_col = None;
for col in 0..frame.width {
if to_skip > 0 {
to_skip -= 1;
@@ -646,25 +648,23 @@ fn write_all_cells(writer: &mut impl Write, frame: &FrameData) {
let cell = &frame.cells[idx];
if cell.skip {
+ next_inline_col = None;
continue;
}
- // Move cursor to position (1-based).
- let _ = write!(writer, "\x1b[{};{}H", row + 1, col + 1);
-
- // Set style.
- let sgr = build_sgr(cell.fg, cell.bg, cell.modifier);
- let _ = writer.write_all(sgr.as_bytes());
-
- write_hyperlink_if_changed(
+ let cursor_position = (next_inline_col != Some(col)).then_some((col, row));
+ write_cell(
writer,
+ cursor_position,
+ cell,
+ &mut last_sgr,
&mut active_hyperlink,
- cell_hyperlink_uri(frame, cell),
+ frame,
);
-
- // Write the symbol.
- let _ = writer.write_all(cell.symbol.as_bytes());
- to_skip = cell_width(cell).saturating_sub(1);
+ let width = cell_width(cell);
+ next_inline_col =
+ (cell.symbol.is_ascii() && width == 1).then_some(col.saturating_add(1));
+ to_skip = width.saturating_sub(1);
}
}
diff --git a/src/server/headless.rs b/src/server/headless.rs
index ff6f367d..7393a7c5 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -9156,6 +9156,80 @@ next_tab = ""
}
#[test]
+ fn render_and_stream_sends_large_terminal_frame_for_terminal_ansi_client() {
+ let mut server = test_headless_server();
+ server.app.state.workspaces = vec![crate::workspace::Workspace::test_new("test")];
+ server.app.state.ensure_test_terminals();
+ server.app.state.active = Some(0);
+ server.app.state.selected = 0;
+ server.app.state.mode = crate::app::Mode::Terminal;
+ let (client_tx, _client_control_rx, client_rx) = test_client_writer();
+
+ server.clients.insert(
+ 1,
+ ClientConnection::new(
+ (278, 85),
+ crate::kitty_graphics::HostCellSize::default(),
+ crate::terminal_theme::TerminalTheme::default(),
+ None,
+ 1,
+ RenderEncoding::TerminalAnsi,
+ Some(client_tx),
+ ),
+ );
+ server.foreground_client_id = Some(1);
+ server.sync_foreground_client_state();
+
+ server.render_and_stream();
+ match read_server_message(
+ client_rx
+ .recv_timeout(Duration::from_millis(100))
+ .expect("initial terminal frame"),
+ ) {
+ ServerMessage::Terminal(frame) => {
+ assert_eq!(frame.seq, 1);
+ assert_eq!((frame.width, frame.height), (278, 85));
+ assert!(frame.full);
+ }
+ other => panic!("expected terminal frame, got {other:?}"),
+ }
+
+ assert!(server.handle_server_event(ServerEvent::ClientResize {
+ client_id: 1,
+ cols: 710,
+ rows: 202,
+ cell_width_px: 0,
+ cell_height_px: 0,
+ }));
+ server.render_and_stream();
+
+ match read_server_message(
+ client_rx
+ .recv_timeout(Duration::from_millis(100))
+ .expect("large terminal frame"),
+ ) {
+ ServerMessage::Terminal(frame) => {
+ assert_eq!(frame.seq, 2);
+ assert_eq!((frame.width, frame.height), (710, 202));
+ assert!(frame.full);
+ assert!(!frame.bytes.is_empty());
+ }
+ other => panic!("expected terminal frame, got {other:?}"),
+ }
+
+ server.app.state.mode = crate::app::Mode::Navigate;
+ server.render_and_stream();
+ match read_server_message(
+ client_rx
+ .recv_timeout(Duration::from_millis(100))
+ .expect("follow-up terminal frame"),
+ ) {
+ ServerMessage::Terminal(frame) => assert_eq!(frame.seq, 3),
+ other => panic!("expected terminal frame, got {other:?}"),
+ }
+ }
+
+ #[test]
fn terminal_ansi_input_does_not_reset_blit_baseline() {
let mut server = test_headless_server();
let (client_tx, _client_control_rx, client_rx) = test_client_writer();