diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-02 21:57:59 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-02 21:59:06 +0300 |
| commit | d1471e64834d0c7daf072f11c01c30db35324850 (patch) | |
| tree | 5763e6da098d3aabe5932decf26e5cf1d31c814c /src/protocol | |
| parent | 848554517ae6473caf61f738d60def90f77e1dc9 (diff) | |
fix: draw host cursor on windows
refs #930
Diffstat (limited to 'src/protocol')
| -rw-r--r-- | src/protocol/render_ansi.rs | 93 |
1 files changed, 88 insertions, 5 deletions
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 { @@ -1040,6 +1067,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 { cells: vec![make_cell("A", 0, 0, 0)], |
