aboutsummaryrefslogtreecommitdiff
path: root/src/client/input.rs
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-09-26 02:20:08 +0300
committerGitHub <noreply@github.com>2026-09-26 01:20:08 +0200
commit7184be49fe0dbaf62a53ebd56ac5a830c9fb6abf (patch)
treebc195c9b3f3f61b3933d3fba71126e042d6e17bd /src/client/input.rs
parent21d0ce60267ad947c081d3d3fba401c859f06dd2 (diff)
fix: retain mouse input grace for split csi prefixes (#4636)
refs #4630 Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Diffstat (limited to 'src/client/input.rs')
-rw-r--r--src/client/input.rs52
1 files changed, 49 insertions, 3 deletions
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 {
@@ -770,9 +774,51 @@ mod tests {
}
#[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