Skip to content
Merged
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
20 changes: 20 additions & 0 deletions apps/web/src/terminal/ghostty/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,26 @@ describe("GhosttyTerminalCore snapshots", () => {
expect(core.encodeKey(event)).toBe(expected);
});

it.each(["", "\x1b[>1u"])("encodes macOS Option text with keyboard mode %j", async (mode) => {
using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("MacIntel");
const core = await createCore();
core.write(mode);
const option = {
code: "KeyL",
key: "@",
repeat: false,
shiftKey: false,
ctrlKey: false,
altKey: true,
metaKey: false,
isComposing: false,
getModifierState: () => false,
} as unknown as KeyboardEvent;

expect(core.encodeKey(option)).toBe("@");
expect(core.encodeKey({ ...option, code: "KeyA", key: "Å", shiftKey: true })).toBe("Å");
});

it("preserves styles, wide cells, and selection after shared memory grows", async () => {
const core = await createCore();
const runtime = await loadGhosttyRuntime();
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/terminal/ghostty/keyCodes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ describe("ghosttyConsumedMods", () => {
expect(ghosttyConsumedMods({ ...shifted, ctrlKey: true })).toBe(0);
expect(ghosttyConsumedMods({ ...shifted, key: "Tab" })).toBe(0);
expect(ghosttyConsumedMods({ ...shifted, key: " " })).toBe(1);
expect(ghosttyConsumedMods({ ...shifted, altKey: true }, "Linux x86_64")).toBe(0);
});

it("consumes macOS Option when it produced a character", () => {
const option = { ...shifted, altKey: true, key: "[", shiftKey: false };
expect(ghosttyConsumedMods(option, "MacIntel")).toBe(1 << 2);
expect(ghosttyConsumedMods({ ...option, shiftKey: true }, "MacIntel")).toBe(1 | (1 << 2));
expect(ghosttyConsumedMods({ ...option, key: "ArrowLeft" }, "MacIntel")).toBe(0);
expect(ghosttyConsumedMods({ ...option, ctrlKey: true }, "MacIntel")).toBe(0);
expect(ghosttyConsumedMods({ ...option, metaKey: true }, "MacIntel")).toBe(0);
});
});

Expand Down
10 changes: 6 additions & 4 deletions apps/web/src/terminal/ghostty/keyCodes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { US_SHIFTED_TO_UNSHIFTED_KEYS } from "@t3tools/shared/keybindings";
import { isMacPlatform, US_SHIFTED_TO_UNSHIFTED_KEYS } from "@t3tools/shared/keybindings";

// This order mirrors GhosttyKey in ghostty/vt/key/event.h. The values are
// intentionally derived from the official W3C-aligned enum instead of
Expand Down Expand Up @@ -216,12 +216,14 @@ export function loadGhosttyKeyboardLayoutMap(): Promise<GhosttyKeyboardLayoutMap
return promise;
}

// Browsers omit consumed modifiers; treat Shift as consumed for unchorded characters.
// Browsers omit consumed modifiers; macOS Option composes layout characters too.
export function ghosttyConsumedMods(
event: Pick<KeyboardEvent, "altKey" | "ctrlKey" | "key" | "metaKey" | "shiftKey">,
platform = navigator.platform,
): number {
if (!event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return 0;
return [...event.key].length === 1 ? 1 : 0;
if (event.ctrlKey || event.metaKey || [...event.key].length !== 1) return 0;
if (event.altKey && !isMacPlatform(platform)) return 0;
return (event.shiftKey ? 1 : 0) | (event.altKey ? 1 << 2 : 0);
}

export function ghosttyUnshiftedCodepoint(
Expand Down
Loading