diff options
| author | Pierre Kisters <1524059+lhns@users.noreply.github.com> | 2026-07-17 15:24:52 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-17 15:24:52 +0200 |
| commit | d9682a2eef5ac0236808f05337f4f224bcf9d55f (patch) | |
| tree | d0fc3b71324b4caa14ca99a89f0e07f74bd4a7fe /zellij-utils/src | |
| parent | docs(changelog): pane rename clear fix (diff) | |
| download | sl0ppij-d9682a2eef5ac0236808f05337f4f224bcf9d55f.tar.gz sl0ppij-d9682a2eef5ac0236808f05337f4f224bcf9d55f.tar.xz sl0ppij-d9682a2eef5ac0236808f05337f4f224bcf9d55f.zip | |
fix: input spam when moving mouse while typing by splitting the input bytes into their respective events (#5323)
Diffstat (limited to 'zellij-utils/src')
| -rw-r--r-- | zellij-utils/src/vendored/termwiz/input.rs | 264 |
1 files changed, 235 insertions, 29 deletions
diff --git a/zellij-utils/src/vendored/termwiz/input.rs b/zellij-utils/src/vendored/termwiz/input.rs index e9797412..6010896c 100644 --- a/zellij-utils/src/vendored/termwiz/input.rs +++ b/zellij-utils/src/vendored/termwiz/input.rs @@ -1016,7 +1016,7 @@ mod windows { let mut buf = [0u8; 4]; self.buf .extend_with(unicode.encode_utf8(&mut buf).as_bytes()); - self.process_bytes(callback, true); + self.process_bytes(|e, _consumed| callback(e), true); return; }, _ => match event.wVirtualKeyCode as i32 { @@ -1209,7 +1209,7 @@ mod windows { _ => {}, } } - self.process_bytes(callback, false); + self.process_bytes(|e, _consumed| callback(e), false); } } } @@ -1594,7 +1594,13 @@ impl InputParser { } } - fn dispatch_callback<F: FnMut(InputEvent)>(&mut self, mut callback: F, event: InputEvent) { + fn dispatch_callback<F: FnMut(InputEvent, usize)>( + &mut self, + mut callback: F, + event: InputEvent, + ) { + // `self.buf` is already advanced past this event, so `self.buf.len()` is + // the remainder `parse_with_consumed` diffs into a per-event byte count. match (self.state, &event) { ( InputState::Normal, @@ -1614,10 +1620,13 @@ impl InputParser { ) => { // The prior ESC was not part of an ALT sequence, so emit // it before we start collecting for paste. - callback(InputEvent::Key(KeyEvent { - key: KeyCode::Escape, - modifiers: Modifiers::NONE, - })); + callback( + InputEvent::Key(KeyEvent { + key: KeyCode::Escape, + modifiers: Modifiers::NONE, + }), + self.buf.len(), + ); self.state = InputState::Pasting(0); }, (InputState::EscapeMaybeAlt, InputEvent::Key(KeyEvent { key, modifiers })) => { @@ -1625,21 +1634,27 @@ impl InputParser { let key = *key; let modifiers = *modifiers; self.state = InputState::Normal; - callback(InputEvent::Key(KeyEvent { - key, - modifiers: modifiers | Modifiers::ALT, - })); + callback( + InputEvent::Key(KeyEvent { + key, + modifiers: modifiers | Modifiers::ALT, + }), + self.buf.len(), + ); }, (InputState::EscapeMaybeAlt, _) => { // The prior ESC was not part of an ALT sequence, so emit // both it and the current event - callback(InputEvent::Key(KeyEvent { - key: KeyCode::Escape, - modifiers: Modifiers::NONE, - })); - callback(event); + callback( + InputEvent::Key(KeyEvent { + key: KeyCode::Escape, + modifiers: Modifiers::NONE, + }), + self.buf.len(), + ); + callback(event, self.buf.len()); }, - (_, _) => callback(event), + (_, _) => callback(event, self.buf.len()), } } @@ -1650,17 +1665,20 @@ impl InputParser { /// match — those sequences are autonomous host events and cannot be /// ALT-combined with the parked ESC, so the ESC must be flushed /// before the sequence is emitted. - fn flush_parked_esc_if_held<F: FnMut(InputEvent)>(&mut self, callback: &mut F) { + fn flush_parked_esc_if_held<F: FnMut(InputEvent, usize)>(&mut self, callback: &mut F) { if self.state == InputState::EscapeMaybeAlt { - callback(InputEvent::Key(KeyEvent { - key: KeyCode::Escape, - modifiers: Modifiers::NONE, - })); + callback( + InputEvent::Key(KeyEvent { + key: KeyCode::Escape, + modifiers: Modifiers::NONE, + }), + self.buf.len(), + ); self.state = InputState::Normal; } } - fn process_bytes<F: FnMut(InputEvent)>(&mut self, mut callback: F, maybe_more: bool) { + fn process_bytes<F: FnMut(InputEvent, usize)>(&mut self, mut callback: F, maybe_more: bool) { while !self.buf.is_empty() { match self.state { InputState::Pasting(offset) => { @@ -1669,7 +1687,7 @@ impl InputParser { let pasted = String::from_utf8_lossy(&self.buf.as_slice()[0..idx]).to_string(); self.buf.advance(pasted.len() + end_paste.len()); - callback(InputEvent::Paste(pasted)); + callback(InputEvent::Paste(pasted), self.buf.len()); self.state = InputState::Normal; } else { self.state = @@ -1693,7 +1711,7 @@ impl InputParser { if let Some((event, len)) = parse_sgr_mouse(self.buf.as_slice()) { self.flush_parked_esc_if_held(&mut callback); self.buf.advance(len); - callback(event); + callback(event, self.buf.len()); continue; } @@ -1701,7 +1719,7 @@ impl InputParser { if let Some((event, len)) = parse_osc(self.buf.as_slice()) { self.flush_parked_esc_if_held(&mut callback); self.buf.advance(len); - callback(event); + callback(event, self.buf.len()); continue; } @@ -1724,7 +1742,7 @@ impl InputParser { if let Some((event, len)) = parse_csi_report(self.buf.as_slice()) { self.flush_parked_esc_if_held(&mut callback); self.buf.advance(len); - callback(event); + callback(event, self.buf.len()); continue; } @@ -1761,8 +1779,10 @@ impl InputParser { self.buf.advance(len); }, (Found::Exact(len, event), _) | (Found::Ambiguous(len, event), false) => { - self.dispatch_callback(&mut callback, event.clone()); + // Advance before dispatching so `self.buf.len()` inside + // `dispatch_callback` already reflects this key's consumption. self.buf.advance(len); + self.dispatch_callback(&mut callback, event.clone()); }, (Found::Ambiguous(_, _), true) | (Found::NeedData, true) => { // The keymap is signalling "this buffer @@ -1841,8 +1861,34 @@ impl InputParser { /// with an empty slice and `maybe_more=false` to allow the partial /// data to be recognized and processed. pub fn parse<F: FnMut(InputEvent)>(&mut self, bytes: &[u8], callback: F, maybe_more: bool) { + // rebind (not `mut callback: F`) to keep the upstream signature intact + let mut callback = callback; + self.parse_with_consumed(bytes, |event, _consumed| callback(event), maybe_more); + } + + /// Like [`InputParser::parse`], but the callback also receives the number + /// of input bytes consumed to produce each event. This allows a caller + /// that forwards raw bytes alongside decoded events to attribute to each + /// event exactly the bytes that produced it when a single chunk of input + /// decodes into multiple events. + pub fn parse_with_consumed<F: FnMut(InputEvent, usize)>( + &mut self, + bytes: &[u8], + mut callback: F, + maybe_more: bool, + ) { self.buf.extend_with(bytes); - self.process_bytes(callback, maybe_more); + // `process_bytes` reports the bytes still buffered after each event; the + // drop between successive remainders is what that event consumed. + let mut prev_remaining = self.buf.len(); + self.process_bytes( + |event, remaining| { + let consumed = prev_remaining.saturating_sub(remaining); + prev_remaining = remaining; + callback(event, consumed); + }, + maybe_more, + ); } pub fn parse_as_vec(&mut self, bytes: &[u8], maybe_more: bool) -> Vec<InputEvent> { @@ -1950,6 +1996,166 @@ mod test { ); } + /// Parse `bytes` and pair each event with the raw bytes it consumed, + /// draining from a copy of the input the same way the client's stdin + /// loop attributes raw bytes to events. + fn parse_with_raw_bytes(bytes: &[u8], maybe_more: bool) -> Vec<(InputEvent, Vec<u8>)> { + let mut p = InputParser::new(); + let mut collected: Vec<(InputEvent, usize)> = Vec::new(); + p.parse_with_consumed(bytes, |ev, n| collected.push((ev, n)), maybe_more); + let mut buffer: Vec<u8> = bytes.to_vec(); + collected + .into_iter() + .map(|(ev, n)| { + let take = n.min(buffer.len()); + let raw: Vec<u8> = buffer.drain(..take).collect(); + (ev, raw) + }) + .collect() + } + + #[test] + fn typed_char_keeps_only_its_own_bytes_before_mouse_reports() { + // A keystroke and two mouse reports arrive in one read: the key must be + // paired with only its own byte and each report with its own bytes. + let events = parse_with_raw_bytes(b"a\x1b[<35;52;16M\x1b[<35;49;16M", MAYBE_MORE); + assert_eq!( + events.len(), + 3, + "expected key + 2 mouse events, got {:?}", + events + ); + assert!( + matches!( + events[0].0, + InputEvent::Key(KeyEvent { + key: KeyCode::Char('a'), + .. + }) + ), + "first event should be the typed key, got {:?}", + events[0].0 + ); + assert_eq!( + events[0].1, b"a", + "the keystroke must not carry the trailing mouse bytes" + ); + assert!(matches!(events[1].0, InputEvent::Mouse(_))); + assert_eq!(events[1].1, b"\x1b[<35;52;16M"); + assert!(matches!(events[2].0, InputEvent::Mouse(_))); + assert_eq!(events[2].1, b"\x1b[<35;49;16M"); + } + + #[test] + fn typed_char_keeps_only_its_own_bytes_after_mouse_reports() { + // A mouse report precedes the keystroke in the read; the key must still + // be paired with only its own byte. + let events = parse_with_raw_bytes(b"\x1b[<35;52;16Ma", MAYBE_MORE); + assert_eq!(events.len(), 2, "got {:?}", events); + assert!(matches!(events[0].0, InputEvent::Mouse(_))); + assert_eq!(events[0].1, b"\x1b[<35;52;16M"); + assert!(matches!( + events[1].0, + InputEvent::Key(KeyEvent { + key: KeyCode::Char('a'), + .. + }) + )); + assert_eq!(events[1].1, b"a"); + } + + #[test] + fn consecutive_chars_before_mouse_each_keep_one_byte() { + // Consecutive keystrokes in one read are each paired with their own byte. + let events = parse_with_raw_bytes(b"ab\x1b[<35;52;16M", MAYBE_MORE); + assert_eq!(events.len(), 3, "got {:?}", events); + assert_eq!(events[0].1, b"a"); + assert_eq!(events[1].1, b"b"); + assert_eq!(events[2].1, b"\x1b[<35;52;16M"); + } + + #[test] + fn single_event_keeps_all_its_bytes() { + // A read that decodes into a single event pairs it with all of the + // read's bytes, including a multi-byte sequence (`\x1bOA`). + let events = parse_with_raw_bytes(b"\x1bOA", NO_MORE); + assert_eq!(events.len(), 1, "got {:?}", events); + assert!(matches!( + events[0].0, + InputEvent::Key(KeyEvent { + key: KeyCode::ApplicationUpArrow, + .. + }) + )); + assert_eq!(events[0].1, b"\x1bOA"); + } + + #[test] + fn lone_esc_batch_then_mouse_report_batch() { + // A lone ESC arrives in one batch and a complete mouse report in the + // next. The ESC is held until the following batch disambiguates it; + // both events are then emitted, each paired with its own bytes. + let mut p = InputParser::new(); + let mut events: Vec<(InputEvent, usize)> = Vec::new(); + let mut buffer: Vec<u8> = Vec::new(); + + buffer.extend_from_slice(b"\x1b"); + p.parse_with_consumed(b"\x1b", |ev, n| events.push((ev, n)), MAYBE_MORE); + assert!( + events.is_empty(), + "a lone ESC with more data possibly coming must be held, got {:?}", + events + ); + + buffer.extend_from_slice(b"\x1b[<35;62;16M"); + p.parse_with_consumed(b"\x1b[<35;62;16M", |ev, n| events.push((ev, n)), MAYBE_MORE); + assert_eq!(events.len(), 2, "got {:?}", events); + assert!(matches!( + events[0].0, + InputEvent::Key(KeyEvent { + key: KeyCode::Escape, + .. + }) + )); + assert_eq!(events[0].1, 1, "the ESC consumed its single byte"); + assert!(matches!(events[1].0, InputEvent::Mouse(_))); + assert_eq!(events[1].1, 12, "the mouse report consumed its 12 bytes"); + + // Draining the accumulated bytes per event, the way the client's + // stdin loop does, pairs each event with its own raw bytes. + let esc_bytes: Vec<u8> = buffer.drain(..events[0].1).collect(); + let mouse_bytes: Vec<u8> = buffer.drain(..events[1].1).collect(); + assert_eq!(esc_bytes, b"\x1b"); + assert_eq!(mouse_bytes, b"\x1b[<35;62;16M"); + assert!(buffer.is_empty()); + } + + #[test] + fn newline_then_carriage_return_are_two_enter_events_with_their_own_bytes() { + // In the legacy encoding a terminal sends `\r` for the Enter key and + // `\n` for a control-j style newline; the keymap decodes both to + // Enter. Arriving together they are two Enter events, each paired + // with its own byte. + let events = parse_with_raw_bytes(b"\n\r", MAYBE_MORE); + assert_eq!(events.len(), 2, "got {:?}", events); + for (event, raw) in &events { + assert!( + matches!( + event, + InputEvent::Key(KeyEvent { + key: KeyCode::Enter, + .. + }) + ), + "expected an Enter key event, got {:?}", + event + ); + assert_eq!(raw.len(), 1, "each Enter is paired with a single byte"); + } + assert_eq!(events[0].1, b"\n"); + assert_eq!(events[1].1, b"\r"); + } + #[test] fn partial() { let mut p = InputParser::new(); |
