mirror of
https://github.com/warpdotdev/warp.git
synced 2026-05-06 15:22:21 +08:00
Fix chord shortcuts on Windows non-Latin keyboard layouts (#9476)
Fixes #9036. ## Description On Windows, non-Latin keyboard layouts (Cyrillic, Greek, Arabic, etc.) translate the physical key to a non-ASCII character even when Ctrl/Cmd is held. That makes bindings like `ctrl-c` / `ctrl-v` (and the terminal's copy/paste shortcuts) fail to match — users have to switch their system layout to English just to copy or paste in Warp. This change detects that situation in `convert_keyboard_input_event` and substitutes the US-QWERTY character that the physical key would produce, so chord shortcuts work regardless of the active OS layout. Same approach used by VS Code, JetBrains, and Chromium. The fallback is gated on `cfg(windows)` and on Ctrl/Super being held with a non-ASCII character — typed text on non-Latin layouts is unaffected. ## Testing Unit tests in `key_events_tests.rs` cover the new `us_qwerty_fallback_for_chord` helper: - Letter mappings (`KeyA` → `a`, etc.) - Digit and punctuation mappings - Returns `None` for keys outside the chord-shortcut set (function keys, modifiers, navigation), so the original `logical_key` is preserved - Returns `None` for `PhysicalKey::Unidentified` Manually verified on Windows: - [x] Copy / Select All / Paste with English (US) layout - [x] Copy / Select All / Paste with Russian layout - [x] Copy / Select All / Paste with German layout - [x] AltGr (Right Alt) bindings with German layout ## Server API dependencies No server API changes. ## Agent Mode - [ ] Warp Agent Mode - This PR was created via Warp's AI Agent Mode CHANGELOG-BUG-FIX: Fixed Ctrl/Cmd shortcuts (e.g. copy, paste) failing on Windows when a non-Latin keyboard layout was active.
This commit is contained in:
@@ -101,6 +101,25 @@ pub fn convert_keyboard_input_event(
|
||||
{
|
||||
input.key_without_modifiers()
|
||||
}
|
||||
// On Windows, non-Latin keyboard layouts (Cyrillic, Greek, Arabic, etc.) translate
|
||||
// the physical key to a non-ASCII character even when Ctrl/Cmd is held. That makes
|
||||
// bindings like `ctrl-c` / `ctrl-v` fail to match. Fall back to the US-QWERTY
|
||||
// position so chord shortcuts work regardless of the active layout — same approach
|
||||
// used by VS Code, JetBrains, and Chromium. Issue #9036.
|
||||
//
|
||||
// Right-Alt is excluded because Windows reports AltGr as Ctrl+Alt; without this
|
||||
// guard, AltGr-produced characters (e.g. `€` on a German layout) would be rewritten
|
||||
// into a spurious chord and the typed character would be swallowed.
|
||||
#[cfg(windows)]
|
||||
Key::Character(c)
|
||||
if (window_state.modifiers.control_key() || window_state.modifiers.super_key())
|
||||
&& !window_state.right_alt_pressed
|
||||
&& !c.is_ascii() =>
|
||||
{
|
||||
us_qwerty_fallback_for_chord(&input.physical_key, shift)
|
||||
.map(|s| Key::Character(s.into()))
|
||||
.unwrap_or_else(|| input.logical_key.clone())
|
||||
}
|
||||
_ => input.logical_key,
|
||||
};
|
||||
let input_key = get_input_key(&logical_key, shift);
|
||||
@@ -253,6 +272,100 @@ fn convert_key(key: Key) -> Option<Cow<'static, str>> {
|
||||
Some(Cow::Borrowed(value))
|
||||
}
|
||||
|
||||
/// Maps a winit `PhysicalKey` to the US-QWERTY character it would produce. Used on Windows
|
||||
/// to recover layout-independent chord shortcuts (e.g. `ctrl-c`) when a non-Latin keyboard
|
||||
/// layout has translated the logical key to a non-ASCII character.
|
||||
///
|
||||
/// Returns `None` for keys outside the standard letter/digit/punctuation set (function keys,
|
||||
/// modifiers, navigation keys, etc.), since those either aren't typically used in chord
|
||||
/// bindings as character keys or are already handled via `NamedKey` in `convert_key`.
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
fn us_qwerty_fallback_for_chord(
|
||||
physical_key: &winit::keyboard::PhysicalKey,
|
||||
shift: bool,
|
||||
) -> Option<&'static str> {
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
let PhysicalKey::Code(code) = physical_key else {
|
||||
return None;
|
||||
};
|
||||
// Letters always return lowercase here; `get_input_key` applies the uppercase
|
||||
// transform downstream when shift is held. Digit/punctuation keys must return
|
||||
// the shifted US-QWERTY symbol up front because `get_input_key` passes
|
||||
// non-letter characters through unchanged, so bindings like `ctrl-shift-}`
|
||||
// would otherwise see `ctrl-shift-]` on non-Latin layouts.
|
||||
Some(match (code, shift) {
|
||||
(KeyCode::KeyA, _) => "a",
|
||||
(KeyCode::KeyB, _) => "b",
|
||||
(KeyCode::KeyC, _) => "c",
|
||||
(KeyCode::KeyD, _) => "d",
|
||||
(KeyCode::KeyE, _) => "e",
|
||||
(KeyCode::KeyF, _) => "f",
|
||||
(KeyCode::KeyG, _) => "g",
|
||||
(KeyCode::KeyH, _) => "h",
|
||||
(KeyCode::KeyI, _) => "i",
|
||||
(KeyCode::KeyJ, _) => "j",
|
||||
(KeyCode::KeyK, _) => "k",
|
||||
(KeyCode::KeyL, _) => "l",
|
||||
(KeyCode::KeyM, _) => "m",
|
||||
(KeyCode::KeyN, _) => "n",
|
||||
(KeyCode::KeyO, _) => "o",
|
||||
(KeyCode::KeyP, _) => "p",
|
||||
(KeyCode::KeyQ, _) => "q",
|
||||
(KeyCode::KeyR, _) => "r",
|
||||
(KeyCode::KeyS, _) => "s",
|
||||
(KeyCode::KeyT, _) => "t",
|
||||
(KeyCode::KeyU, _) => "u",
|
||||
(KeyCode::KeyV, _) => "v",
|
||||
(KeyCode::KeyW, _) => "w",
|
||||
(KeyCode::KeyX, _) => "x",
|
||||
(KeyCode::KeyY, _) => "y",
|
||||
(KeyCode::KeyZ, _) => "z",
|
||||
(KeyCode::Digit1, true) => "!",
|
||||
(KeyCode::Digit1, false) => "1",
|
||||
(KeyCode::Digit2, true) => "@",
|
||||
(KeyCode::Digit2, false) => "2",
|
||||
(KeyCode::Digit3, true) => "#",
|
||||
(KeyCode::Digit3, false) => "3",
|
||||
(KeyCode::Digit4, true) => "$",
|
||||
(KeyCode::Digit4, false) => "4",
|
||||
(KeyCode::Digit5, true) => "%",
|
||||
(KeyCode::Digit5, false) => "5",
|
||||
(KeyCode::Digit6, true) => "^",
|
||||
(KeyCode::Digit6, false) => "6",
|
||||
(KeyCode::Digit7, true) => "&",
|
||||
(KeyCode::Digit7, false) => "7",
|
||||
(KeyCode::Digit8, true) => "*",
|
||||
(KeyCode::Digit8, false) => "8",
|
||||
(KeyCode::Digit9, true) => "(",
|
||||
(KeyCode::Digit9, false) => "9",
|
||||
(KeyCode::Digit0, true) => ")",
|
||||
(KeyCode::Digit0, false) => "0",
|
||||
(KeyCode::Minus, true) => "_",
|
||||
(KeyCode::Minus, false) => "-",
|
||||
(KeyCode::Equal, true) => "+",
|
||||
(KeyCode::Equal, false) => "=",
|
||||
(KeyCode::BracketLeft, true) => "{",
|
||||
(KeyCode::BracketLeft, false) => "[",
|
||||
(KeyCode::BracketRight, true) => "}",
|
||||
(KeyCode::BracketRight, false) => "]",
|
||||
(KeyCode::Backslash, true) => "|",
|
||||
(KeyCode::Backslash, false) => "\\",
|
||||
(KeyCode::Semicolon, true) => ":",
|
||||
(KeyCode::Semicolon, false) => ";",
|
||||
(KeyCode::Quote, true) => "\"",
|
||||
(KeyCode::Quote, false) => "'",
|
||||
(KeyCode::Comma, true) => "<",
|
||||
(KeyCode::Comma, false) => ",",
|
||||
(KeyCode::Period, true) => ">",
|
||||
(KeyCode::Period, false) => ".",
|
||||
(KeyCode::Slash, true) => "?",
|
||||
(KeyCode::Slash, false) => "/",
|
||||
(KeyCode::Backquote, true) => "~",
|
||||
(KeyCode::Backquote, false) => "`",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "key_events_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::get_input_key;
|
||||
use winit::keyboard::{Key::Character, SmolStr};
|
||||
use super::{get_input_key, us_qwerty_fallback_for_chord};
|
||||
use winit::keyboard::{Key::Character, KeyCode, NativeKeyCode, PhysicalKey, SmolStr};
|
||||
|
||||
#[test]
|
||||
fn test_get_input_key() {
|
||||
@@ -48,3 +48,112 @@ fn test_get_input_key() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn us_qwerty_fallback_maps_letters() {
|
||||
// Letters return lowercase regardless of shift; `get_input_key` applies the
|
||||
// uppercase transform downstream.
|
||||
let cases = [
|
||||
(KeyCode::KeyA, "a"),
|
||||
(KeyCode::KeyC, "c"),
|
||||
(KeyCode::KeyV, "v"),
|
||||
(KeyCode::KeyZ, "z"),
|
||||
];
|
||||
for (code, expected) in cases {
|
||||
for shift in [false, true] {
|
||||
assert_eq!(
|
||||
us_qwerty_fallback_for_chord(&PhysicalKey::Code(code), shift),
|
||||
Some(expected),
|
||||
"expected {code:?} -> {expected} (shift={shift})",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn us_qwerty_fallback_maps_digits_and_punctuation() {
|
||||
let cases = [
|
||||
(KeyCode::Digit0, "0"),
|
||||
(KeyCode::Digit9, "9"),
|
||||
(KeyCode::Minus, "-"),
|
||||
(KeyCode::Equal, "="),
|
||||
(KeyCode::Slash, "/"),
|
||||
(KeyCode::Backquote, "`"),
|
||||
(KeyCode::Semicolon, ";"),
|
||||
(KeyCode::Comma, ","),
|
||||
];
|
||||
for (code, expected) in cases {
|
||||
assert_eq!(
|
||||
us_qwerty_fallback_for_chord(&PhysicalKey::Code(code), false),
|
||||
Some(expected),
|
||||
"expected {code:?} -> {expected}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn us_qwerty_fallback_maps_shifted_digits_and_punctuation() {
|
||||
let cases = [
|
||||
(KeyCode::Digit1, "!"),
|
||||
(KeyCode::Digit2, "@"),
|
||||
(KeyCode::Digit6, "^"),
|
||||
(KeyCode::Digit9, "("),
|
||||
(KeyCode::Digit0, ")"),
|
||||
(KeyCode::Minus, "_"),
|
||||
(KeyCode::Equal, "+"),
|
||||
(KeyCode::BracketLeft, "{"),
|
||||
(KeyCode::BracketRight, "}"),
|
||||
(KeyCode::Backslash, "|"),
|
||||
(KeyCode::Semicolon, ":"),
|
||||
(KeyCode::Quote, "\""),
|
||||
(KeyCode::Comma, "<"),
|
||||
(KeyCode::Period, ">"),
|
||||
(KeyCode::Slash, "?"),
|
||||
(KeyCode::Backquote, "~"),
|
||||
];
|
||||
for (code, expected) in cases {
|
||||
assert_eq!(
|
||||
us_qwerty_fallback_for_chord(&PhysicalKey::Code(code), true),
|
||||
Some(expected),
|
||||
"expected {code:?} + shift -> {expected}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn us_qwerty_fallback_returns_none_for_unmapped_keys() {
|
||||
// Keys outside the chord-shortcut set should fall through so the original
|
||||
// logical_key is preserved.
|
||||
let unmapped = [
|
||||
KeyCode::F1,
|
||||
KeyCode::F13,
|
||||
KeyCode::AltLeft,
|
||||
KeyCode::ShiftRight,
|
||||
KeyCode::ControlLeft,
|
||||
KeyCode::Enter,
|
||||
KeyCode::Escape,
|
||||
KeyCode::ArrowUp,
|
||||
KeyCode::Tab,
|
||||
];
|
||||
for code in unmapped {
|
||||
for shift in [false, true] {
|
||||
assert_eq!(
|
||||
us_qwerty_fallback_for_chord(&PhysicalKey::Code(code), shift),
|
||||
None,
|
||||
"{code:?} should not have a chord fallback (shift={shift})",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn us_qwerty_fallback_returns_none_for_unidentified_physical_key() {
|
||||
let unidentified = PhysicalKey::Unidentified(NativeKeyCode::Unidentified);
|
||||
for shift in [false, true] {
|
||||
assert_eq!(
|
||||
us_qwerty_fallback_for_chord(&unidentified, shift),
|
||||
None,
|
||||
"unidentified key should not have a chord fallback (shift={shift})",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user