aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/input/windows_vti.rs97
-rw-r--r--src/input/encode.rs7
-rw-r--r--src/input/model.rs17
-rw-r--r--src/protocol/wire.rs52
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('/'),