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 <[email protected]>
This commit is contained in:
Florent Vandflorentvdry authored and GitHub committed 2026-10-29 22:49:08 +08:00
1 parent b9cfd54742
commit 14d7fd0061
4 files changed
+22 -4

No files matched your search

+3 -3
View File
@@ -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" };
+5
View File
@@ -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", () => {
+10
View File
@@ -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",
+4 -1
View File
@@ -258,7 +258,10 @@ export function strokeOf(
e: Pick<KeyboardEvent, "code" | "key" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey">,
): 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;