fix: prevent cursor jumps during synchronized redraws

refs #4303
This commit is contained in:
Ogulcan Celik
2026-09-18 17:52:16 +03:00
parent 241063f7ff
commit 6c5a7ecd07
+83 -1
View File
@@ -1441,7 +1441,8 @@ impl GhosttyPaneTerminal {
.terminal
.mode_get(crate::ghostty::MODE_SYNCHRONIZED_OUTPUT)
.unwrap_or(false);
if CURSOR_POSITION_SETTLE_ENABLED {
// Intermediate synchronized-frame positions must not become settled cursors.
if CURSOR_POSITION_SETTLE_ENABLED && !synchronized_output {
let cursor_started = crate::render_prof::timer();
let cursor_after_write = current_cursor_state(&mut core);
crate::render_prof::duration_since("pty.cursor_state_update", cursor_started);
@@ -4363,6 +4364,87 @@ mod tests {
);
}
#[test]
#[cfg(windows)]
fn cursor_settle_ignores_intermediate_synchronized_frame_positions() {
let (tx, _rx) = mpsc::channel(4);
let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap();
let pane_id = PaneId::from_raw(1);
pane.process_pty_bytes(pane_id, 0, b"\x1b[15;4H", &tx);
let previous = TerminalCursorState {
x: 3,
y: 14,
visible: true,
shape: 0,
};
{
let mut core = pane.core.lock().unwrap();
let now = Instant::now();
core.cursor_settle_state = CursorPositionSettleState::default();
core.cursor_settle_state.observe(
Some(TerminalCursorState { x: 2, ..previous }),
now - Duration::from_millis(300),
);
// Seed a pending hold whose deadline has passed, without wall-clock sleeps.
core.cursor_settle_state
.observe(Some(previous), now - Duration::from_millis(200));
}
for bytes in [
b"\x1b[?2026h\x1b[15;4Hx\x1b[13;1H".as_slice(),
b"\x1b[0 q\x1b[13;1H \x1b[15;5H",
b"\x1b[?25h",
] {
let result = pane.process_pty_bytes(pane_id, 0, bytes, &tx);
assert!(!result.request_render);
assert_eq!(result.render_delay, None);
assert_eq!(pane.cursor_state(), Some(previous));
assert!(pane.core.lock().unwrap().cursor_settle_state.pending());
}
let result = pane.process_pty_bytes(pane_id, 0, b"\x1b[?2026l", &tx);
assert!(result.request_render);
assert!(!pane.core.lock().unwrap().cursor_settle_state.pending());
assert_eq!(
pane.cursor_state(),
Some(TerminalCursorState { x: 4, ..previous })
);
}
#[test]
#[cfg(windows)]
fn cursor_settle_preserves_final_visibility_and_shape_across_split_sync_sequences() {
let (tx, _rx) = mpsc::channel(4);
let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap();
let pane_id = PaneId::from_raw(1);
pane.process_pty_bytes(pane_id, 0, b"\x1b[15;4H", &tx);
for bytes in [
b"\x1b[?202".as_slice(),
b"6h\x1b[13;1H",
b"\x1b[6 q\x1b[15;5H\x1b[?25l\x1b[?20",
] {
pane.process_pty_bytes(pane_id, 0, bytes, &tx);
assert!(!pane.core.lock().unwrap().cursor_settle_state.pending());
}
let result = pane.process_pty_bytes(pane_id, 0, b"26l", &tx);
assert!(result.request_render);
assert_eq!(result.render_delay, None);
assert_eq!(
pane.cursor_state(),
Some(TerminalCursorState {
x: 4,
y: 14,
visible: false,
shape: 6,
})
);
}
#[test]
#[cfg(not(windows))]
fn cursor_state_uses_live_position_when_settle_policy_disabled() {