diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-08-23 00:01:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-23 00:01:57 +0300 |
| commit | 1d604329b0ded93d18fd6b27f72c7473d1fee20f (patch) | |
| tree | 2ac419b930b57c232b3e7bc8921beebfa1b997bb /src/client | |
| parent | c714e4c20e3272c21722a6f083fc2d592c244385 (diff) | |
fix(windows): preserve non-us shifted text (#3055)
* fix(windows): preserve non-us shifted text
refs #3045
* fix(windows): retain shifted key repeat lifecycle
refs #3045
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/input/windows_vti.rs | 201 |
1 files changed, 193 insertions, 8 deletions
diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs index dcf804c1..adfc9196 100644 --- a/src/client/input/windows_vti.rs +++ b/src/client/input/windows_vti.rs @@ -587,15 +587,25 @@ impl WindowsInputMapper { code, modifiers, kind, + generated_text, .. - } => crate::protocol::ClientInputEvent::Key { - code, - modifiers, - kind, - repeat_count: key.repeat_count.max(1), - generated_text: None, - source: crate::protocol::ClientKeySource::WindowsConsole { record: key }, - }, + } => { + // Windows Unicode is the authoritative host-layout result. Plain and + // AltGr characters already encode from `code`; Shift must remain on the + // physical event, so carry its produced text separately. + let shifted_text = key.key_down + && modifiers == crossterm::event::KeyModifiers::SHIFT.bits(); + crate::protocol::ClientInputEvent::Key { + code, + modifiers, + kind, + repeat_count: key.repeat_count.max(1), + generated_text: if shifted_text { generated_text } else { None }, + source: crate::protocol::ClientKeySource::WindowsConsole { + record: key, + }, + } + } event => event, }) .into_iter() @@ -2029,6 +2039,181 @@ mod tests { } #[test] + fn vti_win32_input_mode_non_us_shifted_text_preserves_generated_text() { + let pressed = WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0x37, + virtual_scan_code: 0x08, + unicode: b'/'.into(), + control_key_state: 0x0010, + }; + let released = WindowsKeyRecord { + key_down: false, + ..pressed + }; + let mut records = win32_input_mode_encoded_record(pressed); + records.extend(win32_input_mode_encoded_record(released)); + + let events = translate_with_provenance(records); + assert_eq!( + events, + vec![ + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('/'), + modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), + kind: crate::protocol::ClientKeyKind::Press, + repeat_count: 1, + generated_text: Some("/".into()), + source: crate::protocol::ClientKeySource::WindowsConsole { record: pressed }, + }, + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('/'), + modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), + kind: crate::protocol::ClientKeyKind::Release, + repeat_count: 1, + generated_text: None, + source: crate::protocol::ClientKeySource::WindowsConsole { record: released }, + }, + ] + ); + + let crate::raw_input::RawInputEvent::Key(key) = events[0].to_raw_input_event() else { + panic!("expected translated key"); + }; + assert_eq!( + crate::input::encode_terminal_key(key.clone(), crate::input::KeyboardProtocol::Legacy), + b"/" + ); + assert_eq!( + crate::input::encode_terminal_key( + key.clone(), + crate::input::KeyboardProtocol::Kitty { flags: 7 }, + ), + b"/" + ); + assert_eq!( + crate::input::encode_terminal_key( + key, + crate::input::KeyboardProtocol::Kitty { flags: 15 }, + ), + b"\x1b[47;2:1u" + ); + } + + #[test] + fn vti_native_layout_text_and_command_modifiers_encode_by_role() { + let cases = [ + ( + "plain unicode", + 0x4c, + 0x26, + 'λ' as u16, + 0, + 0, + None, + "λ".as_bytes(), + ), + ( + "shifted latin", + 0x41, + 0x1e, + 'A' as u16, + 0x0010, + crossterm::event::KeyModifiers::SHIFT.bits(), + Some("A"), + b"A".as_slice(), + ), + ( + "shifted non-ascii", + 0x4c, + 0x26, + 'Λ' as u16, + 0x0010, + crossterm::event::KeyModifiers::SHIFT.bits(), + Some("Λ"), + "Λ".as_bytes(), + ), + ( + "altgr", + 0x45, + 0x12, + '€' as u16, + 0x0009, + 0, + None, + "€".as_bytes(), + ), + ( + "shift altgr", + 0x37, + 0x08, + '{' as u16, + 0x0019, + crossterm::event::KeyModifiers::SHIFT.bits(), + Some("{"), + b"{".as_slice(), + ), + ( + "left alt command", + 0x41, + 0x1e, + 'a' as u16, + 0x0002, + crossterm::event::KeyModifiers::ALT.bits(), + None, + b"\x1ba".as_slice(), + ), + ( + "control command", + 0x41, + 0x1e, + 0x01, + 0x0008, + crossterm::event::KeyModifiers::CONTROL.bits(), + None, + b"\x01".as_slice(), + ), + ]; + + for ( + name, + virtual_key_code, + virtual_scan_code, + unicode, + state, + modifiers, + text, + expected, + ) in cases + { + let events = + translate_with_provenance(win32_input_mode_encoded_record(WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code, + virtual_scan_code, + unicode, + control_key_state: state, + })); + let [event] = events.as_slice() else { + panic!("{name}: expected one translated event, got {events:?}"); + }; + let crate::raw_input::RawInputEvent::Key(key) = event.to_raw_input_event() else { + panic!("{name}: expected translated key"); + }; + + assert_eq!(key.modifiers.bits(), modifiers, "{name}: modifiers"); + assert_eq!(key.generated_text.as_deref(), text, "{name}: text"); + assert_eq!( + crate::input::encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy), + expected, + "{name}: encoding" + ); + } + } + + #[test] fn vti_win32_input_mode_sequence_inside_bracketed_paste_stays_payload() { let records = "\x1b[200~alpha\x1b[13;28;13;1;16;1_bravo\x1b[201~" .chars() |
