aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-24 01:30:05 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-24 01:31:06 +0300
commit36de78dd4fa4e2c203ee955f18caf82a6bca7e80 (patch)
tree2cf7fca12c9c1eeaeb13ae6ad010526d03ccd6fa /src/client
parent503613c8ecbea752a234daf30cb0ad5a8bf93e4e (diff)
fix: preserve kitty graphics during host repaints
refs #1628
Diffstat (limited to 'src/client')
-rw-r--r--src/client/mod.rs34
1 files changed, 22 insertions, 12 deletions
diff --git a/src/client/mod.rs b/src/client/mod.rs
index b1c363f2..23e331e8 100644
--- a/src/client/mod.rs
+++ b/src/client/mod.rs
@@ -87,6 +87,8 @@ struct ClientState {
remote_image_paste_key: Option<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
/// Whether outer focus gain should force a full host-terminal redraw.
redraw_on_focus_gained: bool,
+ /// Whether the next semantic frame must repaint every cell without clearing the surface.
+ repaint_pending: bool,
/// Whether this client draws the cursor into frame cells instead of using the host cursor.
draw_host_cursor: bool,
}
@@ -220,8 +222,8 @@ fn attach_scroll_action(
}
impl ClientState {
- fn request_full_redraw(&mut self) {
- self.blit_encoder = render_ansi::BlitEncoder::new();
+ fn request_repaint(&mut self) {
+ self.repaint_pending = true;
}
}
@@ -1302,6 +1304,7 @@ async fn run_client_loop(
#[cfg(unix)]
remote_image_paste_key: config.remote_image_paste_key,
redraw_on_focus_gained: config.redraw_on_focus_gained,
+ repaint_pending: false,
draw_host_cursor,
};
debug!(?negotiated_encoding, "client render encoding active");
@@ -1417,7 +1420,7 @@ async fn run_client_loop(
&events,
state.redraw_on_focus_gained,
) {
- state.request_full_redraw();
+ state.request_repaint();
}
if crate::raw_input::events_require_host_terminal_theme_query(&events) {
query_host_terminal_theme();
@@ -1489,7 +1492,7 @@ async fn run_client_loop(
&raw_events,
state.redraw_on_focus_gained,
) {
- state.request_full_redraw();
+ state.request_repaint();
}
let msg = ClientMessage::InputEvents { events };
if let Err(e) = write_to_server(&mut write_stream, &msg) {
@@ -1516,11 +1519,14 @@ async fn run_client_loop(
frame_data
};
let encoded = if state.draw_host_cursor {
+ state.blit_encoder.encode_with_suppressed_visible_cursor(
+ &frame_data,
+ state.repaint_pending,
+ )
+ } else {
state
.blit_encoder
- .encode_with_suppressed_visible_cursor(&frame_data, false)
- } else {
- state.blit_encoder.encode(&frame_data, false)
+ .encode(&frame_data, state.repaint_pending)
};
let mut stdout = io::stdout();
let graphics = if state.kitty_graphics_enabled {
@@ -1532,6 +1538,7 @@ async fn run_client_loop(
write_encoded_frame_with_graphics(&mut stdout, &encoded.bytes, graphics);
let _ = stdout.flush();
state.blit_encoder.commit(frame_data, encoded);
+ state.repaint_pending = false;
}
ServerMessage::Terminal(frame) => {
if state.kitty_graphics_enabled && contains_kitty_graphics_bytes(&frame.bytes) {
@@ -1983,15 +1990,18 @@ fn write_encoded_frame_with_graphics(
encoded: &[u8],
graphics: &[u8],
) -> io::Result<()> {
- writer.write_all(encoded)?;
if graphics.is_empty() {
- return Ok(());
+ return writer.write_all(encoded);
}
+ let insertion = render_ansi::final_sync_output_end(encoded).unwrap_or(encoded.len());
+
+ writer.write_all(&encoded[..insertion])?;
record_received_kitty_graphics(graphics);
writer.write_all(b"\x1b7")?;
writer.write_all(graphics)?;
- writer.write_all(b"\x1b8")
+ writer.write_all(b"\x1b8")?;
+ writer.write_all(&encoded[insertion..])
}
fn contains_kitty_graphics_bytes(bytes: &[u8]) -> bool {
@@ -2360,7 +2370,7 @@ mod tests {
}
#[test]
- fn graphics_bytes_are_written_after_blit_with_saved_cursor() {
+ fn graphics_bytes_are_written_inside_synchronized_blit_with_saved_cursor() {
let mut output = Vec::new();
write_encoded_frame_with_graphics(
&mut output,
@@ -2371,7 +2381,7 @@ mod tests {
assert_eq!(
output,
- b"\x1b[?2026htext\x1b[?2026lcursor\x1b7graphics\x1b8"
+ b"\x1b[?2026htext\x1b7graphics\x1b8\x1b[?2026lcursor"
);
}