From d1471e64834d0c7daf072f11c01c30db35324850 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Thu, 2 Jul 2026 21:57:59 +0300 Subject: fix: draw host cursor on windows refs #930 --- src/protocol/render_ansi.rs | 93 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 5 deletions(-) (limited to 'src/protocol') diff --git a/src/protocol/render_ansi.rs b/src/protocol/render_ansi.rs index 3346bb9c..231aa19f 100644 --- a/src/protocol/render_ansi.rs +++ b/src/protocol/render_ansi.rs @@ -33,6 +33,8 @@ use unicode_width::UnicodeWidthStr; use crate::protocol::{underline_style_from_modifier, CellData, FrameData}; +const REVERSED_MODIFIER: u16 = 1 << 6; + /// Bytes produced by a [`BlitEncoder`] for one terminal frame. pub(crate) struct EncodedBlit { /// Terminal escape bytes ready to write to the host terminal. @@ -57,10 +59,23 @@ impl BlitEncoder { } pub(crate) fn encode(&self, frame: &FrameData, force_full: bool) -> EncodedBlit { - self.encode_inner(frame, force_full) + self.encode_inner(frame, force_full, false) + } + + pub(crate) fn encode_with_suppressed_visible_cursor( + &self, + frame: &FrameData, + force_full: bool, + ) -> EncodedBlit { + self.encode_inner(frame, force_full, true) } - fn encode_inner(&self, frame: &FrameData, force_full: bool) -> EncodedBlit { + fn encode_inner( + &self, + frame: &FrameData, + force_full: bool, + suppress_visible_cursor: bool, + ) -> EncodedBlit { let prev = if force_full { None } else { @@ -81,7 +96,7 @@ impl BlitEncoder { prev, &mut next_last_visible_cursor, &mut next_last_cursor_shape, - false, + suppress_visible_cursor, ); if let Some(stats) = prof_stats { crate::render_prof::duration_since("ansi_encode.total", prof_started); @@ -118,6 +133,19 @@ impl BlitEncoder { } } +pub(crate) fn frame_with_drawn_cursor(mut frame: FrameData) -> FrameData { + if let Some(cursor) = frame.cursor.as_ref().filter(|cursor| cursor.visible) { + let (x, y) = clamp_cursor_position(&frame, cursor.x, cursor.y); + let idx = (y as usize) + .saturating_mul(frame.width as usize) + .saturating_add(x as usize); + if let Some(cell) = frame.cells.get_mut(idx) { + cell.modifier ^= REVERSED_MODIFIER; + } + } + frame +} + #[derive(Clone, Copy, Default)] struct ProfBlitStats { scanned_cells: u64, @@ -280,7 +308,6 @@ fn modifier_to_sgr_parts(val: u16) -> Vec<&'static str> { const UNDERLINED: u16 = 1 << 3; // 0x08 const SLOW_BLINK: u16 = 1 << 4; // 0x10 const RAPID_BLINK: u16 = 1 << 5; // 0x20 - const REVERSED: u16 = 1 << 6; // 0x40 const HIDDEN: u16 = 1 << 7; // 0x80 const CROSSED_OUT: u16 = 1 << 8; // 0x100 @@ -308,7 +335,7 @@ fn modifier_to_sgr_parts(val: u16) -> Vec<&'static str> { if val & RAPID_BLINK != 0 { parts.push("6"); } - if val & REVERSED != 0 { + if val & REVERSED_MODIFIER != 0 { parts.push("7"); } if val & HIDDEN != 0 { @@ -1039,6 +1066,62 @@ mod tests { ); } + #[test] + fn drawn_cursor_reverses_visible_cursor_cell() { + let frame = FrameData { + cells: vec![make_cell("A", 0, 0, 0); 9], + width: 3, + height: 3, + cursor: Some(CursorState { + x: 2, + y: 1, + visible: true, + shape: 6, + }), + hyperlinks: Vec::new(), + graphics: Vec::new(), + }; + let drawn = frame_with_drawn_cursor(frame.clone()); + + assert_eq!(drawn.cells[5].modifier, REVERSED_MODIFIER); + assert_eq!(frame.cells[5].modifier, 0); + + let encoded = BlitEncoder::new().encode_with_suppressed_visible_cursor(&drawn, false); + let output_str = String::from_utf8(encoded.bytes).unwrap(); + + assert!( + output_str.contains("\x1b[2;3H\x1b[6 q\x1b[?25l"), + "drawn cursor mode should park the host cursor hidden at the focused cursor position" + ); + assert!( + !output_str.contains("\x1b[?25h"), + "drawn cursor mode should not show the host cursor" + ); + assert!( + output_str.contains("\x1b[0;7;39;49mA"), + "drawn cursor should be emitted as reverse-video cell content" + ); + } + + #[test] + fn drawn_cursor_ignores_hidden_cursor() { + let frame = FrameData { + cells: vec![make_cell("A", 0, 0, 0)], + width: 1, + height: 1, + cursor: Some(CursorState { + x: 0, + y: 0, + visible: false, + shape: 0, + }), + hyperlinks: Vec::new(), + graphics: Vec::new(), + }; + + assert_eq!(frame_with_drawn_cursor(frame.clone()), frame); + } + #[test] fn blit_frame_emits_cursor_shape_before_visibility_without_touching_ime_anchor() { let frame = FrameData { -- cgit v1.2.3