From 6370fd4cdd7dd1a351ce9349f18fa32df4535f9c Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:51:20 +0800 Subject: [PATCH] test(terminal): pin that a DECTCEM-hidden cursor paints no caret (#844) The issue reads `CursorShape::Hidden` as reachable only through DECSCUSR, but alacritty derives it from `TermMode::SHOW_CURSOR`, so `?25l` already suppresses the painted caret: focused bar/underline, the focused block's reverse-video cell, and the unfocused outline. Claude Code itself ends each frame with `?25h` at its input point, which is the caret the reporter saw. Route the paint decision through `GridSnapshot::painted_cursor` and pin it with a test that paints real frames for the reporter's stream shape (alt screen, 69 hides / 75 shows ending hidden): focused, unfocused, `?25h` restoring it, and a re-attach replay. With the hidden filter disabled the test fails with `Some((4, 12, Block))`. Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM --- src/terminal/element.rs | 18 +++++- src/terminal/view.rs | 119 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/src/terminal/element.rs b/src/terminal/element.rs index c1a5b372..b3047b3b 100644 --- a/src/terminal/element.rs +++ b/src/terminal/element.rs @@ -1583,6 +1583,20 @@ pub(super) struct GridSnapshot { history_size: usize, } +impl GridSnapshot { + /// The terminal caret this frame paints, if any. None while the program + /// has the cursor hidden: alacritty reports a DECTCEM-reset cursor + /// (`?25l`) as `CursorShape::Hidden`, and a TUI that hides it is usually + /// drawing a caret of its own that ours must not cover (#844). + pub(super) fn painted_cursor( + &self, + ) -> Option<(usize, usize, crate::core::config::CursorStyle)> { + self.cursor + .filter(|c| !c.hidden) + .map(|c| (c.row, c.col, c.style)) + } +} + impl TerminalElement { pub(super) fn build_grid( &self, @@ -2123,9 +2137,7 @@ impl Element for TerminalElement { let sliver = snap.sliver.as_ref(); let cursor_cell = cursor.map(|c| (c.row, c.ime_col)); - let render_cursor = cursor - .filter(|c| !c.hidden) - .map(|c| (c.row, c.col, c.style)); + let render_cursor = snap.painted_cursor(); // Reverse-video the block cursor's cell up front, so it rides the // normal background-then-glyph path instead of being tinted on top of diff --git a/src/terminal/view.rs b/src/terminal/view.rs index 3d77f506..50fb8557 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -14822,6 +14822,125 @@ mod gpui_tests { ); } + /// #844: a TUI that resets DECTCEM and draws its own reverse-video caret + /// gets no terminal caret painted over it — focused, unfocused, and after + /// a re-attach replays its screen — and `?25h` brings the caret back. + /// + /// The stream is the reporter's shape: an alternate screen and 69 `?25l` + /// interleaved with 75 `?25h`, the last one a hide. What is checked is the + /// snapshot a real paint left behind, through the same `painted_cursor` + /// the element paints from. + #[gpui::test] + fn dectcem_reset_paints_no_terminal_caret_over_the_tuis_own(cx: &mut TestAppContext) { + use crate::core::config::CursorStyle; + + // An Ink-style frame: our own caret as one reverse-video cell at + // row 5 column 13, and the real cursor parked on it. + const FRAME: &[u8] = b"\x1b[5;1H\x1b[2K> type here \x1b[7m \x1b[27m\x1b[5;13H"; + let mut stream = b"\x1b[?1049h".to_vec(); + stream.extend(std::iter::repeat_n(&b"\x1b[?25h"[..], 7).flatten()); + for _ in 0..68 { + stream.extend_from_slice(b"\x1b[?25l"); + stream.extend_from_slice(FRAME); + stream.extend_from_slice(b"\x1b[?25h"); + } + stream.extend_from_slice(b"\x1b[?25l"); + stream.extend_from_slice(FRAME); + assert_eq!(stream.windows(6).filter(|w| w == b"\x1b[?25l").count(), 69); + assert_eq!(stream.windows(6).filter(|w| w == b"\x1b[?25h").count(), 75); + + type Painted = Option<(usize, usize, CursorStyle)>; + // Paints frames until the one the pane settles on matches `want`, and + // returns the last painted caret either way. + let paint_until = |window: &gpui::WindowHandle, + cx: &mut TestAppContext, + focused: bool, + want: &dyn Fn(Painted) -> bool| { + let mut painted = None; + for _ in 0..400 { + window + .update(cx, |view, window, cx| { + if focused { + window.activate_window(); + view.focus_handle.focus(window, cx); + } else { + window.blur(); + } + cx.notify(); + }) + .unwrap(); + let mut vcx = gpui::VisualTestContext::from_window((*window).into(), cx); + vcx.update(|window, _| window.refresh()); + vcx.run_until_parked(); + let (text, snap) = window + .update(cx, |view, window, _| { + assert_eq!(view.focus_handle.is_focused(window), focused); + use alacritty_terminal::grid::Dimensions as _; + use alacritty_terminal::index::{Column, Line}; + let term = view.terminal.term.lock(); + let row = &term.grid()[Line(4)]; + let text = (0..term.grid().columns()) + .map(|col| row[Column(col)].c) + .collect::(); + (text, view.grid_snap.as_ref().map(|s| s.painted_cursor())) + }) + .unwrap(); + if text.starts_with("> type here") + && let Some(p) = snap + { + painted = p; + if want(p) { + break; + } + } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + painted + }; + + let (window, mut daemon) = harness(cx); + DaemonMsg::Output(stream.clone()) + .encode(&mut daemon) + .unwrap(); + let focused = paint_until(&window, cx, true, &|p| p.is_none()); + assert_eq!( + focused, None, + "a focused pane painted its caret over a TUI that reset DECTCEM" + ); + let unfocused = paint_until(&window, cx, false, &|p| p.is_none()); + assert_eq!( + unfocused, None, + "an unfocused pane painted its hollow caret over a TUI that reset DECTCEM" + ); + + DaemonMsg::Output(b"\x1b[?25h".to_vec()) + .encode(&mut daemon) + .unwrap(); + let shown = paint_until(&window, cx, true, &|p| p.is_some()); + assert_eq!( + shown.map(|(row, col, _)| (row, col)), + Some((4, 12)), + "`?25h` must bring the caret back where the program parked it" + ); + + // Switching back to the tab: a brand new view replays the screen the + // daemon kept, then the prompt state, which says a program is running. + let (window, mut daemon) = harness(cx); + DaemonMsg::Snapshot(stream).encode(&mut daemon).unwrap(); + DaemonMsg::Prompt { + active: true, + at_prompt: false, + last_exit: None, + } + .encode(&mut daemon) + .unwrap(); + let replayed = paint_until(&window, cx, true, &|p| p.is_none()); + assert_eq!( + replayed, None, + "a re-attached pane painted its caret over a TUI that reset DECTCEM" + ); + } + #[gpui::test] fn child_exit_emits_the_close_event_but_disconnect_does_not(cx: &mut TestAppContext) { use std::cell::Cell;