// This order mirrors GhosttyKey in ghostty/vt/key/event.h. The values are // intentionally derived from the official W3C-aligned enum instead of // maintaining a second keyboard protocol. const ghosttyKeyboardCodes = [ "Unidentified", "Backquote", "Backslash", "BracketLeft", "BracketRight", "Comma", "Digit0", "Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9", "Equal", "IntlBackslash", "IntlRo", "IntlYen", "KeyA", "KeyB", "KeyC", "KeyD", "KeyE", "KeyF", "KeyG", "KeyH", "KeyI", "KeyJ", "KeyK", "KeyL", "KeyM", "KeyN", "KeyO", "KeyP", "KeyQ", "KeyR", "KeyS", "KeyT", "KeyU", "KeyV", "KeyW", "KeyX", "KeyY", "KeyZ", "Minus", "Period", "Quote", "Semicolon", "Slash", "AltLeft", "AltRight", "Backspace", "CapsLock", "ContextMenu", "ControlLeft", "ControlRight", "Enter", "MetaLeft", "MetaRight", "ShiftLeft", "ShiftRight", "Space", "Tab", "Convert", "KanaMode", "NonConvert", "Delete", "End", "Help", "Home", "Insert", "PageDown", "PageUp", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp", "NumLock", "Numpad0", "Numpad1", "Numpad2", "Numpad3", "Numpad4", "Numpad5", "Numpad6", "Numpad7", "Numpad8", "Numpad9", "NumpadAdd", "NumpadBackspace", "NumpadClear", "NumpadClearEntry", "NumpadComma", "NumpadDecimal", "NumpadDivide", "NumpadEnter", "NumpadEqual", "NumpadMemoryAdd", "NumpadMemoryClear", "NumpadMemoryRecall", "NumpadMemoryStore", "NumpadMemorySubtract", "NumpadMultiply", "NumpadParenLeft", "NumpadParenRight", "NumpadSubtract", "NumpadSeparator", "NumpadArrowUp", "NumpadArrowDown", "NumpadArrowRight", "NumpadArrowLeft", "NumpadBegin", "NumpadHome", "NumpadEnd", "NumpadInsert", "NumpadDelete", "NumpadPageUp", "NumpadPageDown", "Escape", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24", "F25", "Fn", "FnLock", "PrintScreen", "ScrollLock", "Pause", "BrowserBack", "BrowserFavorites", "BrowserForward", "BrowserHome", "BrowserRefresh", "BrowserSearch", "BrowserStop", "Eject", "LaunchApp1", "LaunchApp2", "LaunchMail", "MediaPlayPause", "MediaSelect", "MediaStop", "MediaTrackNext", "MediaTrackPrevious", "Power", "Sleep", "AudioVolumeDown", "AudioVolumeMute", "AudioVolumeUp", "WakeUp", "Copy", "Cut", "Paste", ] as const; const codeToGhosttyKey = new Map( ghosttyKeyboardCodes.map((code, index) => [code, index]), ); export function ghosttyKeyForCode(code: string): number { return codeToGhosttyKey.get(code) ?? 0; } export interface GhosttyKeyboardLayoutMap { get(code: string): string | undefined; } const shiftedToUnshiftedCharacter = new Map([ ["!", "1"], ["@", "2"], ["#", "3"], ["$", "4"], ["%", "5"], ["^", "6"], ["&", "7"], ["*", "8"], ["(", "9"], [")", "0"], ["~", "`"], ["_", "-"], ["+", "="], ["{", "["], ["}", "]"], ["|", "\\"], [":", ";"], ['"', "'"], ["<", ","], [">", "."], ["?", "/"], ]); let keyboardLayoutMapPromise: Promise | undefined; export function loadGhosttyKeyboardLayoutMap(): Promise { if (keyboardLayoutMapPromise) return keyboardLayoutMapPromise; const browserNavigator = globalThis.navigator as | (Navigator & { readonly keyboard?: { getLayoutMap(): Promise; }; }) | undefined; const keyboard = browserNavigator?.keyboard; const promise = keyboard?.getLayoutMap().catch(() => undefined) ?? Promise.resolve(undefined); keyboardLayoutMapPromise = promise; return promise; } // Browsers do not expose consumed modifiers; treat Shift as consumed for // unchorded character input. export function ghosttyConsumedMods( event: Pick, ): number { if (!event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return 0; return [...event.key].length === 1 ? 1 : 0; } export function ghosttyUnshiftedCodepoint( event: Pick, layoutMap?: GhosttyKeyboardLayoutMap, ): number { if ([...event.key].length !== 1) return 0; const layoutCharacter = layoutMap?.get(event.code); if (layoutCharacter && [...layoutCharacter].length === 1) { return layoutCharacter.codePointAt(0) ?? 0; } if (/^[A-Z]$/u.test(event.key)) return event.key.charCodeAt(0) + 32; if (event.shiftKey) { const unshiftedCharacter = shiftedToUnshiftedCharacter.get(event.key); if (unshiftedCharacter) return unshiftedCharacter.codePointAt(0) ?? 0; const lowercase = event.key.toLowerCase(); if (lowercase !== event.key && [...lowercase].length === 1) { return lowercase.codePointAt(0) ?? 0; } // Without layout data the unshifted form of a shifted key is unknowable; // reporting the shifted character as unshifted corrupts Kitty alternate keys. return 0; } return event.key.codePointAt(0) ?? 0; }