From 14d7fd006194aa1086b596c08af7d05ae457e5b2 Mon Sep 17 00:00:00 2001 From: Florent V Date: Thu, 29 Oct 2026 15:49:08 +0100 Subject: [PATCH] fix(keymap): match letter shortcuts by typed character on AZERTY (#291) Letter shortcuts were matched by physical key (KeyboardEvent.code), so on AZERTY the key typing z (physical KeyW) dispatched Ctrl+W and closed the tab instead of undoing. Letters are now named by the character typed when Alt is not held; Alt combos keep the physical key since they may type a symbol (macOS Option, AltGr). The terminal's Ctrl+letter control codes use the typed letter too. Co-authored-by: florentvdry <16673235+florentvdry@users.noreply.github.com> --- src/features/terminal/keys.ts | 6 +++--- src/features/terminal/terminal.test.ts | 5 +++++ src/platform/keymap.test.ts | 10 ++++++++++ src/platform/keymap.ts | 5 ++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/features/terminal/keys.ts b/src/features/terminal/keys.ts index 68bc13ec4..98ce5a91b 100644 --- a/src/features/terminal/keys.ts +++ b/src/features/terminal/keys.ts @@ -60,9 +60,9 @@ export function terminalKeyAction(e: KeyLike, hasSelection: boolean): TerminalKe if (e.code === "KeyT") return { kind: "pass" }; } if (ctrl && !meta && !alt) { - // Use the physical key so layouts and Shift don't change the control code. - const m = /^Key([A-Z])$/.exec(e.code); - if (m) return { kind: "send", data: String.fromCharCode(m[1]!.charCodeAt(0) - 64) }; + // The letter the key types, so Shift doesn't change the control code and AZERTY's Ctrl+Z is ^Z, not ^W. + const letter = /^[a-zA-Z]$/.test(e.key) ? e.key : /^Key([A-Z])$/.exec(e.code)?.[1]; + if (letter) return { kind: "send", data: String.fromCharCode(letter.toUpperCase().charCodeAt(0) - 64) }; if (e.code === "BracketLeft") return { kind: "send", data: "\x1b" }; if (e.code === "Backslash") return { kind: "send", data: "\x1c" }; if (e.code === "BracketRight") return { kind: "send", data: "\x1d" }; diff --git a/src/features/terminal/terminal.test.ts b/src/features/terminal/terminal.test.ts index 831ebe870..f52332254 100644 --- a/src/features/terminal/terminal.test.ts +++ b/src/features/terminal/terminal.test.ts @@ -128,6 +128,11 @@ describe("terminal keys", () => { kind: "send", data: "\x14", }); + // AZERTY: the key typing "z" is physically KeyW; Ctrl+Z must send ^Z (SIGTSTP), not ^W. + expect(terminalKeyAction(key({ key: "z", code: "KeyW", ctrlKey: true }), false)).toEqual({ + kind: "send", + data: "\x1a", + }); }); it("implements macOS conveniences", () => { diff --git a/src/platform/keymap.test.ts b/src/platform/keymap.test.ts index 81ed0aab3..c5d3e6944 100644 --- a/src/platform/keymap.test.ts +++ b/src/platform/keymap.test.ts @@ -29,6 +29,16 @@ describe("keymap", () => { expect(formatBinding("Ctrl+NumpadSubtract", false)).toBe("Ctrl+NumPad -"); }); + it("names letters by the character typed, so AZERTY's Ctrl+Z is not Ctrl+W", () => { + // AZERTY: the key typing "z" is physically KeyW. + expect(strokeOf({ code: "KeyW", key: "z", metaKey: false, ctrlKey: true, altKey: false, shiftKey: false })).toBe( + "Ctrl+Z", + ); + expect(strokeOf({ code: "KeyW", key: "Z", metaKey: false, ctrlKey: true, altKey: false, shiftKey: true })).toBe( + "Ctrl+Shift+Z", + ); + }); + it("uses physical key codes", () => { expect(strokeOf({ code: "KeyF", key: "ƒ", metaKey: false, ctrlKey: false, altKey: true, shiftKey: false })).toBe( "Alt+F", diff --git a/src/platform/keymap.ts b/src/platform/keymap.ts index 5ce56c952..43941d486 100644 --- a/src/platform/keymap.ts +++ b/src/platform/keymap.ts @@ -258,7 +258,10 @@ export function strokeOf( e: Pick, ): string | null { let key: string | undefined; - if (/^Key[A-Z]$/.test(e.code)) key = e.code.slice(3); + // A letter is named by the character it types: on AZERTY the Z key sits on the physical KeyW, + // so Ctrl+Z must not read as Ctrl+W. Alt combos may type a symbol (⌥ on macOS, AltGr), so they keep the physical key. + if (/^[a-zA-Z]$/.test(e.key) && !e.altKey) key = e.key.toUpperCase(); + else if (/^Key[A-Z]$/.test(e.code)) key = e.code.slice(3); else if (/^Digit\d$/.test(e.code)) key = e.code.slice(5); else if (/^Numpad\d$/.test(e.code)) key = e.code.slice(6); else if (/^F\d{1,2}$/.test(e.code)) key = e.code;