diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-08-01 04:27:50 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-01 04:27:50 +0300 |
| commit | b76adc155d979705a353fd6dc87c702f9dfc6c22 (patch) | |
| tree | f613254e0cfbfd2b4450a6a5f3ee7c0ba8136976 /src/client | |
| parent | 8d190eddda2c74afa3dc043f8ec8020ffd7b5a94 (diff) | |
fix(input): preserve native key lifecycle across routing (#2142)
refs #2077
Co-authored-by: Jonathan Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/input.rs | 54 | ||||
| -rw-r--r-- | src/client/input/windows_vti.rs | 503 | ||||
| -rw-r--r-- | src/client/mod.rs | 2 |
3 files changed, 475 insertions, 84 deletions
diff --git a/src/client/input.rs b/src/client/input.rs index 28d38bda..09e0eb92 100644 --- a/src/client/input.rs +++ b/src/client/input.rs @@ -281,7 +281,16 @@ fn windows_crossterm_input_event( code: crate::protocol::ClientKeyCode::Char(codepoint), modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, - } => Some(crate::protocol::ClientInputEvent::Text { codepoint }), + source, + .. + } => Some(crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char(codepoint), + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 1, + generated_text: Some(codepoint.to_string()), + source, + }), event => Some(event), } } @@ -368,20 +377,29 @@ fn windows_client_input_event_from_raw( event: crate::raw_input::RawInputEvent, ) -> Option<crate::protocol::ClientInputEvent> { match event { - crate::raw_input::RawInputEvent::Key(key) if key.is_text_commit => { - let crossterm::event::KeyCode::Char(codepoint) = key.code else { - return None; - }; - Some(crate::protocol::ClientInputEvent::Text { codepoint }) - } + crate::raw_input::RawInputEvent::Text(text) => Some( + crate::protocol::ClientInputEvent::TextCommit(text.into_string()), + ), crate::raw_input::RawInputEvent::Key(key) => { let code = crate::protocol::ClientKeyCode::from_crossterm(key.code)?; let modifiers = key.modifiers.bits(); let kind = crate::protocol::ClientKeyKind::from_crossterm(key.kind); + let source = if let Some(bytes) = key.vt_bytes() { + crate::protocol::ClientKeySource::Vt { + bytes: bytes.to_vec(), + } + } else if let Some(record) = key.windows_record() { + crate::protocol::ClientKeySource::WindowsConsole { record } + } else { + crate::protocol::ClientKeySource::Synthesized + }; Some(crate::protocol::ClientInputEvent::Key { code, modifiers, kind, + repeat_count: key.repeat_count, + generated_text: key.generated_text.clone(), + source, }) } crate::raw_input::RawInputEvent::Mouse(mouse) => { @@ -573,12 +591,19 @@ mod windows_tests { } #[test] - fn windows_crossterm_printable_press_is_text() { + fn windows_crossterm_printable_press_keeps_key_semantics_and_text() { let event = Event::Key(KeyEvent::new(KeyCode::Char('你'), KeyModifiers::empty())); assert_eq!( windows_crossterm_input_event(event), - Some(crate::protocol::ClientInputEvent::Text { codepoint: '你' }) + Some(crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('你'), + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 1, + generated_text: Some("你".to_string()), + source: crate::protocol::ClientKeySource::Synthesized, + }) ); } @@ -673,6 +698,9 @@ mod windows_tests { code: crate::protocol::ClientKeyCode::Char('d'), modifiers: KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Vt { bytes: vec![4] }, } ); } @@ -693,6 +721,11 @@ mod windows_tests { code: crate::protocol::ClientKeyCode::Up, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Vt { + bytes: b"\x1b[A".to_vec() + }, } ); } @@ -712,6 +745,9 @@ mod windows_tests { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Vt { bytes: vec![0x1b] }, } ); } diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs index 8966112e..c95cbc43 100644 --- a/src/client/input/windows_vti.rs +++ b/src/client/input/windows_vti.rs @@ -9,6 +9,7 @@ use tokio::sync::mpsc; use super::windows_client_input_event_from_raw; #[cfg(windows)] use super::ClientLoopEvent; +use crate::input::WindowsKeyRecord; #[cfg(windows)] pub(super) fn raw_console_reader_loop( @@ -165,16 +166,6 @@ enum WindowsInputRecord { } #[derive(Clone, Copy, Debug)] -struct WindowsKeyRecord { - key_down: bool, - repeat_count: u16, - virtual_key_code: u16, - virtual_scan_code: u16, - unicode: u16, - control_key_state: u32, -} - -#[derive(Clone, Copy, Debug)] struct WindowsMouseRecord { x: u16, y: u16, @@ -502,6 +493,10 @@ impl WindowsInputMapper { return Some(vec![0x1b]); } + if record.virtual_scan_code != 0 { + return None; + } + if !record.key_down || record.repeat_count.max(1) != 1 || windows_key_modifiers(record.control_key_state).bits() != 0 @@ -550,6 +545,10 @@ impl WindowsInputMapper { code, modifiers: modifiers.bits(), kind, + repeat_count: 1, + + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, )) } @@ -574,6 +573,35 @@ impl WindowsInputMapper { } let is_alt_code = Self::is_alt_code(key); + let carries_native_record = key.repeat_count != 0 + && key.virtual_key_code != 0 + && key.virtual_key_code != 0x03 + && !matches!(key.virtual_key_code, 0xe5 | 0xe7) + && !is_alt_code + && !(0xd800..=0xdfff).contains(&key.unicode); + if carries_native_record { + return self + .translate_semantic_key_event(key, Self::semantic_key_kind(key, is_alt_code, 0)) + .map(|event| match event { + crate::protocol::ClientInputEvent::Key { + code, + modifiers, + kind, + .. + } => crate::protocol::ClientInputEvent::Key { + code, + modifiers, + kind, + repeat_count: key.repeat_count.max(1), + generated_text: None, + source: crate::protocol::ClientKeySource::WindowsConsole { record: key }, + }, + event => event, + }) + .into_iter() + .collect(); + } + (0..key.repeat_count.max(1)) .filter_map(|repeat_idx| { self.translate_semantic_key_event( @@ -625,7 +653,9 @@ impl WindowsInputMapper { if key.virtual_key_code == 0 { let codepoint = self.utf16_unit_to_char(key.unicode)?; if !codepoint.is_control() { - return Some(crate::protocol::ClientInputEvent::Text { codepoint }); + return Some(crate::protocol::ClientInputEvent::TextCommit( + codepoint.to_string(), + )); } } if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) @@ -637,6 +667,10 @@ impl WindowsInputMapper { code: crate::protocol::ClientKeyCode::Char('j'), modifiers: modifiers.bits(), kind, + repeat_count: 1, + + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }); } @@ -656,6 +690,10 @@ impl WindowsInputMapper { code, modifiers: modifiers.bits(), kind, + repeat_count: 1, + + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }); } } @@ -667,10 +705,20 @@ impl WindowsInputMapper { }) }; - code.map(|code| crate::protocol::ClientInputEvent::Key { - code, - modifiers: modifiers.bits(), - kind, + code.map(|code| { + let generated_text = matches!(code, crate::protocol::ClientKeyCode::Char(_)) + .then(|| char::from_u32(key.unicode as u32)) + .flatten() + .filter(|ch| !ch.is_control()) + .map(|ch| ch.to_string()); + crate::protocol::ClientInputEvent::Key { + code, + modifiers: modifiers.bits(), + kind, + repeat_count: 1, + generated_text, + source: crate::protocol::ClientKeySource::Synthesized, + } }) } @@ -1063,7 +1111,7 @@ mod tests { key_down: true, repeat_count, virtual_key_code: vk, - virtual_scan_code: 0, + virtual_scan_code: 1, unicode: 0, control_key_state: 0, }) @@ -1115,6 +1163,38 @@ mod tests { fn translate( records: impl IntoIterator<Item = WindowsInputRecord>, ) -> Vec<crate::protocol::ClientInputEvent> { + semantic_only(translate_with_provenance(records)) + } + + fn semantic_only( + events: impl IntoIterator<Item = crate::protocol::ClientInputEvent>, + ) -> Vec<crate::protocol::ClientInputEvent> { + events + .into_iter() + .map(|event| match event { + crate::protocol::ClientInputEvent::Key { + code, + modifiers, + kind, + repeat_count, + generated_text, + .. + } => crate::protocol::ClientInputEvent::Key { + code, + modifiers, + kind, + repeat_count, + generated_text, + source: crate::protocol::ClientKeySource::Synthesized, + }, + event => event, + }) + .collect() + } + + fn translate_with_provenance( + records: impl IntoIterator<Item = WindowsInputRecord>, + ) -> Vec<crate::protocol::ClientInputEvent> { let mut translator = WindowsInputTranslator::default(); records .into_iter() @@ -1237,10 +1317,20 @@ mod tests { control_key_state, }); - assert_eq!( - translate(records), - vec![crate::protocol::ClientInputEvent::Text { codepoint: '你' }] - ); + let events = translate(records); + match events.as_slice() { + [crate::protocol::ClientInputEvent::TextCommit(text)] => { + assert_eq!(text, "你"); + } + [crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('你'), + repeat_count: 1, + generated_text: Some(text), + source: crate::protocol::ClientKeySource::Synthesized, + .. + }] => assert_eq!(text, "你"), + other => panic!("unexpected VK=0 result: {other:?}"), + } } } @@ -1292,6 +1382,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Char('c'), modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }] ); } @@ -1304,6 +1398,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Char('j'), modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }] ); } @@ -1344,6 +1442,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Char(expected), modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }], "vk={vk:#x} unicode={unicode:#x}" ); @@ -1362,6 +1464,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Vt { bytes: vec![0x1b] }, }] ); } @@ -1378,48 +1484,87 @@ mod tests { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Vt { bytes: vec![0x1b] }, }] ); } #[test] fn vti_physical_escape_key_record_is_immediately_semantic() { - let mut translator = WindowsInputTranslator::default(); + let record = WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0x1b, + virtual_scan_code: 0x01, + unicode: 0x1b, + control_key_state: 0, + }; assert_eq!( - translator.translate(key_vk_with_scan_unicode(0x1b, 0x01, '\x1b', 0)), + translate_with_provenance([WindowsInputRecord::Key(record)]), vec![crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::WindowsConsole { record }, }] ); } #[test] - fn vti_repeated_escape_record_stays_semantic() { + fn vti_grouped_escape_down_and_up_keep_native_ownership_record() { + use crate::protocol::ClientKeyKind::{Press, Release}; + + let events = translate_with_provenance([ + key_vk_with_repeat(0x1b, 3), + key_vk_up_with_scan_unicode(0x1b, 1, '\0', 0), + ]); + assert_eq!( - translate([key_vk_with_repeat(0x1b, 3)]), - vec![ - crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::Esc, - modifiers: 0, - kind: crate::protocol::ClientKeyKind::Press, - }, - crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::Esc, - modifiers: 0, - kind: crate::protocol::ClientKeyKind::Repeat, - }, - crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::Esc, - modifiers: 0, - kind: crate::protocol::ClientKeyKind::Repeat, - }, - ] + events + .iter() + .map(|event| match event { + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Esc, + kind, + repeat_count, + source: crate::protocol::ClientKeySource::WindowsConsole { record }, + .. + } => (*kind, *repeat_count, record.key_down, record.repeat_count), + event => panic!("unexpected grouped event: {event:?}"), + }) + .collect::<Vec<_>>(), + [(Press, 3, true, 3), (Release, 1, false, 1)] ); } #[test] + fn vti_ctrl_break_record_keeps_semantic_path() { + assert!(matches!( + translate_with_provenance([key_vk(0x03, 0x0008)]).as_slice(), + [crate::protocol::ClientInputEvent::Key { .. }] + )); + } + + #[test] + fn vti_ime_virtual_keys_keep_semantic_path() { + for vk in [0xe5, 0xe7] { + assert!(matches!( + translate_with_provenance([key_vk_with_unicode(vk, 'é', 0)]).as_slice(), + [crate::protocol::ClientInputEvent::Key { + generated_text: Some(text), + source: crate::protocol::ClientKeySource::Synthesized, + .. + }] if text == "é" + )); + } + } + + #[test] fn vti_modified_escape_remains_semantic() { assert_eq!( translate([key_vk_with_utf16_mods(0x1b, 0, 0x0010)]), @@ -1427,6 +1572,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Esc, modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }] ); } @@ -1445,6 +1594,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Enter, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }] ); } @@ -1453,23 +1606,14 @@ mod tests { fn vti_repeated_virtual_key_records_emit_repeats() { assert_eq!( translate([key_vk_with_repeat(0x08, 3)]), - vec![ - crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::Backspace, - modifiers: 0, - kind: crate::protocol::ClientKeyKind::Press, - }, - crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::Backspace, - modifiers: 0, - kind: crate::protocol::ClientKeyKind::Repeat, - }, - crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::Backspace, - modifiers: 0, - kind: crate::protocol::ClientKeyKind::Repeat, - }, - ] + vec![crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Backspace, + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 3, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, + }] ); } @@ -1481,6 +1625,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Enter, modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }] ); } @@ -1493,6 +1641,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Char('j'), modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Release, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }] ); } @@ -1505,6 +1657,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Enter, modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }] ); } @@ -1518,16 +1674,28 @@ mod tests { code: crate::protocol::ClientKeyCode::Enter, modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Enter, modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), kind: crate::protocol::ClientKeyKind::Repeat, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Enter, modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), kind: crate::protocol::ClientKeyKind::Repeat, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, ] ); @@ -1589,11 +1757,19 @@ mod tests { code: crate::protocol::ClientKeyCode::Enter, modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Enter, modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), kind: crate::protocol::ClientKeyKind::Release, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, ] ); @@ -1612,11 +1788,19 @@ mod tests { code: crate::protocol::ClientKeyCode::Enter, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Enter, modifiers: 0, kind: crate::protocol::ClientKeyKind::Release, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, ] ); @@ -1633,11 +1817,19 @@ mod tests { code: crate::protocol::ClientKeyCode::Backspace, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Backspace, modifiers: 0, kind: crate::protocol::ClientKeyKind::Release, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, ] ); @@ -1656,11 +1848,19 @@ mod tests { code: crate::protocol::ClientKeyCode::Char('j'), modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Char('j'), modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Release, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, ] ); @@ -1678,11 +1878,19 @@ mod tests { code: crate::protocol::ClientKeyCode::Char('j'), modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Char('j'), modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Release, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, ] ); @@ -1700,33 +1908,121 @@ mod tests { code: crate::protocol::ClientKeyCode::Enter, modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Enter, modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Release, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, ] ); } #[test] - fn vti_win32_input_mode_printable_key_becomes_char() { - let records = "\x1b[65;30;97;1;0;1_\x1b[65;30;97;0;0;1_" - .chars() - .map(key_char); + fn vti_special_key_does_not_emit_incidental_printable_text() { + assert_eq!( + translate_with_provenance([WindowsInputRecord::Key(WindowsKeyRecord { + key_down: true, + repeat_count: 0, + virtual_key_code: 0x0d, + virtual_scan_code: 0, + unicode: b'a'.into(), + control_key_state: 0, + })]), + vec![crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Enter, + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, + }] + ); + } + #[test] + fn vti_win32_input_mode_printable_keys_preserve_physical_records() { assert_eq!( - translate(records), - vec![ - crate::protocol::ClientInputEvent::Text { codepoint: 'a' }, - crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::Char('a'), - modifiers: 0, - kind: crate::protocol::ClientKeyKind::Release, - }, - ] + translate(win32_input_mode_encoded_record(WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0, + virtual_scan_code: 0, + unicode: b'a'.into(), + control_key_state: 0, + })), + vec![crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('a'), + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 1, + generated_text: Some("a".into()), + source: crate::protocol::ClientKeySource::Synthesized, + }] ); + + use crate::protocol::ClientKeyKind::{Press, Release}; + for repeat_count in [1, 3] { + let mut records = win32_input_mode_encoded_record(WindowsKeyRecord { + key_down: true, + repeat_count, + virtual_key_code: 0x41, + virtual_scan_code: 30, + unicode: b'a'.into(), + control_key_state: 0, + }); + records.extend(win32_input_mode_encoded_record(WindowsKeyRecord { + key_down: false, + repeat_count: 1, + virtual_key_code: 0x41, + virtual_scan_code: 30, + unicode: b'a'.into(), + control_key_state: 0, + })); + + assert_eq!( + translate_with_provenance(records) + .iter() + .map(|event| match event { + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('a'), + modifiers: 0, + kind, + repeat_count: event_repeat_count, + generated_text: None, + source: + crate::protocol::ClientKeySource::WindowsConsole { + record: + WindowsKeyRecord { + repeat_count: record_repeat_count, + virtual_key_code: 0x41, + virtual_scan_code: 30, + unicode, + control_key_state: 0, + key_down, + }, + }, + } if *unicode == u16::from(b'a') => { + (*kind, *event_repeat_count, *key_down, *record_repeat_count) + } + event => panic!("unexpected printable key event: {event:?}"), + }) + .collect::<Vec<_>>(), + [ + (Press, repeat_count, true, repeat_count), + (Release, 1, false, 1) + ], + "repeat_count={repeat_count}" + ); + } } #[test] @@ -1817,20 +2113,33 @@ mod tests { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::WindowsConsole { + record: WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0x1b, + virtual_scan_code: 0x02, + unicode: 0, + control_key_state: 0, + } + }, }] ); } #[test] fn vti_win32_input_mode_physical_escape_is_immediately_semantic() { - let records = win32_input_mode_encoded_record(WindowsKeyRecord { + let record = WindowsKeyRecord { key_down: true, repeat_count: 1, virtual_key_code: 0x1b, virtual_scan_code: 0x01, unicode: 0x1b, control_key_state: 0, - }); + }; + let records = win32_input_mode_encoded_record(record); let mut translator = WindowsInputTranslator::default(); assert_eq!( @@ -1842,6 +2151,9 @@ mod tests { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::WindowsConsole { record }, }] ); } @@ -1868,6 +2180,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Up, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }] ); } @@ -1882,6 +2198,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Vt { bytes: vec![0x1b] }, }] ); } @@ -1897,11 +2217,19 @@ mod tests { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Vt { bytes: vec![0x1b] }, }, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Enter, modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, ] ); @@ -1912,17 +2240,25 @@ mod tests { let mut translator = WindowsInputTranslator::default(); assert!(translator.translate(key_char('\x1b')).is_empty()); assert_eq!( - translator.translate(key_vk(0x26, 0)), + semantic_only(translator.translate(key_vk(0x26, 0))), vec![ crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Up, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }, ] ); @@ -1936,6 +2272,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Char('c'), modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }] ); } @@ -1948,6 +2288,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Up, modifiers: crossterm::event::KeyModifiers::ALT.bits(), kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }] ); } @@ -1960,6 +2304,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Char('@'), modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }] ); } @@ -1972,6 +2320,9 @@ mod tests { code: crate::protocol::ClientKeyCode::Char('é'), modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 1, + generated_text: Some("é".into()), + source: crate::protocol::ClientKeySource::Synthesized, }] ); } @@ -1989,6 +2340,10 @@ mod tests { code: crate::protocol::ClientKeyCode::Char('🙂'), modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::Synthesized, }] ); } diff --git a/src/client/mod.rs b/src/client/mod.rs index 52fd6dd6..d7fcdf71 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1841,7 +1841,7 @@ fn should_bridge_clipboard_image_paste( events.as_slice(), [crate::raw_input::RawInputEvent::Key(key)] if key.kind == crossterm::event::KeyEventKind::Press - && crate::config::terminal_key_matches_combo(*key, remote_image_paste_key) + && crate::config::terminal_key_matches_combo(key, remote_image_paste_key) ) } |
