aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-18 02:51:29 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-18 02:51:29 +0300
commitc1f7b6f331501a784bfad896310eae1d064bd9b9 (patch)
treebcab5fd693c024543ab90ea766f56153c4fee891 /src
parente298e4589050349630d0faaec00ef11555b83ae1 (diff)
fix: preserve lone escape input boundaries
refs #541
Diffstat (limited to 'src')
-rw-r--r--src/client/input.rs28
-rw-r--r--src/client/input/windows_vti.rs10
-rw-r--r--src/platform/mod.rs2
-rw-r--r--src/raw_input.rs46
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();