aboutsummaryrefslogtreecommitdiff
path: root/src/client/input/windows_vti.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/input/windows_vti.rs')
-rw-r--r--src/client/input/windows_vti.rs303
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());