Move the TUI cursor before showing it (#40166)

## Why

Showing the cursor before moving it can briefly expose it at its previous
position during a draw.

## What changed

Reorder terminal cursor updates so the cursor is positioned before it is made
visible.

## Testing

Add a regression test that verifies the cursor move escape sequence is emitted
before the show-cursor sequence.

GitOrigin-RevId: 2df277ba315bef4a7a56c11ecb78fcd52c354ac5
This commit is contained in:
Felipe Coury
2026-08-23 00:46:45 +00:00
committed by copyberry
parent a73485dc76
commit 1e6185e522

View File

@@ -429,8 +429,8 @@ where
None => self.hide_cursor()?, None => self.hide_cursor()?,
Some(position) => { Some(position) => {
self.set_cursor_style(cursor_style)?; self.set_cursor_style(cursor_style)?;
self.show_cursor()?;
self.set_cursor_position(position)?; self.set_cursor_position(position)?;
self.show_cursor()?;
} }
} }
@@ -896,11 +896,11 @@ mod tests {
} }
fn hide_cursor(&mut self) -> io::Result<()> { fn hide_cursor(&mut self) -> io::Result<()> {
Ok(()) queue!(self, crossterm::cursor::Hide)
} }
fn show_cursor(&mut self) -> io::Result<()> { fn show_cursor(&mut self) -> io::Result<()> {
Ok(()) queue!(self, crossterm::cursor::Show)
} }
fn get_cursor_position(&mut self) -> io::Result<Position> { fn get_cursor_position(&mut self) -> io::Result<Position> {
@@ -909,6 +909,8 @@ mod tests {
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> { fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
self.cursor = position.into(); self.cursor = position.into();
let Position { x, y } = self.cursor;
queue!(self, MoveTo(x, y))?;
Ok(()) Ok(())
} }
@@ -1255,6 +1257,43 @@ mod tests {
); );
} }
#[test]
fn terminal_draw_moves_cursor_before_showing_it() {
let cursor_position = Position { x: 1, y: 0 };
let mut terminal =
Terminal::with_options(CaptureBackend::new(/*width*/ 2, /*height*/ 1))
.expect("terminal");
terminal.set_viewport_area(Rect::new(
/*x*/ 0, /*y*/ 0, /*width*/ 2, /*height*/ 1,
));
terminal
.try_draw(|frame| {
frame.set_cursor_position(cursor_position);
io::Result::Ok(())
})
.expect("draw");
let mut expected_move = Vec::new();
queue!(expected_move, MoveTo(cursor_position.x, cursor_position.y)).expect("queue move");
let expected_move = String::from_utf8(expected_move).expect("move utf8");
let mut expected_show = Vec::new();
queue!(expected_show, crossterm::cursor::Show).expect("queue show");
let expected_show = String::from_utf8(expected_show).expect("show utf8");
let actual = terminal.backend().output();
let move_index = actual.find(&expected_move).expect("cursor move");
let show_index = actual.find(&expected_show).expect("cursor show");
assert!(
move_index < show_index,
"expected cursor move before show, got {actual:?}"
);
assert_snapshot!(
actual[move_index..].escape_debug().to_string(),
@r"\u{1b}[1;2H\u{1b}[?25h"
);
}
#[test] #[test]
fn reset_cursor_style_emits_default_user_shape() { fn reset_cursor_style_emits_default_user_shape() {
let mut output = Vec::new(); let mut output = Vec::new();