From 4e624cd50e26f1284fed6e89f557479c235fcb8d Mon Sep 17 00:00:00 2001 From: JJ Liebig Date: Wed, 7 Oct 2026 16:48:40 +0200 Subject: [PATCH] fix: preserve windows accent-key shortcuts (#5021) --- src/client/input/windows_vti.rs | 29 ++++++++++++++++++--- src/client/shell/tests/input_conformance.rs | 13 ++++++--- src/platform/windows.rs | 20 +++++++++++--- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs index 70967f817..56ae7bc47 100644 --- a/src/client/input/windows_vti.rs +++ b/src/client/input/windows_vti.rs @@ -791,11 +791,26 @@ pub(crate) struct TestWindowsInput(WindowsInputTranslator); #[cfg(all(test, windows))] impl TestWindowsInput { - pub(crate) fn key( + pub(crate) fn key_in_layout( &mut self, record: crate::input::WindowsKeyRecord, + layout: windows_sys::Win32::UI::Input::KeyboardAndMouse::HKL, ) -> Vec { - self.0.translate(WindowsInputRecord::Key(record)) + let oem_char = (is_oem_virtual_key(record.virtual_key_code) && record.unicode == 0) + .then(|| { + crate::platform::resolve_base_printable_key_in_layout( + record.virtual_key_code, + record.virtual_scan_code, + layout, + ) + }) + .flatten(); + self.0 + .mapper + .translate_key_with_oem_char(record, oem_char) + .into_iter() + .flat_map(|item| self.0.pump.process(item)) + .collect() } pub(crate) fn idle(&mut self) -> Vec { @@ -869,6 +884,14 @@ impl WindowsInputMapper { } fn translate_key(&mut self, key: WindowsKeyRecord) -> Vec { + self.translate_key_with_oem_char(key, resolve_ctrl_oem_char(key)) + } + + fn translate_key_with_oem_char( + &mut self, + key: WindowsKeyRecord, + oem_char: Option, + ) -> Vec { if !self.key_record_can_emit_event(key) { return Vec::new(); } @@ -905,7 +928,7 @@ impl WindowsInputMapper { }; } - let events = self.translate_semantic_key_events(key, resolve_ctrl_oem_char(key)); + let events = self.translate_semantic_key_events(key, oem_char); let items = if let Some(bytes) = self.paste_payload_bytes_for_key(key) { vec![PlatformInputItem::PasteAwareKey { win32_paste_bytes: bytes.clone(), diff --git a/src/client/shell/tests/input_conformance.rs b/src/client/shell/tests/input_conformance.rs index a3382e1b0..b44d5c122 100644 --- a/src/client/shell/tests/input_conformance.rs +++ b/src/client/shell/tests/input_conformance.rs @@ -1464,7 +1464,7 @@ mod windows_records { } /// The press and release records Windows produces for `vk` under `chord`, - /// or None for dead keys and multi-unit output. + /// or None for multi-unit output. fn key_records( layout: HKL, vk: u16, @@ -1515,6 +1515,8 @@ mod windows_records { let unicode = match written { 0 => 0, 1 => buf[0], + // A dead key has not committed a character yet. + n if n < 0 => 0, _ => return None, }; let press = crate::input::WindowsKeyRecord { @@ -1551,8 +1553,11 @@ mod windows_records { herdr: &mut HerdrPath, input: &mut crate::client::input::windows_vti::TestWindowsInput, record: crate::input::WindowsKeyRecord, + layout: HKL, ) -> Option> { - let mut events = input.key(record); + // The generated record belongs to this layout, independently of the + // foreground window's layout while the test runs. + let mut events = input.key_in_layout(record, layout); events.extend(input.idle()); let outcome = herdr.state.handle_client_events(&events); herdr.deliver(vec![outcome]) @@ -1584,8 +1589,8 @@ mod windows_records { continue; }; let expected = [win32_input_mode(press), win32_input_mode(release)].concat(); - let pressed = deliver_record(&mut herdr, &mut input, press); - let released = deliver_record(&mut herdr, &mut input, release); + let pressed = deliver_record(&mut herdr, &mut input, press, layout); + let released = deliver_record(&mut herdr, &mut input, release, layout); let page_key_scrolls_herdr = chord.name == "plain" && matches!(key_name.as_str(), "pageup" | "pagedown") && herdr.runtime.plain_page_keys_use_host_scrollback() == Some(true); diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 4ddbf09fe..441a7337e 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -644,11 +644,21 @@ fn standard_windows_path(path: &std::path::Path) -> Option { /// Resolves against the current foreground layout because asynchronous console /// records do not retain the layout that was active when the key was pressed. pub(crate) fn resolve_base_printable_key(vk: u16, scan: u16) -> Option { + // SAFETY: the foreground window and its layout are owned by Win32. + let layout = unsafe { + let thread_id = GetWindowThreadProcessId(GetForegroundWindow(), null_mut()); + GetKeyboardLayout(thread_id) + }; + resolve_base_printable_key_in_layout(vk, scan, layout) +} + +pub(crate) fn resolve_base_printable_key_in_layout( + vk: u16, + scan: u16, + layout: windows_sys::Win32::UI::Input::KeyboardAndMouse::HKL, +) -> Option { // SAFETY: Win32 owns the handles; the fixed buffers match the API lengths. unsafe { - let thread_id = GetWindowThreadProcessId(GetForegroundWindow(), null_mut()); - let layout = GetKeyboardLayout(thread_id); - let key_state = [0u8; 256]; let mut output = [0u16; 2]; let written = ToUnicodeEx( @@ -660,7 +670,9 @@ pub(crate) fn resolve_base_printable_key(vk: u16, scan: u16) -> Option { 0x4, layout, ); - let units = output.get(..usize::try_from(written).ok()?)?; + // A negative result identifies a dead key; its spacing accent is still + // the key's identity. Flag 0x4 above keeps composition state unchanged. + let units = output.get(..usize::try_from(written.unsigned_abs()).ok()?)?; let mut chars = char::decode_utf16(units.iter().copied()); let ch = chars.next()?.ok()?; (chars.next().is_none() && !ch.is_control()).then_some(ch)