diff options
Diffstat (limited to 'src/client/input/windows_vti.rs')
| -rw-r--r-- | src/client/input/windows_vti.rs | 303 |
1 files changed, 290 insertions, 13 deletions
diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs index 0deb3185..c4642885 100644 --- a/src/client/input/windows_vti.rs +++ b/src/client/input/windows_vti.rs @@ -20,9 +20,16 @@ pub(super) fn raw_console_reader_loop( handle: windows_sys::Win32::Foundation::HANDLE, event_tx: mpsc::Sender<ClientLoopEvent>, should_quit: &Arc<AtomicBool>, + host_color_query_sent: bool, ) { let mut mapper = WindowsInputMapper::default(); let mut pump = WindowsInputPump::default(); + if host_color_query_sent { + // Native records distinguish physical Escape from scan-code-zero host replies. + pump.framer + .host_default_color_query_sent(std::time::Duration::from_secs(1)); + super::super::query_host_terminal_theme(); + } let mut handoff = WindowsInputHandoff::default(); while !should_quit.load(Ordering::Acquire) { @@ -325,10 +332,12 @@ struct WindowsInputMapper { pending_paste_high_surrogate: Option<u16>, mouse_buttons: WindowsMouseButtons, win32_input: WindowsWin32InputModeFramer, + pending_escape_origin: Option<EscapeOrigin>, } struct WindowsInputPump { framer: crate::raw_input::RawInputFramer, + pending_escape_origin: Option<EscapeOrigin>, paste_from_win32_key_records: bool, pending_physical_escape: Option<(crate::protocol::ClientInputEvent, bool)>, default_mouse_candidate: DefaultMouseCandidate, @@ -346,6 +355,7 @@ impl Default for WindowsInputPump { fn default() -> Self { Self { framer: crate::raw_input::RawInputFramer::for_host_input(), + pending_escape_origin: None, paste_from_win32_key_records: false, pending_physical_escape: None, default_mouse_candidate: DefaultMouseCandidate::default(), @@ -357,6 +367,7 @@ impl Default for WindowsInputPump { #[derive(Clone, Debug, PartialEq, Eq)] enum PlatformInputItem { Bytes(Vec<u8>), + Escape(EscapeOrigin), Semantic(crate::protocol::ClientInputEvent), PasteAwareBytes { paste_bytes: Vec<u8>, @@ -372,6 +383,12 @@ enum PlatformInputItem { }, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum EscapeOrigin { + HostReply, + Keyboard, +} + #[derive(Default)] struct WindowsWin32InputModeFramer { buffer: Vec<u8>, @@ -386,6 +403,17 @@ enum WindowsWin32InputModeItem { } impl WindowsInputPump { + fn flush_before_semantic(&mut self) -> Vec<crate::raw_input::RawInputEvent> { + // A scan-code-zero reply prefix can outlive an unrelated keyboard record. + if self.pending_escape_origin == Some(EscapeOrigin::HostReply) + && self.framer.holds_host_default_color_escape() + { + return Vec::new(); + } + self.pending_escape_origin = None; + self.framer.flush_interrupted() + } + fn process(&mut self, item: PlatformInputItem) -> Vec<crate::protocol::ClientInputEvent> { let mut events = Vec::new(); if let Some((escape, open_bracket)) = self.pending_physical_escape.take() { @@ -412,7 +440,7 @@ impl WindowsInputPump { } if let Some(escape) = item.physical_escape_press() { if !self.framer.has_pending_bracketed_paste() { - let raw_events = self.framer.flush_interrupted(); + let raw_events = self.flush_before_semantic(); events.extend(self.process_raw_events(raw_events)); self.pending_physical_escape = Some((escape, false)); return events; @@ -421,11 +449,17 @@ impl WindowsInputPump { let mut next = match item { PlatformInputItem::Bytes(bytes) => { + self.pending_escape_origin = None; let raw_events = self.framer.push(&bytes); self.process_raw_events(raw_events) } + PlatformInputItem::Escape(origin) => { + self.pending_escape_origin = Some(origin); + let raw_events = self.framer.push(b"\x1b"); + self.process_raw_events(raw_events) + } PlatformInputItem::Semantic(event) => { - let raw_events = self.framer.flush_interrupted(); + let raw_events = self.flush_before_semantic(); let mut events = self.process_raw_events(raw_events); events.push(event); events @@ -444,6 +478,11 @@ impl WindowsInputPump { self.stage_default_mouse_record(nested_record, &raw_bytes); } let decode_win32_record = self.paste_from_win32_key_records || !pending_paste; + self.pending_escape_origin = if decode_win32_record && !pending_paste { + WindowsInputMapper::raw_escape_origin(nested_record) + } else { + None + }; let raw_events = if decode_win32_record { if pending_paste { self.framer.push(&win32_paste_bytes) @@ -487,7 +526,7 @@ impl WindowsInputPump { let raw_events = if events.is_empty() { self.framer.flush_timeout() } else { - self.framer.flush_interrupted() + self.flush_before_semantic() }; let mut output = self.process_raw_events(raw_events); output.extend(events); @@ -508,7 +547,12 @@ impl WindowsInputPump { events.extend(self.process_raw_events(raw_events)); } } - let raw_events = self.framer.flush_timeout(); + let raw_events = if self.pending_escape_origin == Some(EscapeOrigin::Keyboard) { + self.pending_escape_origin = None; + self.framer.flush_keyboard_escape() + } else { + self.framer.flush_timeout() + }; events.extend(self.process_raw_events(raw_events)); events } @@ -606,6 +650,7 @@ fn remove_matching_key(keys: &mut Vec<WindowsKeyRecord>, record: WindowsKeyRecor impl PlatformInputItem { fn raw_bytes(&self) -> Option<&[u8]> { match self { + Self::Escape(_) => Some(b"\x1b"), Self::Bytes(bytes) | Self::PasteAwareBytes { raw_bytes: bytes, .. @@ -684,10 +729,18 @@ impl WindowsInputMapper { self.win32_input .flush_timeout() .into_iter() - .map(PlatformInputItem::Bytes) + .map(|bytes| self.raw_byte_item(bytes)) .collect() } + fn raw_byte_item(&mut self, bytes: Vec<u8>) -> PlatformInputItem { + let origin = self.pending_escape_origin.take(); + match origin { + Some(origin) if bytes.as_slice() == b"\x1b" => PlatformInputItem::Escape(origin), + _ => PlatformInputItem::Bytes(bytes), + } + } + fn translate(&mut self, record: WindowsInputRecord) -> Vec<PlatformInputItem> { match record { WindowsInputRecord::Key(key) => self.translate_key(key), @@ -714,7 +767,10 @@ impl WindowsInputMapper { return Vec::new(); } - if self.key_record_is_raw_escape(key) { + if let Some(origin) = Self::raw_escape_origin(key) { + if self.win32_input.buffer.is_empty() { + self.pending_escape_origin = Some(origin); + } return self.translate_win32_input_mode_bytes(&[0x1b]); } @@ -760,13 +816,13 @@ impl WindowsInputMapper { self.with_pending_win32_flush(items) } - fn key_record_is_raw_escape(&self, key: WindowsKeyRecord) -> bool { + fn raw_escape_origin(key: WindowsKeyRecord) -> Option<EscapeOrigin> { let modifiers = windows_key_modifiers(key.control_key_state); if !key.key_down || key.repeat_count.max(1) != 1 || modifiers.contains(crossterm::event::KeyModifiers::ALT) { - return false; + return None; } // Physical Escape carries a scan code. Scan-code-zero Escape can @@ -778,17 +834,22 @@ impl WindowsInputMapper { && key.unicode == 0x1b && modifiers == crossterm::event::KeyModifiers::CONTROL; - bare_escape || ctrl_bracket + if bare_escape && key.virtual_scan_code == 0 { + Some(EscapeOrigin::HostReply) + } else if bare_escape || ctrl_bracket { + Some(EscapeOrigin::Keyboard) + } else { + None + } } fn translate_win32_input_mode_bytes(&mut self, bytes: &[u8]) -> Vec<PlatformInputItem> { let mut items = Vec::new(); for item in self.win32_input.push(bytes) { match item { - WindowsWin32InputModeItem::Bytes(bytes) => { - items.push(PlatformInputItem::Bytes(bytes)) - } + WindowsWin32InputModeItem::Bytes(bytes) => items.push(self.raw_byte_item(bytes)), WindowsWin32InputModeItem::Key { bytes, record } => { + self.pending_escape_origin = None; trace_input_transport("transport=win32-serialized"); let win32_paste_bytes = self.paste_payload_bytes_for_key(record).unwrap_or_default(); @@ -818,7 +879,7 @@ impl WindowsInputMapper { &mut self, record: WindowsKeyRecord, ) -> Option<Vec<u8>> { - if self.key_record_is_raw_escape(record) { + if Self::raw_escape_origin(record).is_some() { return Some(vec![0x1b]); } @@ -1389,6 +1450,217 @@ fn windows_input_trace_enabled() -> bool { mod tests { use super::*; + #[cfg(windows)] + #[test] + fn host_default_color_replies_reach_shell_without_pane_input() { + use crate::protocol::{ + ClientHostColor, ClientHostDefaultColorKind, ClientHostThemeUpdate, ClientMessage, + }; + + let mut translator = WindowsInputTranslator::default(); + translator + .pump + .framer + .host_default_color_query_sent(std::time::Duration::from_secs(1)); + assert!(translator.translate(key_char('\x1b')).is_empty()); + for _ in 0..3 { + assert!(translator.idle().is_empty()); + } + assert!(matches!( + translator + .translate(key_vk_with_unicode(0x41, 'a', 0)) + .as_slice(), + [crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('a'), + .. + }] + )); + + let events = "]10;rgb:aaaa/bbbb/cccc\x1b\\\x1b]11;rgb:1111/2222/3333\x1b\\" + .chars() + .map(key_char) + .flat_map(|record| translator.translate(record)) + .collect::<Vec<_>>(); + let mut shell = crate::client::shell::ClientShellState::new( + crate::client::shell::ClientShellConfig::from_config(&crate::config::Config::default()), + ); + let outcome = shell.handle_client_events(&events); + assert_eq!( + outcome.requests, + vec![ + ClientMessage::ClientShellHostTheme { + update: ClientHostThemeUpdate::DefaultColor { + kind: ClientHostDefaultColorKind::Foreground, + color: ClientHostColor { + r: 0xaa, + g: 0xbb, + b: 0xcc, + }, + }, + }, + ClientMessage::ClientShellHostTheme { + update: ClientHostThemeUpdate::DefaultColor { + kind: ClientHostDefaultColorKind::Background, + color: ClientHostColor { + r: 0x11, + g: 0x22, + b: 0x33, + }, + }, + }, + ] + ); + assert!(outcome.actions.is_empty()); + } + + #[cfg(windows)] + #[test] + fn ctrl_bracket_precedes_next_key_while_host_color_reply_is_pending() { + let mut translator = WindowsInputTranslator::default(); + translator + .pump + .framer + .host_default_color_query_sent(std::time::Duration::from_secs(1)); + assert!(translator + .translate(key_vk_with_utf16_mods(0xdb, 0x1b, 0x0008)) + .is_empty()); + let events = translator.translate(key_vk_with_unicode(0x0d, '\r', 0)); + assert!(matches!( + events.as_slice(), + [ + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Esc, + .. + }, + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Enter, + .. + } + ] + )); + } + + #[cfg(windows)] + #[test] + fn ctrl_bracket_idle_does_not_cancel_later_host_color_reply() { + let mut translator = WindowsInputTranslator::default(); + translator + .pump + .framer + .host_default_color_query_sent(std::time::Duration::from_secs(1)); + assert!(translator + .translate(key_vk_with_utf16_mods(0xdb, 0x1b, 0x0008)) + .is_empty()); + assert!(matches!( + translator.idle().as_slice(), + [crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Esc, + .. + }] + )); + assert!(translator.translate(key_char('\x1b')).is_empty()); + assert!(translator.idle().is_empty()); + let events = "]10;rgb:aaaa/bbbb/cccc\x1b\\" + .chars() + .map(key_char) + .flat_map(|record| translator.translate(record)) + .collect::<Vec<_>>(); + assert!(matches!( + events.as_slice(), + [crate::protocol::ClientInputEvent::HostDefaultColor { + kind: crate::protocol::ClientHostDefaultColorKind::Foreground, + .. + }] + )); + } + + #[cfg(windows)] + #[test] + fn serialized_host_reply_escape_survives_intervening_key() { + let mut translator = WindowsInputTranslator::default(); + translator + .pump + .framer + .host_default_color_query_sent(std::time::Duration::from_secs(1)); + for record in win32_input_mode_encoded_raw_bytes(b"\x1b") { + assert!(translator.translate(record).is_empty()); + } + let key_events = translator.translate(key_vk_with_unicode(0x0d, '\r', 0)); + assert!(matches!( + key_events.as_slice(), + [crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Enter, + .. + }] + )); + let events = "]10;rgb:aaaa/bbbb/cccc\x1b\\" + .chars() + .map(key_char) + .flat_map(|record| translator.translate(record)) + .collect::<Vec<_>>(); + assert!(matches!( + events.as_slice(), + [crate::protocol::ClientInputEvent::HostDefaultColor { + kind: crate::protocol::ClientHostDefaultColorKind::Foreground, + .. + }] + )); + } + + #[cfg(windows)] + #[test] + fn expired_host_color_reply_tail_does_not_swallow_semantic_key() { + let mut translator = WindowsInputTranslator::default(); + translator + .pump + .framer + .host_default_color_query_sent(std::time::Duration::ZERO); + for record in "\x1b]11;rgb:1111/".chars().map(key_char) { + assert!(translator.translate(record).is_empty()); + } + assert!(translator.idle().is_empty()); + assert!(matches!( + translator + .translate(key_vk_with_unicode(0x0d, '\r', 0)) + .as_slice(), + [crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Enter, + .. + }] + )); + for record in "2222/3333\x1b\\".chars().map(key_char) { + assert!(translator.translate(record).is_empty()); + } + assert!(matches!( + translator + .translate(key_vk_with_unicode(0x41, 'a', 0)) + .as_slice(), + [crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('a'), + .. + }] + )); + } + + #[cfg(windows)] + #[test] + fn unanswered_host_color_query_stops_holding_escape() { + let mut translator = WindowsInputTranslator::default(); + translator + .pump + .framer + .host_default_color_query_sent(std::time::Duration::from_millis(1)); + std::thread::sleep(std::time::Duration::from_millis(10)); + assert!(translator.translate(key_char('\x1b')).is_empty()); + assert!(matches!( + translator.idle().as_slice(), + [crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Esc, + .. + }] + )); + } + fn key_char(ch: char) -> WindowsInputRecord { WindowsInputRecord::Key(WindowsKeyRecord { key_down: true, @@ -2083,6 +2355,11 @@ mod tests { }, ] { let mut translator = WindowsInputTranslator::default(); + #[cfg(windows)] + translator + .pump + .framer + .host_default_color_query_sent(std::time::Duration::from_secs(1)); assert!(translator .translate(WindowsInputRecord::Key(record)) .is_empty()); |
