// Adapted from T3 Code's libghostty-vt browser adapter (MIT, T3 Tools Inc.). // See LICENSE-T3CODE in this directory. // 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; // SAFETY: navigator.keyboard is Chromium-only and absent from lib.dom; the // optional field keeps every other engine on the undefined branch. const keyboard = ( globalThis.navigator as Navigator & { readonly keyboard?: { getLayoutMap(): Promise }; } | undefined )?.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; }