diff --git a/apps/web/src/terminal/ghostty/keyCodes.test.ts b/apps/web/src/terminal/ghostty/keyCodes.test.ts index ef4b623fdfc8..053881efca72 100644 --- a/apps/web/src/terminal/ghostty/keyCodes.test.ts +++ b/apps/web/src/terminal/ghostty/keyCodes.test.ts @@ -14,12 +14,29 @@ describe("ghosttyKeyForCode", () => { describe("ghosttyConsumedMods", () => { const shifted = { altKey: false, ctrlKey: false, key: "@", metaKey: false, shiftKey: true }; - it("only consumes a lone Shift producing a character", () => { - expect(ghosttyConsumedMods(shifted)).toBe(1); - expect(ghosttyConsumedMods({ ...shifted, ctrlKey: true })).toBe(0); - expect(ghosttyConsumedMods({ ...shifted, key: "Tab" })).toBe(0); + it("consumes a lone Shift producing a character", () => { + expect(ghosttyConsumedMods(shifted, "Linux")).toBe(1); + expect(ghosttyConsumedMods({ ...shifted, ctrlKey: true }, "MacIntel")).toBe(0); + expect(ghosttyConsumedMods({ ...shifted, key: "Tab" }, "MacIntel")).toBe(0); // Deliberate: Shift+Space collapses to Space so it still types one. - expect(ghosttyConsumedMods({ ...shifted, key: " " })).toBe(1); + expect(ghosttyConsumedMods({ ...shifted, key: " " }, "Linux")).toBe(1); + }); + + it("consumes a lone macOS Option that produced a character", () => { + const option = { altKey: true, ctrlKey: false, key: "@", metaKey: false, shiftKey: false }; + expect(ghosttyConsumedMods(option, "MacIntel")).toBe(1 << 2); + expect(ghosttyConsumedMods({ ...option, key: "€" }, "MacIntel")).toBe(1 << 2); + expect(ghosttyConsumedMods({ ...option, key: "∫" }, "MacIntel")).toBe(1 << 2); + expect(ghosttyConsumedMods({ ...option, key: "\\", shiftKey: true }, "MacIntel")).toBe( + (1 << 2) | 1, + ); + expect(ghosttyConsumedMods({ ...option, ctrlKey: true }, "MacIntel")).toBe(0); + expect(ghosttyConsumedMods({ ...option, metaKey: true }, "MacIntel")).toBe(0); + expect(ghosttyConsumedMods({ ...option, key: "ArrowLeft" }, "MacIntel")).toBe(0); + expect(ghosttyConsumedMods({ ...option, key: "Dead" }, "MacIntel")).toBe(0); + expect(ghosttyConsumedMods(option, "Linux")).toBe(0); + expect(ghosttyConsumedMods(option, "Win32")).toBe(0); + expect(ghosttyConsumedMods({ ...option, shiftKey: true }, "Linux")).toBe(0); }); }); diff --git a/apps/web/src/terminal/ghostty/keyCodes.ts b/apps/web/src/terminal/ghostty/keyCodes.ts index a643827d6f2d..8c66188f7465 100644 --- a/apps/web/src/terminal/ghostty/keyCodes.ts +++ b/apps/web/src/terminal/ghostty/keyCodes.ts @@ -233,13 +233,34 @@ export function loadGhosttyKeyboardLayoutMap(): Promise, + platform = typeof navigator === "undefined" ? "" : (navigator.platform ?? ""), ): number { - if (!event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return 0; - return [...event.key].length === 1 ? 1 : 0; + if ([...event.key].length !== 1 || event.ctrlKey || event.metaKey) return 0; + const shift = event.shiftKey ? GHOSTTY_MOD_SHIFT : 0; + // Same host check as isMacPlatform in lib/utils.ts. Kept local so this module + // stays free of the app utility graph. + if (event.altKey && /mac|iphone|ipad|ipod/i.test(platform)) return shift | GHOSTTY_MOD_ALT; + if (!event.shiftKey || event.altKey) return 0; + return GHOSTTY_MOD_SHIFT; } export function ghosttyUnshiftedCodepoint(