diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/input/windows_vti.rs | 97 | ||||
| -rw-r--r-- | src/input/encode.rs | 7 | ||||
| -rw-r--r-- | src/input/model.rs | 17 | ||||
| -rw-r--r-- | src/protocol/wire.rs | 52 |
4 files changed, 169 insertions, 4 deletions
diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs index 79dde8c3..b877245c 100644 --- a/src/client/input/windows_vti.rs +++ b/src/client/input/windows_vti.rs @@ -2384,6 +2384,103 @@ mod tests { } #[test] + fn vti_us_international_dead_key_only_emits_composed_text() { + fn encode_for_kitty(events: Vec<crate::protocol::ClientInputEvent>, flags: u16) -> Vec<u8> { + events + .into_iter() + .flat_map(|event| match event.to_raw_input_event() { + crate::raw_input::RawInputEvent::Key(key) => crate::input::encode_terminal_key( + key, + crate::input::KeyboardProtocol::Kitty { flags }, + ), + crate::raw_input::RawInputEvent::Text(text) => { + text.as_str().as_bytes().to_vec() + } + _ => panic!("unexpected event while encoding dead-key input"), + }) + .collect() + } + + let dead_press = WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0x36, + virtual_scan_code: 0x07, + unicode: 0, + control_key_state: 0x0030, + }; + let dead_release = WindowsKeyRecord { + key_down: false, + ..dead_press + }; + let composed_press = WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0x45, + virtual_scan_code: 0x12, + unicode: 'ê' as u16, + control_key_state: 0x0020, + }; + let composed_release = WindowsKeyRecord { + key_down: false, + unicode: 'e' as u16, + ..composed_press + }; + + let mut translator = WindowsInputTranslator::default(); + for (record, kind) in [ + (dead_press, crate::protocol::ClientKeyKind::Press), + (dead_release, crate::protocol::ClientKeyKind::Release), + ] { + let events = translator.translate(WindowsInputRecord::Key(record)); + assert!(matches!( + events.as_slice(), + [crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('6'), + modifiers, + kind: actual_kind, + generated_text: None, + source: crate::protocol::ClientKeySource::WindowsConsole { + record: actual_record, + }, + .. + }] if *modifiers == crossterm::event::KeyModifiers::SHIFT.bits() + && *actual_kind == kind + && *actual_record == record + )); + for flags in [1, 31] { + assert!(encode_for_kitty(events.clone(), flags).is_empty()); + } + } + + let events = translator.translate(WindowsInputRecord::Key(composed_press)); + assert!(matches!( + events.as_slice(), + [crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('ê'), + kind: crate::protocol::ClientKeyKind::Press, + source: crate::protocol::ClientKeySource::WindowsConsole { + record: actual_record, + }, + .. + }] if *actual_record == composed_press + )); + assert_eq!(encode_for_kitty(events, 1), "ê".as_bytes()); + + let events = translator.translate(WindowsInputRecord::Key(composed_release)); + assert!(encode_for_kitty(events, 1).is_empty()); + assert!(translator.idle().is_empty()); + + let ordinary_shifted = WindowsKeyRecord { + unicode: '^' as u16, + ..dead_press + }; + let events = + WindowsInputTranslator::default().translate(WindowsInputRecord::Key(ordinary_shifted)); + assert_eq!(encode_for_kitty(events, 1), b"^"); + } + + #[test] fn vti_win32_input_mode_non_us_shifted_text_preserves_generated_text() { let pressed = WindowsKeyRecord { key_down: true, diff --git a/src/input/encode.rs b/src/input/encode.rs index 774ab1a6..6dfff4ce 100644 --- a/src/input/encode.rs +++ b/src/input/encode.rs @@ -16,6 +16,13 @@ pub fn encode_key(key: KeyEvent, protocol: KeyboardProtocol) -> Vec<u8> { } pub fn encode_terminal_key(key: TerminalKey, protocol: KeyboardProtocol) -> Vec<u8> { + // A zero Unicode value on this Windows character event means the host layout is + // still composing a dead key. Kitty panes must not receive its physical fallback. + // Legacy Windows panes take the native ConPTY fallback before reaching this encoder. + if matches!(protocol, KeyboardProtocol::Kitty { .. }) && key.is_windows_shift_dead_key() { + return Vec::new(); + } + // REPORT_ALL_KEYS must retain physical press/repeat/release semantics instead of // reducing a native key to its layout-generated text. let preserve_physical_key = key.has_physical_identity() && protocol.reports_all_keys(); diff --git a/src/input/model.rs b/src/input/model.rs index 304c4f20..50cd7e42 100644 --- a/src/input/model.rs +++ b/src/input/model.rs @@ -78,6 +78,7 @@ pub struct TerminalKey { pub shifted_codepoint: Option<u32>, pub generated_text: Option<String>, physical_identity_hint: bool, + windows_shift_dead_key: bool, source: KeySource, } @@ -91,6 +92,7 @@ impl TerminalKey { shifted_codepoint: None, generated_text: None, physical_identity_hint: false, + windows_shift_dead_key: false, source: KeySource::Synthesized, } } @@ -139,6 +141,7 @@ impl TerminalKey { #[cfg(any(windows, test))] pub fn with_windows_record(mut self, record: WindowsKeyRecord) -> Self { + self = self.with_windows_composition_hint(Some(record)); self.repeat_count = if self.kind == crossterm::event::KeyEventKind::Release { 1 } else { @@ -153,6 +156,16 @@ impl TerminalKey { self } + pub(crate) fn with_windows_composition_hint( + mut self, + record: Option<WindowsKeyRecord>, + ) -> Self { + self.windows_shift_dead_key = matches!(self.code, KeyCode::Char(_)) + && self.modifiers == KeyModifiers::SHIFT + && record.is_some_and(|record| record.unicode == 0); + self + } + pub(crate) fn with_physical_identity_hint(mut self, physical: bool) -> Self { self.physical_identity_hint = physical; self @@ -176,6 +189,10 @@ impl TerminalKey { None } + pub(crate) fn is_windows_shift_dead_key(&self) -> bool { + self.windows_shift_dead_key + } + pub(crate) fn identity(&self) -> KeyIdentity { match self.source { #[cfg(any(windows, test))] diff --git a/src/protocol/wire.rs b/src/protocol/wire.rs index cbca829a..96881b14 100644 --- a/src/protocol/wire.rs +++ b/src/protocol/wire.rs @@ -325,6 +325,13 @@ impl ClientPaneInputEvent { } pub(crate) fn to_raw_input_event(&self) -> crate::raw_input::RawInputEvent { + self.to_raw_input_event_with_windows_source(cfg!(any(windows, test))) + } + + fn to_raw_input_event_with_windows_source( + &self, + attach_windows_source: bool, + ) -> crate::raw_input::RawInputEvent { match self { Self::Key { code, @@ -344,16 +351,19 @@ impl ClientPaneInputEvent { .with_kind(kind.to_crossterm()) .with_repeat_count(*repeat_count) .with_generated_text(generated_text.clone()) - .with_physical_identity_hint(*tracks_release && generated_text.is_some()); + .with_physical_identity_hint(*tracks_release && generated_text.is_some()) + .with_windows_composition_hint(*windows_record); if let Some(shifted_codepoint) = shifted_codepoint { key = key.with_shifted_codepoint(*shifted_codepoint); } #[cfg(any(windows, test))] - if let Some(record) = windows_record { - key = key.with_windows_record(*record); + if attach_windows_source { + if let Some(record) = windows_record { + key = key.with_windows_record(*record); + } } #[cfg(not(any(windows, test)))] - let _ = windows_record; + let _ = (attach_windows_source, windows_record); crate::raw_input::RawInputEvent::Key(key) } Self::TextCommit(text) => { @@ -2032,6 +2042,40 @@ mod tests { } #[test] + fn client_shell_pane_input_reconstructs_windows_dead_key_without_native_source() { + let event = ClientPaneInputEvent::Key { + code: ClientKeyCode::Char('6'), + modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), + kind: ClientKeyKind::Press, + repeat_count: 1, + shifted_codepoint: None, + generated_text: None, + tracks_release: true, + physical_key_id: Some(0x07), + windows_record: Some(crate::input::WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0x36, + virtual_scan_code: 0x07, + unicode: 0, + control_key_state: 0x0030, + }), + }; + let crate::raw_input::RawInputEvent::Key(key) = + event.to_raw_input_event_with_windows_source(false) + else { + panic!("pane dead key should remain a key"); + }; + assert!(key.is_windows_shift_dead_key()); + assert_eq!(key.windows_record(), None); + assert!(crate::input::encode_terminal_key( + key, + crate::input::KeyboardProtocol::Kitty { flags: 1 }, + ) + .is_empty()); + } + + #[test] fn client_shell_key_roundtrip_preserves_physical_generated_text_encoding() { let key = crate::input::TerminalKey::new( crossterm::event::KeyCode::Char('/'), |
