diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-18 02:51:29 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-18 02:51:29 +0300 |
| commit | c1f7b6f331501a784bfad896310eae1d064bd9b9 (patch) | |
| tree | bcab5fd693c024543ab90ea766f56153c4fee891 /src | |
| parent | e298e4589050349630d0faaec00ef11555b83ae1 (diff) | |
fix: preserve lone escape input boundaries
refs #541
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/input.rs | 28 | ||||
| -rw-r--r-- | src/client/input/windows_vti.rs | 10 | ||||
| -rw-r--r-- | src/platform/mod.rs | 2 | ||||
| -rw-r--r-- | src/raw_input.rs | 46 |
4 files changed, 74 insertions, 12 deletions
diff --git a/src/client/input.rs b/src/client/input.rs index a49c4316..1d557543 100644 --- a/src/client/input.rs +++ b/src/client/input.rs @@ -66,7 +66,7 @@ fn unix_stdin_reader_loop( let stdin = io::stdin(); let mut reader = stdin.lock(); let mut scratch = [0u8; 4096]; - let mut framer = crate::raw_input::RawInputByteFramer::default(); + let mut framer = crate::raw_input::RawInputByteFramer::for_host_input(); if host_color_query_sent { framer.host_color_query_sent(); framer.enable_host_color_scheme_change_tracking(); @@ -164,19 +164,17 @@ fn windows_crossterm_reader_loop( event_tx: mpsc::Sender<ClientLoopEvent>, should_quit: &Arc<AtomicBool>, ) { - let mut framer = crate::raw_input::RawInputFramer::default(); - let mut raw_sequence_pending = false; + let mut framer = crate::raw_input::RawInputFramer::for_host_input(); while !should_quit.load(Ordering::Acquire) { match crossterm::event::poll(Duration::from_millis(10)) { Ok(true) => {} Ok(false) => { - if raw_sequence_pending { + if framer.has_pending_input() { tracing::debug!("windows input raw sequence timed out; flushing"); if !send_windows_raw_events(framer.flush_timeout(), &event_tx) { return; } - raw_sequence_pending = false; } continue; } @@ -188,15 +186,14 @@ fn windows_crossterm_reader_loop( Err(_) => break, }; + let raw_sequence_pending = framer.has_pending_input(); if let Some(bytes) = windows_key_raw_bytes(&event, raw_sequence_pending) { tracing::debug!( bytes = ?bytes, pending_before = raw_sequence_pending, "windows input routed through raw framer" ); - let events = framer.push(&bytes); - raw_sequence_pending = events.is_empty(); - if !send_windows_raw_events(events, &event_tx) { + if !send_windows_raw_events(framer.push(&bytes), &event_tx) { return; } continue; @@ -207,7 +204,6 @@ fn windows_crossterm_reader_loop( if !send_windows_raw_events(framer.flush_timeout(), &event_tx) { return; } - raw_sequence_pending = false; } if windows_event_is_control_key(&event) { @@ -225,7 +221,7 @@ fn windows_crossterm_reader_loop( } } - if raw_sequence_pending { + if framer.has_pending_input() { let _ = send_windows_raw_events(framer.flush_timeout(), &event_tx); } } @@ -403,6 +399,18 @@ mod tests { assert!(timeout_ms <= 20); } + #[cfg(not(target_os = "macos"))] + #[test] + fn windows_repeated_escape_keeps_second_escape_pending() { + let mut framer = crate::raw_input::RawInputFramer::for_host_input(); + + let events = framer.push(b"\x1b\x1b"); + + assert_eq!(events.len(), 1); + assert!(framer.has_pending_input()); + assert_eq!(framer.flush_timeout().len(), 1); + } + #[test] fn mouse_active_escape_sequences_get_longer_reassembly_window() { let mut escape = crate::raw_input::RawInputByteFramer::default(); diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs index 6404e586..3b29e790 100644 --- a/src/client/input/windows_vti.rs +++ b/src/client/input/windows_vti.rs @@ -191,12 +191,20 @@ struct WindowsInputMapper { win32_input: WindowsWin32InputModeFramer, } -#[derive(Default)] struct WindowsInputPump { framer: crate::raw_input::RawInputFramer, paste_from_win32_key_records: bool, } +impl Default for WindowsInputPump { + fn default() -> Self { + Self { + framer: crate::raw_input::RawInputFramer::for_host_input(), + paste_from_win32_key_records: false, + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] enum PlatformInputItem { Bytes(Vec<u8>), diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 1415181e..fbde21b7 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -47,6 +47,7 @@ pub(crate) struct PlatformCapabilities { pub(crate) live_handoff: bool, pub(crate) remote_attach: bool, pub(crate) direct_terminal_attach: bool, + pub(crate) preserve_legacy_doubled_escape_input: bool, } pub(crate) const fn capabilities() -> PlatformCapabilities { @@ -54,6 +55,7 @@ pub(crate) const fn capabilities() -> PlatformCapabilities { live_handoff: cfg!(unix), remote_attach: cfg!(unix), direct_terminal_attach: cfg!(unix), + preserve_legacy_doubled_escape_input: cfg!(target_os = "macos"), } } diff --git a/src/raw_input.rs b/src/raw_input.rs index 848df88d..a6ca316a 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -128,6 +128,12 @@ pub(crate) struct RawInputFramer { } impl RawInputFramer { + pub(crate) fn for_host_input() -> Self { + Self { + byte_framer: RawInputByteFramer::for_host_input(), + } + } + pub(crate) fn push(&mut self, data: &[u8]) -> Vec<RawInputEvent> { Self::events_from_chunks(self.byte_framer.push(data)) } @@ -185,12 +191,26 @@ pub(crate) struct RawInputByteFramer { host_color_replies_awaited: u8, held_pending_color_esc: bool, host_color_scheme_change_tracking: bool, + split_coalesced_escape: bool, } const HOST_COLOR_QUERY_REPLIES: u8 = 2; const MAX_ORPHANED_SGR_MOUSE_TAIL_BYTES: usize = 32; impl RawInputByteFramer { + pub(crate) fn for_host_input() -> Self { + Self::with_host_input_policy( + crate::platform::capabilities().preserve_legacy_doubled_escape_input, + ) + } + + fn with_host_input_policy(preserve_legacy_doubled_escape_input: bool) -> Self { + Self { + split_coalesced_escape: !preserve_legacy_doubled_escape_input, + ..Self::default() + } + } + pub(crate) fn push(&mut self, data: &[u8]) -> Vec<Vec<u8>> { self.buffer.extend_from_slice(data); self.drain_available_chunks() @@ -405,6 +425,12 @@ impl RawInputByteFramer { continue; } + if self.split_coalesced_escape && self.buffer.starts_with(b"\x1b\x1b") { + chunks.push(vec![ESC]); + self.buffer.drain(..1); + continue; + } + let Some((event, consumed)) = extract_one_event(&self.buffer) else { break; }; @@ -491,7 +517,7 @@ pub fn spawn_input_reader() -> mpsc::Receiver<RawInputEvent> { let stdin = std::io::stdin(); let mut reader = stdin.lock(); let mut scratch = [0u8; 1024]; - let mut framer = RawInputFramer::default(); + let mut framer = RawInputFramer::for_host_input(); framer.host_color_query_sent(); framer.enable_host_color_scheme_change_tracking(); @@ -1620,6 +1646,24 @@ mod tests { assert!(framer.flush_timeout().is_empty()); } + #[cfg(not(target_os = "macos"))] + #[test] + fn non_macos_host_input_splits_lone_escape_from_arrow() { + let mut framer = RawInputByteFramer::for_host_input(); + + assert_eq!( + framer.push(b"\x1b\x1b[D"), + vec![b"\x1b".to_vec(), b"\x1b[D".to_vec()] + ); + } + + #[test] + fn macos_host_input_policy_preserves_legacy_doubled_escape_alt_arrow() { + let mut framer = RawInputByteFramer::with_host_input_policy(true); + + assert_eq!(framer.push(b"\x1b\x1b[D"), vec![b"\x1b\x1b[D".to_vec()]); + } + #[test] fn legacy_reader_extends_only_incomplete_sgr_mouse_timeout() { let mut mouse = RawInputFramer::default(); |
