Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions apps/web/src/terminal/ghostty/keyCodes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand Down
29 changes: 25 additions & 4 deletions apps/web/src/terminal/ghostty/keyCodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,34 @@ export function loadGhosttyKeyboardLayoutMap(): Promise<GhosttyKeyboardLayoutMap
return promise;
}

// Browsers do not expose consumed modifiers; treat Shift as consumed for
// unchorded character input.
const GHOSTTY_MOD_SHIFT = 1 << 0;
const GHOSTTY_MOD_ALT = 1 << 2;

/**
* Modifiers the layout consumed to produce `event.key`, as a GhosttyMods bitmask.
*
* The DOM does not report consumed modifiers. A lone Shift is consumed so a
* shifted character is encoded as text. On macOS a lone Option is consumed too,
* along with Shift when Shift participated. This WASM build of libghostty-vt is
* not macOS, so it ignores `macos-option-as-alt` and DEC 1036 prefixes Alt:
* German Option+L (`@`) becomes `ESC @` (readline set-mark) and the rest of the
* Option layer (`€`, `~`, `[]{}|`, `\`) is dropped the same way. Consuming
* Option makes the encoder write the composed character, matching Ghostty's
* default of `macos-option-as-alt = false`. Ctrl and Meta are never consumed,
* so those chords stay intact. Option+arrow is not a single character, so word
* motion is unchanged.
*/
export function ghosttyConsumedMods(
event: Pick<KeyboardEvent, "altKey" | "ctrlKey" | "key" | "metaKey" | "shiftKey">,
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(
Expand Down
Loading