diff --git a/src/client/input.rs b/src/client/input.rs index fd374491fe..9e0d300ebd 100644 --- a/src/client/input.rs +++ b/src/client/input.rs @@ -189,7 +189,7 @@ fn idle_flush_timeout_ms( host_mouse_capture_active: bool, ) -> i32 { if host_mouse_capture_active - && (framer.has_pending_lone_escape() || framer.has_pending_incomplete_sgr_mouse_sequence()) + && (framer.has_pending_lone_escape() || framer.has_pending_incomplete_mouse_sequence()) { crate::raw_input::MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS } else { @@ -543,18 +543,20 @@ mod tests { 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 mouse = crate::raw_input::RawInputByteFramer::default(); - assert!(mouse.push(b"\x1b[<3").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(); + assert!(default_mouse.push(b"\x1b[MC").is_empty()); let mut unrelated = crate::raw_input::RawInputByteFramer::default(); assert!(unrelated.push(b"\x1b[49:33;2:").is_empty()); - for framer in [&escape, &mouse, &unrelated] { + for framer in [&escape, &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, &mouse] { + for framer in [&escape, &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 5bd5771916..12bcba4d89 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -176,8 +176,8 @@ impl RawInputFramer { self.byte_framer.has_pending_input() } - pub(crate) fn has_pending_incomplete_sgr_mouse_sequence(&self) -> bool { - self.byte_framer.has_pending_incomplete_sgr_mouse_sequence() + pub(crate) fn has_pending_incomplete_mouse_sequence(&self) -> bool { + self.byte_framer.has_pending_incomplete_mouse_sequence() } #[cfg(any(windows, test))] @@ -277,8 +277,9 @@ impl RawInputByteFramer { self.buffer.as_slice() == [ESC] } - pub(crate) fn has_pending_incomplete_sgr_mouse_sequence(&self) -> bool { + pub(crate) fn has_pending_incomplete_mouse_sequence(&self) -> bool { starts_with_incomplete_sgr_mouse_sequence(&self.buffer) + || starts_with_incomplete_default_mouse_sequence(&self.buffer) } #[cfg(any(windows, test))] @@ -586,7 +587,7 @@ pub(crate) fn events_require_host_terminal_theme_query(events: &[RawInputEvent]) } fn input_flush_timeout_ms(framer: &RawInputFramer) -> i32 { - if framer.has_pending_incomplete_sgr_mouse_sequence() { + if framer.has_pending_incomplete_mouse_sequence() { MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS } else { RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS @@ -784,6 +785,12 @@ fn extract_one_event(buffer: &[u8]) -> Option<(RawInputEvent, usize)> { if buffer[0] == ESC { let seq_len = complete_escape_sequence_len(buffer)?; + if buffer[..seq_len].starts_with(b"\x1b[M") { + let event = parse_default_mouse(&buffer[..seq_len]) + .map(RawInputEvent::Mouse) + .unwrap_or(RawInputEvent::Unsupported); + return Some((event, seq_len)); + } let seq = std::str::from_utf8(&buffer[..seq_len]).ok()?; if let Some((kind, color)) = parse_default_color_response(seq) { @@ -980,6 +987,13 @@ fn complete_escape_sequence_len(buffer: &[u8]) -> Option { } } + if buffer.len() >= 7 + && buffer.starts_with(b"\x1b\x1b[M") + && parse_default_mouse(&buffer[1..7]).is_some() + { + return Some(1); + } + if buffer.starts_with(b"\x1b\x1b") { return complete_escape_sequence_len(&buffer[1..]).map(|len| len + 1); } @@ -988,6 +1002,9 @@ fn complete_escape_sequence_len(buffer: &[u8]) -> Option { if buffer.starts_with(b"\x1b[<") { return find_csi_final(buffer, b"Mm"); } + if buffer.starts_with(b"\x1b[M") { + return (buffer.len() >= 6).then_some(6); + } return find_csi_final( buffer, b"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", @@ -1020,6 +1037,10 @@ fn starts_with_incomplete_sgr_mouse_sequence(buffer: &[u8]) -> bool { .all(|byte| byte.is_ascii_digit() || *byte == b';') } +fn starts_with_incomplete_default_mouse_sequence(buffer: &[u8]) -> bool { + buffer.starts_with(b"\x1b[M") && buffer.len() < 6 +} + fn starts_with_incomplete_orphaned_sgr_mouse_tail(buffer: &[u8]) -> bool { if buffer.len() > MAX_ORPHANED_SGR_MOUSE_TAIL_BYTES { return false; @@ -1166,6 +1187,23 @@ fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option { .position(|window| window == needle) } +fn parse_default_mouse(sequence: &[u8]) -> Option { + let &[ESC, b'[', b'M', encoded_cb, encoded_column, encoded_row] = sequence else { + return None; + }; + let cb = encoded_cb.checked_sub(32)?; + let column = u16::from(encoded_column).checked_sub(33)?; + let row = u16::from(encoded_row).checked_sub(33)?; + let (kind, modifiers) = parse_mouse_cb(cb)?; + + Some(MouseEvent { + kind, + column, + row, + modifiers, + }) +} + fn parse_sgr_mouse(sequence: &str) -> Option { let body = sequence.strip_prefix("\x1b[<")?; let final_char = body.chars().last()?; @@ -1361,6 +1399,28 @@ mod tests { assert_eq!(mouse.modifiers, KeyModifiers::empty()); } + #[test] + fn parses_default_mouse_encoding() { + let events = parse_raw_input_bytes_sync(b"\x1b[MCN1"); + let [RawInputEvent::Mouse(mouse)] = events.as_slice() else { + panic!("expected one mouse event"); + }; + assert_eq!(mouse.kind, MouseEventKind::Moved); + assert_eq!((mouse.column, mouse.row), (45, 16)); + assert_eq!(mouse.modifiers, KeyModifiers::empty()); + } + + #[test] + fn rejected_default_mouse_frame_preserves_trailing_input() { + let events = parse_raw_input_bytes_with_ranges(b"\x1b[M\x82AAx"); + + assert_eq!(events.len(), 2); + assert!(matches!(events[0].event, RawInputEvent::Unsupported)); + assert_eq!((events[0].start, events[0].len), (0, 6)); + assert!(matches!(events[1].event, RawInputEvent::Key(_))); + assert_eq!((events[1].start, events[1].len), (6, 1)); + } + #[test] fn parses_extended_button_drag_as_mouse_motion() { for input in [ @@ -1916,6 +1976,28 @@ mod tests { } } + #[test] + fn lone_escape_then_default_mouse_report_emits_both_events() { + let mut framer = RawInputFramer::default(); + + assert!(framer.push(b"\x1b").is_empty()); + let events = framer.push(b"\x1b[MCN1"); + + assert_eq!(events.len(), 2); + let mut events = events.into_iter(); + assert_raw_key(events.next().unwrap(), KeyCode::Esc, KeyModifiers::empty()); + assert!(matches!( + events.next().unwrap(), + RawInputEvent::Mouse(MouseEvent { + kind: MouseEventKind::Moved, + column: 45, + row: 16, + .. + }) + )); + assert!(framer.flush_timeout().is_empty()); + } + #[test] fn legacy_doubled_escape_alt_arrow_remains_one_event() { let mut framer = RawInputFramer::default(); @@ -1950,14 +2032,28 @@ mod tests { } #[test] - fn legacy_reader_extends_only_incomplete_sgr_mouse_timeout() { - let mut mouse = RawInputFramer::default(); - assert!(mouse.push(b"\x1b[<3").is_empty()); + fn legacy_reader_extends_incomplete_mouse_timeouts() { + let mut sgr_mouse = RawInputFramer::default(); + assert!(sgr_mouse.push(b"\x1b[<3").is_empty()); assert_eq!( - input_flush_timeout_ms(&mouse), + input_flush_timeout_ms(&sgr_mouse), MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS ); + let report = b"\x1b[MCN1"; + for split in 3..report.len() { + let mut default_mouse = RawInputFramer::default(); + assert!(default_mouse.push(&report[..split]).is_empty()); + assert_eq!( + input_flush_timeout_ms(&default_mouse), + MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS + ); + assert!(matches!( + default_mouse.push(&report[split..]).as_slice(), + [RawInputEvent::Mouse(_)] + )); + } + let mut escape = RawInputFramer::default(); assert!(escape.push(b"\x1b").is_empty()); assert_eq!(