From 7184be49fe0dbaf62a53ebd56ac5a830c9fb6abf Mon Sep 17 00:00:00 2001 From: akbash Date: Sat, 26 Sep 2026 02:20:08 +0300 Subject: fix: retain mouse input grace for split csi prefixes (#4636) refs #4630 Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com> --- src/client/input.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++++--- src/raw_input.rs | 5 +++++ 2 files changed, 54 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/client/input.rs b/src/client/input.rs index 45656f84..23067576 100644 --- a/src/client/input.rs +++ b/src/client/input.rs @@ -371,8 +371,12 @@ fn idle_flush_timeout_ms( framer: &crate::raw_input::RawInputByteFramer, host_mouse_capture_active: bool, ) -> i32 { + // A mouse report split after ESC[ is still ambiguous with legacy Alt+[. + // Give it the mouse-active grace period without changing timeout decoding. if host_mouse_capture_active - && (framer.has_pending_lone_escape() || framer.has_pending_incomplete_mouse_sequence()) + && (framer.has_pending_lone_escape() + || framer.has_pending_csi_introducer() + || framer.has_pending_incomplete_mouse_sequence()) { crate::raw_input::MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS } else { @@ -769,10 +773,52 @@ mod tests { assert_eq!(framer.flush_timeout().len(), 1); } + #[test] + fn captured_mouse_report_split_after_csi_survives_idle_gap() { + let mut framer = crate::raw_input::RawInputByteFramer::for_host_input(); + framer.enable_host_appearance_query_on_focus(); + assert_eq!(framer.push(b"\x1b[I"), vec![b"\x1b[I".to_vec()]); + assert!(framer.push(b"\x1b[").is_empty()); + + // #4630 input.bin offsets 10465/10467: ESC[ and its continuation + // arrived 32.937 ms apart. Apply the Unix reader's two idle waits + // without sleeping, using its production timeout selector. + let gap = std::time::Duration::from_micros(32_937); + let first_wait = + std::time::Duration::from_millis(idle_flush_timeout_ms(&framer, true) as u64); + let mut chunks = Vec::new(); + if gap >= first_wait { + chunks.extend(framer.flush_timeout()); + if chunks.is_empty() + && gap + >= first_wait + + std::time::Duration::from_millis( + crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS as u64, + ) + { + chunks.extend(framer.flush_timeout()); + } + } + chunks.extend(framer.push(b"<35;64;37M\x1b[<35;65;36M\x1b[<35;64;36M")); + assert_eq!( + chunks, + vec![ + b"\x1b[<35;64;37M".to_vec(), + b"\x1b[<35;65;36M".to_vec(), + b"\x1b[<35;64;36M".to_vec(), + ], + "captured mouse reports must remain whole, not become key fragments" + ); + assert!(!framer.has_pending_input()); + assert_eq!(framer.push(b"x"), vec![b"x".to_vec()]); + } + #[test] fn mouse_active_escape_sequences_get_longer_reassembly_window() { let mut escape = crate::raw_input::RawInputByteFramer::default(); assert!(escape.push(b"\x1b").is_empty()); + let mut csi = crate::raw_input::RawInputByteFramer::default(); + assert!(csi.push(b"\x1b[").is_empty()); let mut sgr_mouse = crate::raw_input::RawInputByteFramer::default(); assert!(sgr_mouse.push(b"\x1b[<3").is_empty()); let mut default_mouse = crate::raw_input::RawInputByteFramer::default(); @@ -780,13 +826,13 @@ mod tests { let mut unrelated = crate::raw_input::RawInputByteFramer::default(); assert!(unrelated.push(b"\x1b[49:33;2:").is_empty()); - for framer in [&escape, &sgr_mouse, &default_mouse, &unrelated] { + for framer in [&escape, &csi, &sgr_mouse, &default_mouse, &unrelated] { assert_eq!( idle_flush_timeout_ms(framer, false), crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS ); } - for framer in [&escape, &sgr_mouse, &default_mouse] { + for framer in [&escape, &csi, &sgr_mouse, &default_mouse] { assert_eq!( idle_flush_timeout_ms(framer, true), crate::raw_input::MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS diff --git a/src/raw_input.rs b/src/raw_input.rs index cfbcba62..b2c3beda 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -229,6 +229,11 @@ impl RawInputByteFramer { self.buffer.as_slice() == [ESC] } + #[cfg(unix)] + pub(crate) fn has_pending_csi_introducer(&self) -> bool { + self.buffer.as_slice() == b"\x1b[" + } + #[cfg(unix)] pub(crate) fn has_pending_incomplete_mouse_sequence(&self) -> bool { starts_with_incomplete_sgr_mouse_sequence(&self.buffer) -- cgit v1.2.3