fix(input): parse default mouse reports (#2312)

* fix(input): parse default mouse reports

refs #2309

* fix(input): preserve split default mouse reports

refs #2309

---------

Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Co-authored-by: Can Celik <ogulcancelik@gmail.com>
This commit is contained in:
akbash
2026-08-04 22:35:33 +03:00
committed by GitHub
co-authored by akbash-bot Can Celik
parent d8838082dc
commit ee8429fb79
2 changed files with 111 additions and 13 deletions
+7 -5
View File
@@ -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
+104 -8
View File
@@ -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<usize> {
}
}
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<usize> {
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<usize> {
.position(|window| window == needle)
}
fn parse_default_mouse(sequence: &[u8]) -> Option<MouseEvent> {
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<MouseEvent> {
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!(