Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,16 @@ fn set_mouse_capture(enabled: bool) -> io::Result<()> {
}
}

/// Re-apply the host mouse mode set after the host surface may have been
/// re-created underneath us. `set_mouse_capture` clears host mouse reporting
/// first, so this also drops a tracking mode that a reconnect restored behind
/// herdr's back in an encoding the client cannot parse.
fn refresh_host_mouse_capture(enabled: bool) {
if let Err(err) = set_mouse_capture(enabled) {
warn!(err = %err, "failed to re-assert host mouse capture");
}
}

fn restore_terminal_state(
reset_modify_other_keys: bool,
reset_host_color_scheme_reports: bool,
Expand Down Expand Up @@ -1452,6 +1462,9 @@ async fn run_client_loop(
) {
state.request_repaint();
}
if crate::raw_input::events_require_host_mode_refresh(&events) {
refresh_host_mouse_capture(state.mouse_capture_active);
}
if crate::raw_input::events_require_host_terminal_theme_query(&events) {
query_host_terminal_theme();
}
Expand Down Expand Up @@ -1527,6 +1540,9 @@ async fn run_client_loop(
) {
state.request_repaint();
}
if crate::raw_input::events_require_host_mode_refresh(&raw_events) {
refresh_host_mouse_capture(state.mouse_capture_active);
}
let msg = ClientMessage::InputEvents { events };
if let Err(e) = write_to_server(&mut write_stream, &msg) {
return Err(ClientError::ConnectionLost(e));
Expand All @@ -1536,6 +1552,10 @@ async fn run_client_loop(
state.reported_size = (new_cols, new_rows);
// Resizing invalidates the host-side blit baseline.
state.request_repaint();
// A reconnect re-syncs the terminal size before the user touches
// anything, so this usually restores the mode set before the
// first stray mouse report can leak into a pane.
refresh_host_mouse_capture(state.mouse_capture_active);
let msg = ClientMessage::Resize {
cols: new_cols,
rows: new_rows,
Expand Down
27 changes: 27 additions & 0 deletions src/raw_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,21 @@ pub(crate) fn events_require_host_surface_redraw(
.any(|event| matches!(event, RawInputEvent::OuterFocusGained))
}

/// A host terminal can be re-created underneath a live client. Web VS Code, for
/// example, restores a reconnected terminal from a serialized snapshot that
/// brings back mouse tracking without the SGR encoding herdr asked for, so every
/// mouse report then arrives in an encoding the client cannot parse and leaks
/// into the focused pane as text. Regained focus is the earliest signal that the
/// surface may be new, so use it to re-assert the host terminal modes.
///
/// This is deliberately independent of `redraw_on_focus_gained`: that option
/// only controls repainting, while a stale mode set corrupts input regardless.
pub(crate) fn events_require_host_mode_refresh(events: &[RawInputEvent]) -> bool {
events
.iter()
.any(|event| matches!(event, RawInputEvent::OuterFocusGained))
}

#[cfg(any(not(windows), test))]
pub(crate) fn events_require_host_terminal_theme_query(events: &[RawInputEvent]) -> bool {
events
Expand Down Expand Up @@ -1484,6 +1499,18 @@ mod tests {
assert!(!events_require_host_surface_redraw(&events, true));
}

#[test]
fn outer_focus_gained_requests_host_mode_refresh() {
let events = parse_raw_input_bytes_sync(b"\x1b[I");
assert!(events_require_host_mode_refresh(&events));

let events = parse_raw_input_bytes_sync(b"\x1b[O");
assert!(!events_require_host_mode_refresh(&events));

let events = parse_raw_input_bytes_sync(b"a");
assert!(!events_require_host_mode_refresh(&events));
}

#[test]
fn parses_ghostty_color_scheme_reports() {
for bytes in [
Expand Down
Loading