aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-23 00:01:57 +0300
committerGitHub <noreply@github.com>2026-08-23 00:01:57 +0300
commit1d604329b0ded93d18fd6b27f72c7473d1fee20f (patch)
tree2ac419b930b57c232b3e7bc8921beebfa1b997bb /src/client
parentc714e4c20e3272c21722a6f083fc2d592c244385 (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.rs201
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()