aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-07 21:19:47 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-07 21:20:24 +0300
commit2b99ced89adbd68ac163d16d33e0837fe8f6c3ed (patch)
treeef4ac319f75740287438d2fe01fcd826046e980a /src
parente4a7095c9339fcf14d286936b7eeb16ad4106cc9 (diff)
fix: keep cursor hide inside sync output
refs #967
Diffstat (limited to 'src')
-rw-r--r--src/protocol/render_ansi.rs87
1 files changed, 76 insertions, 11 deletions
diff --git a/src/protocol/render_ansi.rs b/src/protocol/render_ansi.rs
index 231aa19f..b1f0806d 100644
--- a/src/protocol/render_ansi.rs
+++ b/src/protocol/render_ansi.rs
@@ -427,17 +427,15 @@ fn blit_frame_to_with_cursor_memory_and_policy(
let full_redraw =
prev.is_none() || prev.is_some_and(|p| p.width != frame.width || p.height != frame.height);
- // Hide cursor before any cell writes to avoid stray cursor artifacts
- // on terminals that render the hardware cursor at intermediate CUP positions.
- // Keep this outside synchronized output so terminals that defer sync-block
- // side effects still hide the cursor before frame painting begins.
- let _ = writer.write_all(b"\x1b[?25l");
-
// Ask terminals that support synchronized output to apply the whole frame
// atomically. This keeps IMEs and cursor trackers from observing the
// intermediate CUP positions used while painting changed cells.
let _ = writer.write_all(b"\x1b[?2026h");
+ // Hide cursor before any cell writes to avoid stray cursor artifacts
+ // on terminals that render the hardware cursor at intermediate CUP positions.
+ let _ = writer.write_all(b"\x1b[?25l");
+
// Start each frame from a known OSC 8 state. If a previous write was
// interrupted or the outer terminal had an active hyperlink, unlinked cells
// must not inherit it.
@@ -926,8 +924,8 @@ mod tests {
let output_str = String::from_utf8(output).unwrap();
assert!(
- output_str.starts_with("\x1b[?25l\x1b[?2026h"),
- "should hide cursor before synchronized frame painting during full redraw"
+ output_str.starts_with("\x1b[?2026h\x1b[?25l"),
+ "should hide cursor inside synchronized frame painting during full redraw"
);
}
@@ -960,8 +958,8 @@ mod tests {
let output_str = String::from_utf8(output).unwrap();
assert!(
- output_str.starts_with("\x1b[?25l\x1b[?2026h"),
- "should hide cursor before synchronized frame painting during diff"
+ output_str.starts_with("\x1b[?2026h\x1b[?25l"),
+ "should hide cursor inside synchronized frame painting during diff"
);
}
@@ -974,7 +972,7 @@ mod tests {
let output_str = String::from_utf8(output).unwrap();
assert!(
- output_str.starts_with("\x1b[?25l\x1b[?2026h"),
+ output_str.starts_with("\x1b[?2026h\x1b[?25l"),
"should begin synchronized output before frame writes"
);
let sync_end = output_str
@@ -987,6 +985,73 @@ mod tests {
}
#[test]
+ fn blit_frame_begins_sync_before_hiding_cursor_after_visible_cursor_repeat() {
+ let visible = FrameData {
+ cells: vec![make_cell("A", 0, 0, 0); 9],
+ width: 3,
+ height: 3,
+ cursor: Some(CursorState {
+ x: 2,
+ y: 1,
+ visible: true,
+ shape: 0,
+ }),
+ hyperlinks: Vec::new(),
+ graphics: Vec::new(),
+ };
+ let mut changed = visible.clone();
+ changed.cells[0] = make_cell("B", 0, 0, 0);
+
+ let mut last_visible_cursor = None;
+ let mut last_cursor_shape = 0;
+ let mut first_output = Vec::new();
+ blit_frame_to_with_cursor_memory_and_policy(
+ &mut first_output,
+ &visible,
+ None,
+ &mut last_visible_cursor,
+ &mut last_cursor_shape,
+ true,
+ false,
+ );
+
+ let mut second_output = Vec::new();
+ blit_frame_to_with_cursor_memory_and_policy(
+ &mut second_output,
+ &changed,
+ Some(&visible),
+ &mut last_visible_cursor,
+ &mut last_cursor_shape,
+ true,
+ false,
+ );
+
+ let second_output_str = std::str::from_utf8(&second_output).unwrap();
+ assert!(
+ second_output_str.starts_with("\x1b[?2026h\x1b[?25l"),
+ "next frame should enter synchronized output before hiding the cursor"
+ );
+
+ let hide = second_output_str
+ .find("\x1b[?25l")
+ .expect("second frame should hide cursor before painting");
+ let first_paint = second_output_str
+ .find("\x1b[1;1H")
+ .expect("second frame should paint changed cell");
+ assert!(
+ hide < first_paint,
+ "cursor should still hide before painting"
+ );
+
+ first_output.extend_from_slice(&second_output);
+ let combined = String::from_utf8(first_output).unwrap();
+ assert!(
+ combined.contains("\x1b[?2026l\x1b[2;3H\x1b[?25h\x1b[?2026h\x1b[?25l"),
+ "post-sync cursor repeat should be followed by a synchronized cursor hide"
+ );
+ }
+
+ #[test]
fn blit_frame_can_repeat_final_cursor_state_after_synchronized_output() {
let frame = FrameData {
cells: vec![make_cell("A", 0, 0, 0); 9],