Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
1072 lines
34 KiB
TypeScript
1072 lines
34 KiB
TypeScript
export const KITTY_KEYBOARD_DISAMBIGUATE_ESC_CODES = 0b00001;
|
|
export const KITTY_KEYBOARD_REPORT_EVENT_TYPES = 0b00010;
|
|
export const KITTY_KEYBOARD_REPORT_ALTERNATE_KEYS = 0b00100;
|
|
export const KITTY_KEYBOARD_REPORT_ALL_KEYS_AS_ESC_CODES = 0b01000;
|
|
export const KITTY_KEYBOARD_REPORT_ASSOCIATED_TEXT = 0b10000;
|
|
export const KITTY_SUPPORTED_KEYBOARD_FLAGS =
|
|
KITTY_KEYBOARD_DISAMBIGUATE_ESC_CODES |
|
|
KITTY_KEYBOARD_REPORT_EVENT_TYPES |
|
|
KITTY_KEYBOARD_REPORT_ALTERNATE_KEYS |
|
|
KITTY_KEYBOARD_REPORT_ALL_KEYS_AS_ESC_CODES |
|
|
KITTY_KEYBOARD_REPORT_ASSOCIATED_TEXT;
|
|
|
|
const MAX_KEYBOARD_MODE_STACK_DEPTH = 32;
|
|
|
|
export type KittyKeyboardModeState = {
|
|
mainFlags: number;
|
|
alternateFlags: number;
|
|
mainStack: number[];
|
|
alternateStack: number[];
|
|
alternateScreenActive: boolean;
|
|
};
|
|
|
|
export type KittyKeyboardModeApplyMode = 1 | 2 | 3;
|
|
|
|
export type KittyKeyboardEvent = {
|
|
type?: string;
|
|
key: string;
|
|
code?: string;
|
|
location?: number;
|
|
repeat?: boolean;
|
|
isComposing?: boolean;
|
|
keyCode?: number;
|
|
shiftKey?: boolean;
|
|
altKey?: boolean;
|
|
ctrlKey?: boolean;
|
|
metaKey?: boolean;
|
|
getModifierState?: (key: string) => boolean;
|
|
/** Unmodified key in the active layout, supplied by KeyboardLayoutMap when available. */
|
|
unshiftedKey?: string;
|
|
/** The platform consumed Alt/Option to produce text rather than a shortcut modifier. */
|
|
altKeyProducesText?: boolean;
|
|
/** Legacy cursor-key mode used only for baseline fallback sequences. */
|
|
applicationCursorMode?: boolean;
|
|
};
|
|
|
|
type KittyKeyDefinition =
|
|
| { kind: "csi-u"; code: number }
|
|
| { kind: "csi-letter"; code: string }
|
|
| { kind: "ss3"; code: string; modifiedTildeCode?: number }
|
|
| { kind: "csi-tilde"; code: number };
|
|
|
|
const FUNCTIONAL_CSI_U_CODES: Record<string, number> = {
|
|
Escape: 27,
|
|
Enter: 13,
|
|
Tab: 9,
|
|
Backspace: 127,
|
|
CapsLock: 57358,
|
|
ScrollLock: 57359,
|
|
NumLock: 57360,
|
|
PrintScreen: 57361,
|
|
Pause: 57362,
|
|
ContextMenu: 57363,
|
|
MediaPlay: 57428,
|
|
MediaPause: 57429,
|
|
MediaPlayPause: 57430,
|
|
MediaReverse: 57431,
|
|
MediaStop: 57432,
|
|
MediaFastForward: 57433,
|
|
MediaRewind: 57434,
|
|
MediaTrackNext: 57435,
|
|
MediaTrackPrevious: 57436,
|
|
MediaRecord: 57437,
|
|
AudioVolumeDown: 57438,
|
|
AudioVolumeUp: 57439,
|
|
AudioVolumeMute: 57440,
|
|
ShiftLeft: 57441,
|
|
ControlLeft: 57442,
|
|
AltLeft: 57443,
|
|
MetaLeft: 57444,
|
|
HyperLeft: 57445,
|
|
KittyMetaLeft: 57446,
|
|
SuperLeft: 57444,
|
|
ShiftRight: 57447,
|
|
ControlRight: 57448,
|
|
AltRight: 57449,
|
|
MetaRight: 57450,
|
|
HyperRight: 57451,
|
|
KittyMetaRight: 57452,
|
|
SuperRight: 57450,
|
|
AltGraph: 57453,
|
|
ISOLevel3Shift: 57453,
|
|
ISOLevel5Shift: 57454,
|
|
};
|
|
|
|
const CSI_LETTER_KEYS: Record<string, string> = {
|
|
ArrowUp: "A",
|
|
ArrowDown: "B",
|
|
ArrowRight: "C",
|
|
ArrowLeft: "D",
|
|
Home: "H",
|
|
End: "F",
|
|
};
|
|
|
|
const SS3_KEYS: Record<string, { code: string; modifiedTildeCode?: number }> = {
|
|
F1: { code: "P" },
|
|
F2: { code: "Q" },
|
|
F3: { code: "R", modifiedTildeCode: 13 },
|
|
F4: { code: "S" },
|
|
};
|
|
|
|
const CSI_TILDE_KEYS: Record<string, number> = {
|
|
Insert: 2,
|
|
Delete: 3,
|
|
PageUp: 5,
|
|
PageDown: 6,
|
|
F5: 15,
|
|
F6: 17,
|
|
F7: 18,
|
|
F8: 19,
|
|
F9: 20,
|
|
F10: 21,
|
|
F11: 23,
|
|
F12: 24,
|
|
};
|
|
|
|
const PC101_BASE_KEYS: Record<string, string> = {
|
|
Backquote: "`",
|
|
Minus: "-",
|
|
Equal: "=",
|
|
BracketLeft: "[",
|
|
BracketRight: "]",
|
|
Backslash: "\\",
|
|
Semicolon: ";",
|
|
Quote: "'",
|
|
Comma: ",",
|
|
Period: ".",
|
|
Slash: "/",
|
|
Space: " ",
|
|
};
|
|
|
|
const MODIFIER_KEYS = new Set([
|
|
"Shift", "Control", "Alt", "Meta", "OS", "Super", "Hyper",
|
|
"AltGraph", "ISOLevel3Shift", "ISOLevel5Shift",
|
|
]);
|
|
const LOCK_KEYS = new Set(["CapsLock", "NumLock", "ScrollLock"]);
|
|
const NON_TEXT_DOM_KEYS = new Set(["Dead", "Process", "Unidentified", "Compose"]);
|
|
const LEGACY_C0_KEYS = new Set(["Enter", "Tab", "Backspace"]);
|
|
const LEGACY_NAMED_DATA_KEYS = new Set([
|
|
"Escape", "Enter", "Tab", "Backspace", "Insert", "Delete", "PageUp", "PageDown",
|
|
"ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "ContextMenu",
|
|
...Array.from({ length: 12 }, (_, index) => `F${index + 1}`),
|
|
]);
|
|
|
|
const sanitizeFlags = (flags: number): number => {
|
|
if (!Number.isFinite(flags)) return 0;
|
|
return Math.max(0, Math.floor(flags)) & KITTY_SUPPORTED_KEYBOARD_FLAGS;
|
|
};
|
|
|
|
const clampPositiveInteger = (value: number, fallback: number): number => {
|
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
};
|
|
|
|
export const createKittyKeyboardModeState = (): KittyKeyboardModeState => ({
|
|
mainFlags: 0,
|
|
alternateFlags: 0,
|
|
mainStack: [],
|
|
alternateStack: [],
|
|
alternateScreenActive: false,
|
|
});
|
|
|
|
export const createKittyKeyboardSessionStateStore = () => {
|
|
const states = new WeakMap<object, KittyKeyboardModeState>();
|
|
return {
|
|
resolve(owner: object, preserveExisting: boolean): KittyKeyboardModeState {
|
|
const existing = states.get(owner);
|
|
if (existing && preserveExisting) return existing;
|
|
const created = createKittyKeyboardModeState();
|
|
states.set(owner, created);
|
|
return created;
|
|
},
|
|
reset(owner: object): void {
|
|
const existing = states.get(owner);
|
|
if (existing) resetKittyKeyboardModeState(existing);
|
|
},
|
|
};
|
|
};
|
|
|
|
export const resetKittyKeyboardModeState = (state: KittyKeyboardModeState): void => {
|
|
state.mainFlags = 0;
|
|
state.alternateFlags = 0;
|
|
state.mainStack.length = 0;
|
|
state.alternateStack.length = 0;
|
|
state.alternateScreenActive = false;
|
|
};
|
|
|
|
export const snapshotKittyKeyboardModeState = (
|
|
state: KittyKeyboardModeState,
|
|
): KittyKeyboardModeState => ({
|
|
mainFlags: sanitizeFlags(state.mainFlags),
|
|
alternateFlags: sanitizeFlags(state.alternateFlags),
|
|
mainStack: state.mainStack.slice(-MAX_KEYBOARD_MODE_STACK_DEPTH).map(sanitizeFlags),
|
|
alternateStack: state.alternateStack.slice(-MAX_KEYBOARD_MODE_STACK_DEPTH).map(sanitizeFlags),
|
|
alternateScreenActive: state.alternateScreenActive === true,
|
|
});
|
|
|
|
export const restoreKittyKeyboardModeState = (
|
|
state: KittyKeyboardModeState,
|
|
snapshot: KittyKeyboardModeState,
|
|
): void => {
|
|
const restored = snapshotKittyKeyboardModeState(snapshot);
|
|
state.mainFlags = restored.mainFlags;
|
|
state.alternateFlags = restored.alternateFlags;
|
|
state.mainStack.splice(0, state.mainStack.length, ...restored.mainStack);
|
|
state.alternateStack.splice(0, state.alternateStack.length, ...restored.alternateStack);
|
|
state.alternateScreenActive = restored.alternateScreenActive;
|
|
};
|
|
|
|
export const getKittyKeyboardModeFlags = (state: KittyKeyboardModeState): number => {
|
|
return state.alternateScreenActive ? state.alternateFlags : state.mainFlags;
|
|
};
|
|
|
|
export const isKittyKeyboardModeActive = (state: KittyKeyboardModeState): boolean => (
|
|
getKittyKeyboardModeFlags(state) !== 0
|
|
);
|
|
|
|
const isPhysicalDeadAltShortcut = (event: KittyKeyboardEvent): boolean => (
|
|
event.key === "Dead" &&
|
|
event.altKey === true &&
|
|
!modifierState(event, "AltGraph") &&
|
|
event.altKeyProducesText !== true &&
|
|
event.code !== undefined &&
|
|
(
|
|
/^Key[A-Z]$/.test(event.code) ||
|
|
/^Digit[0-9]$/.test(event.code) ||
|
|
PC101_BASE_KEYS[event.code] !== undefined
|
|
)
|
|
);
|
|
|
|
export const shouldDeferKittyKeyEvent = (event: KittyKeyboardEvent): boolean => (
|
|
event.isComposing === true ||
|
|
event.keyCode === 229 ||
|
|
(NON_TEXT_DOM_KEYS.has(event.key) && !isPhysicalDeadAltShortcut(event))
|
|
);
|
|
|
|
export const shouldMarkKittyTextInputEvent = (
|
|
event: Pick<InputEvent, "data" | "inputType">,
|
|
): boolean => event.inputType === "insertText" && Boolean(event.data);
|
|
|
|
export const shouldTreatKittyAltAsText = (
|
|
event: Pick<KittyKeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey">,
|
|
isMac: boolean,
|
|
altAsMeta: boolean,
|
|
): boolean => (
|
|
isMac &&
|
|
!altAsMeta &&
|
|
event.altKey === true &&
|
|
event.ctrlKey !== true &&
|
|
event.metaKey !== true &&
|
|
(event.key === "Dead" || Array.from(event.key).length === 1)
|
|
);
|
|
|
|
export const shouldExpectLegacyKeyboardData = (event: KittyKeyboardEvent): boolean => (
|
|
event.metaKey !== true &&
|
|
(Array.from(event.key).length === 1 || LEGACY_NAMED_DATA_KEYS.has(event.key))
|
|
);
|
|
|
|
const legacyControlCharacterForAscii = (character: string): string | null => {
|
|
if (/^[a-zA-Z]$/.test(character)) {
|
|
return String.fromCharCode(character.toLowerCase().charCodeAt(0) - 96);
|
|
}
|
|
const mappings: Record<string, string> = {
|
|
" ": "\0",
|
|
"/": "\u001f",
|
|
"2": "\0",
|
|
"3": "\u001b",
|
|
"4": "\u001c",
|
|
"5": "\u001d",
|
|
"6": "\u001e",
|
|
"7": "\u001f",
|
|
"8": "\u007f",
|
|
"?": "\u007f",
|
|
"@": "\0",
|
|
"[": "\u001b",
|
|
"\\": "\u001c",
|
|
"]": "\u001d",
|
|
"^": "\u001e",
|
|
"_": "\u001f",
|
|
};
|
|
return mappings[character] ?? null;
|
|
};
|
|
|
|
const asciiCharacter = (value: string | null | undefined): string | null => {
|
|
if (!value) return null;
|
|
const points = Array.from(value);
|
|
const codePoint = points.length === 1 ? points[0].codePointAt(0) : undefined;
|
|
return codePoint !== undefined && codePoint >= 0x20 && codePoint <= 0x7e
|
|
? points[0]
|
|
: null;
|
|
};
|
|
|
|
const activeOrBaseLegacyAscii = (event: KittyKeyboardEvent): string | null => (
|
|
asciiCharacter(event.key) ?? asciiCharacter(getBaseLayoutCharacter(event))
|
|
);
|
|
|
|
const legacyControlCharacterForEvent = (event: KittyKeyboardEvent): string | null => {
|
|
const active = asciiCharacter(event.key);
|
|
if (active !== null) {
|
|
if (event.shiftKey && /^[A-Z]$/.test(active)) return null;
|
|
return legacyControlCharacterForAscii(active);
|
|
}
|
|
const base = asciiCharacter(getBaseLayoutCharacter(event));
|
|
return base === null ? null : legacyControlCharacterForAscii(base);
|
|
};
|
|
|
|
const legacyCtrlAsciiPassthroughCharacter = (event: KittyKeyboardEvent): string | null => {
|
|
if (
|
|
!event.ctrlKey ||
|
|
event.shiftKey ||
|
|
event.metaKey ||
|
|
modifierState(event, "AltGraph") ||
|
|
legacyControlCharacterForEvent(event) !== null
|
|
) return null;
|
|
return activeOrBaseLegacyAscii(event);
|
|
};
|
|
|
|
const isLegacyCtrlAsciiPassthrough = (event: KittyKeyboardEvent): boolean => (
|
|
legacyCtrlAsciiPassthroughCharacter(event) !== null
|
|
);
|
|
|
|
const hasLayoutSpecificLegacyCtrlMapping = (event: KittyKeyboardEvent): boolean => {
|
|
if (
|
|
!event.ctrlKey ||
|
|
event.shiftKey ||
|
|
event.metaKey ||
|
|
event.altKeyProducesText ||
|
|
modifierState(event, "AltGraph") ||
|
|
modifierState(event, "Hyper") ||
|
|
modifierState(event, "KittyMeta")
|
|
) return false;
|
|
const active = asciiCharacter(event.key);
|
|
const base = asciiCharacter(getBaseLayoutCharacter(event));
|
|
return active !== null &&
|
|
(base === null || active !== base) &&
|
|
legacyControlCharacterForAscii(active) !== null;
|
|
};
|
|
|
|
export const encodeLegacyKeyboardEvent = (
|
|
event: KittyKeyboardEvent,
|
|
applicationCursorMode = false,
|
|
includeLocks = false,
|
|
): string | null => {
|
|
if (event.type === "keyup" || event.type === "keypress" || shouldDeferKittyKeyEvent(event)) {
|
|
return null;
|
|
}
|
|
|
|
const legacyModifier =
|
|
(event.shiftKey ? 1 : 0) |
|
|
(event.altKey && !event.altKeyProducesText ? 2 : 0) |
|
|
(event.ctrlKey ? 4 : 0) |
|
|
(event.metaKey ? 8 : 0) |
|
|
(modifierState(event, "Hyper") ? 16 : 0) |
|
|
(modifierState(event, "KittyMeta") ? 32 : 0) |
|
|
(includeLocks && modifierState(event, "CapsLock") ? 64 : 0) |
|
|
(includeLocks && modifierState(event, "NumLock") ? 128 : 0);
|
|
const modifiedCsi = (body: string, final: string) => (
|
|
`\u001b[${body};${legacyModifier + 1}${final}`
|
|
);
|
|
const cursorKey = (final: string) => {
|
|
if (legacyModifier) return modifiedCsi("1", final);
|
|
return applicationCursorMode ? `\u001bO${final}` : `\u001b[${final}`;
|
|
};
|
|
|
|
const cursorFinals: Record<string, string> = {
|
|
ArrowUp: "A",
|
|
ArrowDown: "B",
|
|
ArrowRight: "C",
|
|
ArrowLeft: "D",
|
|
Home: "H",
|
|
End: "F",
|
|
};
|
|
if (cursorFinals[event.key]) return cursorKey(cursorFinals[event.key]);
|
|
if (event.key === "Clear" && event.code === "Numpad5") {
|
|
if (legacyModifier) return modifiedCsi("1", "E");
|
|
return applicationCursorMode ? "\u001bOE" : "\u001b[E";
|
|
}
|
|
|
|
if (event.key === "Escape") return event.altKey ? "\u001b\u001b" : "\u001b";
|
|
if (event.key === "Enter") return event.altKey ? "\u001b\r" : "\r";
|
|
if (event.key === "Tab") {
|
|
const value = event.shiftKey ? "\u001b[Z" : "\t";
|
|
return event.altKey && !event.altKeyProducesText ? `\u001b${value}` : value;
|
|
}
|
|
if (event.key === "Backspace") {
|
|
const value = event.ctrlKey ? "\b" : "\u007f";
|
|
return event.altKey ? `\u001b${value}` : value;
|
|
}
|
|
|
|
const tildeCodes: Record<string, number> = {
|
|
ContextMenu: 29,
|
|
Insert: 2,
|
|
Delete: 3,
|
|
PageUp: 5,
|
|
PageDown: 6,
|
|
F5: 15,
|
|
F6: 17,
|
|
F7: 18,
|
|
F8: 19,
|
|
F9: 20,
|
|
F10: 21,
|
|
F11: 23,
|
|
F12: 24,
|
|
};
|
|
const tildeCode = tildeCodes[event.key];
|
|
if (tildeCode !== undefined) {
|
|
return legacyModifier
|
|
? modifiedCsi(String(tildeCode), "~")
|
|
: `\u001b[${tildeCode}~`;
|
|
}
|
|
|
|
const fMatch = /^F([1-4])$/.exec(event.key);
|
|
if (fMatch) {
|
|
const number = Number(fMatch[1]);
|
|
if (number === 3 && legacyModifier) return modifiedCsi("13", "~");
|
|
const final = "PQRS"[number - 1];
|
|
return legacyModifier ? modifiedCsi("1", final) : `\u001bO${final}`;
|
|
}
|
|
|
|
if (
|
|
event.ctrlKey &&
|
|
!event.metaKey &&
|
|
!modifierState(event, "AltGraph") &&
|
|
(!event.shiftKey || (event.altKey && !event.altKeyProducesText))
|
|
) {
|
|
const control = legacyControlCharacterForEvent(event);
|
|
if (control !== null) {
|
|
return event.altKey && !event.altKeyProducesText ? `\u001b${control}` : control;
|
|
}
|
|
const passthrough = legacyCtrlAsciiPassthroughCharacter(event);
|
|
if (passthrough !== null) {
|
|
return event.altKey && !event.altKeyProducesText ? `\u001b${passthrough}` : passthrough;
|
|
}
|
|
}
|
|
|
|
if (event.ctrlKey && event.shiftKey) {
|
|
if (event.code === "Space") return "\0";
|
|
if (event.code === "Minus") return "\u001f";
|
|
if (event.code === "Digit2") return "\0";
|
|
if (event.code === "Digit6") return "\u001e";
|
|
}
|
|
|
|
if (Array.from(event.key).length === 1 && !event.ctrlKey && !event.metaKey) {
|
|
if (event.altKey && !event.altKeyProducesText) return `\u001b${event.key}`;
|
|
return event.key;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
export const setKittyKeyboardAlternateScreenActive = (
|
|
state: KittyKeyboardModeState,
|
|
active: boolean,
|
|
): void => {
|
|
state.alternateScreenActive = active;
|
|
};
|
|
|
|
export const setKittyKeyboardModeFlags = (
|
|
state: KittyKeyboardModeState,
|
|
flags: number,
|
|
mode: KittyKeyboardModeApplyMode = 1,
|
|
): number => {
|
|
const sanitized = sanitizeFlags(flags);
|
|
const current = getKittyKeyboardModeFlags(state);
|
|
|
|
let next = current;
|
|
switch (mode) {
|
|
case 1:
|
|
next = sanitized;
|
|
break;
|
|
case 2:
|
|
next = current | sanitized;
|
|
break;
|
|
case 3:
|
|
next = current & ~sanitized;
|
|
break;
|
|
}
|
|
|
|
if (state.alternateScreenActive) state.alternateFlags = next;
|
|
else state.mainFlags = next;
|
|
return next;
|
|
};
|
|
|
|
export const pushKittyKeyboardModeFlags = (
|
|
state: KittyKeyboardModeState,
|
|
flags = 0,
|
|
): number => {
|
|
const stack = state.alternateScreenActive ? state.alternateStack : state.mainStack;
|
|
stack.push(getKittyKeyboardModeFlags(state));
|
|
if (stack.length > MAX_KEYBOARD_MODE_STACK_DEPTH) stack.shift();
|
|
return setKittyKeyboardModeFlags(state, flags, 1);
|
|
};
|
|
|
|
export const popKittyKeyboardModeFlags = (
|
|
state: KittyKeyboardModeState,
|
|
count = 1,
|
|
): number => {
|
|
const stack = state.alternateScreenActive ? state.alternateStack : state.mainStack;
|
|
const total = clampPositiveInteger(count, 1);
|
|
let next = getKittyKeyboardModeFlags(state);
|
|
|
|
for (let i = 0; i < total; i += 1) {
|
|
next = stack.pop() ?? 0;
|
|
}
|
|
|
|
if (state.alternateScreenActive) state.alternateFlags = next;
|
|
else state.mainFlags = next;
|
|
return next;
|
|
};
|
|
|
|
export const buildKittyKeyboardModeQueryResponse = (
|
|
state: KittyKeyboardModeState,
|
|
): string => `\u001b[?${getKittyKeyboardModeFlags(state)}u`;
|
|
|
|
const modifierState = (event: KittyKeyboardEvent, key: string): boolean => {
|
|
try {
|
|
return event.getModifierState?.(key) === true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const getKittyModifierValue = (
|
|
event: KittyKeyboardEvent,
|
|
includeLocks = true,
|
|
): number => {
|
|
let bits = 0;
|
|
const altGraph = modifierState(event, "AltGraph");
|
|
if (event.shiftKey) bits |= 0b00000001;
|
|
if (event.altKey && !altGraph && !event.altKeyProducesText) bits |= 0b00000010;
|
|
if (event.ctrlKey && !altGraph) bits |= 0b00000100;
|
|
if (event.metaKey) bits |= 0b00001000;
|
|
if (modifierState(event, "Hyper")) bits |= 0b00010000;
|
|
// Chromium reserves getModifierState("Meta") for the platform Super key.
|
|
// Keep a distinct non-standard name so synthetic/native integrations can
|
|
// still represent Kitty's separate Meta bit without double-counting Super.
|
|
if (modifierState(event, "KittyMeta")) bits |= 0b00100000;
|
|
if (includeLocks && modifierState(event, "CapsLock")) bits |= 0b01000000;
|
|
if (includeLocks && modifierState(event, "NumLock")) bits |= 0b10000000;
|
|
return bits + 1;
|
|
};
|
|
|
|
const getEventType = (event: KittyKeyboardEvent): 1 | 2 | 3 => {
|
|
if (event.type === "keyup") return 3;
|
|
return event.repeat ? 2 : 1;
|
|
};
|
|
|
|
const numpadKeyDefinition = (event: KittyKeyboardEvent): KittyKeyDefinition | null => {
|
|
const code = event.code ?? "";
|
|
if (!code.startsWith("Numpad")) return null;
|
|
|
|
const navigationCodes: Record<string, number> = {
|
|
Insert: 57425,
|
|
Delete: 57426,
|
|
ArrowLeft: 57417,
|
|
ArrowRight: 57418,
|
|
ArrowUp: 57419,
|
|
ArrowDown: 57420,
|
|
PageUp: 57421,
|
|
PageDown: 57422,
|
|
Home: 57423,
|
|
End: 57424,
|
|
};
|
|
if (navigationCodes[event.key] !== undefined) {
|
|
return { kind: "csi-u", code: navigationCodes[event.key] };
|
|
}
|
|
|
|
const suffix = code.slice("Numpad".length);
|
|
if (event.key === "Clear") return { kind: "csi-tilde", code: 57427 };
|
|
if (/^[0-9]$/.test(suffix)) {
|
|
return { kind: "csi-u", code: 57399 + Number(suffix) };
|
|
}
|
|
const operationCodes: Record<string, number> = {
|
|
Decimal: 57409,
|
|
Divide: 57410,
|
|
Multiply: 57411,
|
|
Subtract: 57412,
|
|
Add: 57413,
|
|
Enter: 57414,
|
|
Equal: 57415,
|
|
Comma: 57416,
|
|
Separator: 57416,
|
|
};
|
|
const mapped = operationCodes[suffix];
|
|
return mapped === undefined ? null : { kind: "csi-u", code: mapped };
|
|
};
|
|
|
|
const getKeyDefinition = (
|
|
event: KittyKeyboardEvent,
|
|
includeNumpad = true,
|
|
): KittyKeyDefinition | null => {
|
|
if (includeNumpad) {
|
|
const numpad = numpadKeyDefinition(event);
|
|
if (numpad) return numpad;
|
|
}
|
|
|
|
const functionalCode = FUNCTIONAL_CSI_U_CODES[event.key];
|
|
if (functionalCode !== undefined) return { kind: "csi-u", code: functionalCode };
|
|
|
|
const modifierCode = event.code ? FUNCTIONAL_CSI_U_CODES[event.code] : undefined;
|
|
if (modifierCode !== undefined) return { kind: "csi-u", code: modifierCode };
|
|
|
|
const fMatch = /^F(\d+)$/.exec(event.key);
|
|
if (fMatch) {
|
|
const number = Number(fMatch[1]);
|
|
if (number >= 13 && number <= 35) {
|
|
return { kind: "csi-u", code: 57363 + number };
|
|
}
|
|
}
|
|
|
|
const letter = CSI_LETTER_KEYS[event.key];
|
|
if (letter) return { kind: "csi-letter", code: letter };
|
|
const ss3 = SS3_KEYS[event.key];
|
|
if (ss3) return { kind: "ss3", ...ss3 };
|
|
const tilde = CSI_TILDE_KEYS[event.key];
|
|
if (tilde !== undefined) return { kind: "csi-tilde", code: tilde };
|
|
return null;
|
|
};
|
|
|
|
const getBaseLayoutCharacter = (event: KittyKeyboardEvent): string | null => {
|
|
const code = event.code ?? "";
|
|
if (/^Key[A-Z]$/.test(code)) return code.slice(3).toLowerCase();
|
|
if (/^Digit[0-9]$/.test(code)) return code.slice(5);
|
|
return PC101_BASE_KEYS[code] ?? null;
|
|
};
|
|
|
|
const getUnicodeKeyCode = (event: KittyKeyboardEvent): number | null => {
|
|
const base = getBaseLayoutCharacter(event);
|
|
if (event.unshiftedKey && Array.from(event.unshiftedKey).length === 1) {
|
|
const unshifted = event.unshiftedKey.toLocaleLowerCase();
|
|
if (Array.from(unshifted).length === 1) return unshifted.codePointAt(0) ?? null;
|
|
}
|
|
if (Array.from(event.key).length === 1) {
|
|
const value = event.key.codePointAt(0) ?? null;
|
|
if (value !== null && value >= 65 && value <= 90) return value + 32;
|
|
if (event.shiftKey && base && !/\p{L}/u.test(event.key)) {
|
|
return base.codePointAt(0) ?? null;
|
|
}
|
|
const lower = event.key.toLocaleLowerCase();
|
|
if (Array.from(lower).length === 1) return lower.codePointAt(0) ?? value;
|
|
return value;
|
|
}
|
|
return base?.codePointAt(0) ?? null;
|
|
};
|
|
|
|
const textCodePoints = (text: string): number[] => (
|
|
Array.from(text, (character) => character.codePointAt(0)!)
|
|
.filter((code) => code >= 0x20 && !(code >= 0x7f && code <= 0x9f))
|
|
);
|
|
|
|
const associatedTextForEvent = (
|
|
event: KittyKeyboardEvent,
|
|
functional: boolean,
|
|
): number[] => {
|
|
if (
|
|
event.type === "keyup" ||
|
|
(event.ctrlKey && !modifierState(event, "AltGraph")) ||
|
|
event.metaKey ||
|
|
modifierState(event, "Hyper") ||
|
|
modifierState(event, "KittyMeta") ||
|
|
MODIFIER_KEYS.has(event.key) ||
|
|
NON_TEXT_DOM_KEYS.has(event.key) ||
|
|
(functional && (
|
|
event.code?.startsWith("Numpad") !== true ||
|
|
Array.from(event.key).length !== 1
|
|
))
|
|
) return [];
|
|
return textCodePoints(event.key);
|
|
};
|
|
|
|
const alternateKeySuffix = (
|
|
event: KittyKeyboardEvent,
|
|
flags: number,
|
|
functional: boolean,
|
|
keyCode: number,
|
|
): string => {
|
|
if (!(flags & KITTY_KEYBOARD_REPORT_ALTERNATE_KEYS) || functional) return "";
|
|
const base = getBaseLayoutCharacter(event)?.codePointAt(0);
|
|
const shifted = event.shiftKey && Array.from(event.key).length === 1
|
|
? event.key.codePointAt(0)
|
|
: undefined;
|
|
const alternateShifted = shifted !== keyCode ? shifted : undefined;
|
|
const alternateBase = base !== keyCode ? base : undefined;
|
|
if (alternateShifted !== undefined && alternateBase !== undefined) {
|
|
return `:${alternateShifted}:${alternateBase}`;
|
|
}
|
|
if (alternateShifted !== undefined) return `:${alternateShifted}`;
|
|
if (alternateBase !== undefined) return `::${alternateBase}`;
|
|
return "";
|
|
};
|
|
|
|
const encodeCsiU = (
|
|
event: KittyKeyboardEvent,
|
|
keyCode: number,
|
|
flags: number,
|
|
functional: boolean,
|
|
): string => {
|
|
const eventType = getEventType(event);
|
|
const reportAll = (flags & KITTY_KEYBOARD_REPORT_ALL_KEYS_AS_ESC_CODES) !== 0;
|
|
const modifiers = getKittyModifierValue(event, flags !== 0);
|
|
const reportEvents = (flags & KITTY_KEYBOARD_REPORT_EVENT_TYPES) !== 0;
|
|
const text = reportAll &&
|
|
(flags & KITTY_KEYBOARD_REPORT_ASSOCIATED_TEXT)
|
|
? associatedTextForEvent(event, functional)
|
|
: [];
|
|
|
|
let sequence = `\u001b[${keyCode}${alternateKeySuffix(event, flags, functional, keyCode)}`;
|
|
const includeEvent = reportEvents && eventType !== 1;
|
|
if (modifiers !== 1 || includeEvent || text.length > 0) {
|
|
sequence += `;${modifiers === 1 && !includeEvent && text.length > 0 ? "" : modifiers}`;
|
|
if (includeEvent) sequence += `:${eventType}`;
|
|
}
|
|
if (text.length > 0) sequence += `;${text.join(":")}`;
|
|
return `${sequence}u`;
|
|
};
|
|
|
|
const encodeLegacyFunctional = (
|
|
definition: Exclude<KittyKeyDefinition, { kind: "csi-u" }>,
|
|
modifiers: number,
|
|
eventType: 1 | 2 | 3,
|
|
reportEvents: boolean,
|
|
legacyMode: boolean,
|
|
applicationCursorMode: boolean,
|
|
): string => {
|
|
const eventSuffix = reportEvents && eventType !== 1 ? `:${eventType}` : "";
|
|
const modifierField = modifiers !== 1 || eventSuffix ? `;${modifiers}${eventSuffix}` : "";
|
|
if (definition.kind === "csi-letter") {
|
|
if (modifierField) return `\u001b[1${modifierField}${definition.code}`;
|
|
return `\u001b${legacyMode && applicationCursorMode ? "O" : "["}${definition.code}`;
|
|
}
|
|
if (definition.kind === "ss3") {
|
|
if (definition.modifiedTildeCode !== undefined && (modifierField || !legacyMode)) {
|
|
return `\u001b[${definition.modifiedTildeCode}${modifierField}~`;
|
|
}
|
|
return modifierField
|
|
? `\u001b[1${modifierField}${definition.code}`
|
|
: `\u001b${legacyMode ? "O" : "["}${definition.code}`;
|
|
}
|
|
return `\u001b[${definition.code}${modifierField}~`;
|
|
};
|
|
|
|
const shouldDisambiguateTextKey = (event: KittyKeyboardEvent): boolean => {
|
|
if (isPhysicalDeadAltShortcut(event)) return true;
|
|
if (event.key === "Escape") return true;
|
|
if (Array.from(event.key).length !== 1) return false;
|
|
if (modifierState(event, "AltGraph") || event.altKeyProducesText) return false;
|
|
if (event.altKey || event.ctrlKey || event.metaKey) return true;
|
|
return false;
|
|
};
|
|
|
|
export const shouldEncodeKittyCompositionText = (state: KittyKeyboardModeState): boolean => {
|
|
const flags = getKittyKeyboardModeFlags(state);
|
|
return (flags & KITTY_KEYBOARD_REPORT_ALL_KEYS_AS_ESC_CODES) !== 0;
|
|
};
|
|
|
|
export const encodeKittyCompositionText = (
|
|
state: KittyKeyboardModeState,
|
|
text: string,
|
|
): string | null => {
|
|
const flags = getKittyKeyboardModeFlags(state);
|
|
if (!(flags & KITTY_KEYBOARD_REPORT_ALL_KEYS_AS_ESC_CODES)) return null;
|
|
// Without associated text, CSI 0 u is unidentified and drops the glyph.
|
|
// Callers fall back to the literal committed text instead.
|
|
if (!(flags & KITTY_KEYBOARD_REPORT_ASSOCIATED_TEXT)) return null;
|
|
const points = textCodePoints(text);
|
|
return points.length > 0 ? `\u001b[0;;${points.join(":")}u` : null;
|
|
};
|
|
|
|
export const encodeKittyKeyEvent = (
|
|
state: KittyKeyboardModeState,
|
|
event: KittyKeyboardEvent,
|
|
): string | null => {
|
|
const flags = getKittyKeyboardModeFlags(state);
|
|
if (event.type === "keypress" || shouldDeferKittyKeyEvent(event)) {
|
|
return null;
|
|
}
|
|
|
|
const eventType = getEventType(event);
|
|
const reportEvents = (flags & KITTY_KEYBOARD_REPORT_EVENT_TYPES) !== 0;
|
|
const reportAll = (flags & KITTY_KEYBOARD_REPORT_ALL_KEYS_AS_ESC_CODES) !== 0;
|
|
const disambiguate = (flags & KITTY_KEYBOARD_DISAMBIGUATE_ESC_CODES) !== 0;
|
|
const baselineMode = (
|
|
flags & (
|
|
KITTY_KEYBOARD_DISAMBIGUATE_ESC_CODES |
|
|
KITTY_KEYBOARD_REPORT_EVENT_TYPES |
|
|
KITTY_KEYBOARD_REPORT_ALL_KEYS_AS_ESC_CODES
|
|
)
|
|
) === 0;
|
|
const isNumpad = event.code?.startsWith("Numpad") === true;
|
|
const altGraph = modifierState(event, "AltGraph");
|
|
const textProducingNumpad =
|
|
isNumpad &&
|
|
Array.from(event.key).length === 1 &&
|
|
(!event.ctrlKey || altGraph) &&
|
|
!event.metaKey &&
|
|
!modifierState(event, "Hyper") &&
|
|
!modifierState(event, "KittyMeta") &&
|
|
(!event.altKey || altGraph || event.altKeyProducesText);
|
|
const numpadBegin = event.key === "Clear" && event.code === "Numpad5";
|
|
const definition: KittyKeyDefinition | null =
|
|
numpadBegin && reportEvents && !disambiguate && !reportAll
|
|
? { kind: "csi-letter", code: "E" }
|
|
: getKeyDefinition(
|
|
event,
|
|
!isNumpad || reportAll || (disambiguate && !textProducingNumpad),
|
|
);
|
|
const modifierOrLock = MODIFIER_KEYS.has(event.key) || LOCK_KEYS.has(event.key);
|
|
const hasLockModifier =
|
|
getKittyModifierValue(event) !== getKittyModifierValue(event, false);
|
|
const unicodeKeyCode = definition === null ? getUnicodeKeyCode(event) : null;
|
|
const alternateNonTextEvent =
|
|
unicodeKeyCode !== null &&
|
|
!altGraph &&
|
|
!event.altKeyProducesText &&
|
|
(
|
|
event.ctrlKey ||
|
|
event.altKey ||
|
|
event.metaKey ||
|
|
modifierState(event, "Hyper") ||
|
|
modifierState(event, "KittyMeta")
|
|
) &&
|
|
alternateKeySuffix(event, flags, false, unicodeKeyCode) !== "";
|
|
|
|
const legacyPressMode =
|
|
(baselineMode && eventType !== 3) ||
|
|
(reportEvents && eventType === 1 && !disambiguate && !reportAll);
|
|
if (
|
|
legacyPressMode &&
|
|
!alternateNonTextEvent &&
|
|
!(flags !== 0 && hasLockModifier) &&
|
|
hasLayoutSpecificLegacyCtrlMapping(event)
|
|
) {
|
|
return encodeLegacyKeyboardEvent(event, event.applicationCursorMode, flags !== 0);
|
|
}
|
|
|
|
if (
|
|
(baselineMode || (reportEvents && eventType === 1 && !disambiguate && !reportAll)) &&
|
|
!alternateNonTextEvent &&
|
|
!(flags !== 0 && hasLockModifier) &&
|
|
isLegacyCtrlAsciiPassthrough(event)
|
|
) {
|
|
return encodeLegacyKeyboardEvent(event, event.applicationCursorMode);
|
|
}
|
|
|
|
if (
|
|
baselineMode &&
|
|
!alternateNonTextEvent &&
|
|
event.ctrlKey &&
|
|
event.shiftKey &&
|
|
!event.altKey &&
|
|
!event.metaKey &&
|
|
!(flags !== 0 && hasLockModifier) &&
|
|
!modifierState(event, "Hyper") &&
|
|
!modifierState(event, "KittyMeta")
|
|
) {
|
|
const legacyControl = encodeLegacyKeyboardEvent(event, event.applicationCursorMode);
|
|
if (legacyControl !== null) return legacyControl;
|
|
}
|
|
if (
|
|
flags === KITTY_KEYBOARD_REPORT_EVENT_TYPES &&
|
|
eventType === 1 &&
|
|
event.key === " " &&
|
|
event.code === "Space" &&
|
|
event.ctrlKey &&
|
|
event.shiftKey &&
|
|
!event.altKey &&
|
|
!event.metaKey &&
|
|
!hasLockModifier &&
|
|
!modifierState(event, "Hyper") &&
|
|
!modifierState(event, "KittyMeta")
|
|
) {
|
|
return encodeLegacyKeyboardEvent(event, event.applicationCursorMode, flags !== 0);
|
|
}
|
|
if (
|
|
baselineMode &&
|
|
eventType !== 3 &&
|
|
(event.key === "ContextMenu" || numpadBegin)
|
|
) {
|
|
return encodeLegacyKeyboardEvent(event, event.applicationCursorMode, flags !== 0);
|
|
}
|
|
|
|
const hasLegacyFunctionalModifier = getKittyModifierValue(event, false) !== 1;
|
|
if (
|
|
baselineMode &&
|
|
eventType !== 3 &&
|
|
definition !== null &&
|
|
definition.kind !== "csi-u" &&
|
|
hasLegacyFunctionalModifier
|
|
) {
|
|
return encodeLegacyKeyboardEvent(event, event.applicationCursorMode, flags !== 0);
|
|
}
|
|
|
|
const legacyC0Special =
|
|
event.key === "Escape" ||
|
|
LEGACY_C0_KEYS.has(event.key) ||
|
|
(event.key === " " && event.code === "Space");
|
|
const lockSensitiveSpace =
|
|
flags !== 0 &&
|
|
hasLockModifier &&
|
|
event.key === " " &&
|
|
event.code === "Space";
|
|
const supportedLegacyC0Modifiers =
|
|
!event.metaKey &&
|
|
!modifierState(event, "Hyper") &&
|
|
!modifierState(event, "KittyMeta") &&
|
|
!modifierState(event, "AltGraph") &&
|
|
!(event.ctrlKey && event.altKey && event.shiftKey);
|
|
if (
|
|
baselineMode &&
|
|
eventType !== 3 &&
|
|
legacyC0Special &&
|
|
!lockSensitiveSpace &&
|
|
supportedLegacyC0Modifiers &&
|
|
(event.ctrlKey || event.altKey || event.shiftKey)
|
|
) {
|
|
return encodeLegacyKeyboardEvent(event, event.applicationCursorMode);
|
|
}
|
|
|
|
// Even before an application opts into enhancement flags, Kitty's baseline
|
|
// protocol uses CSI-u when legacy encoding would lose Shift or a modifier
|
|
// that has no historical terminal representation.
|
|
const baselineDisambiguatedTextEvent =
|
|
alternateNonTextEvent || (
|
|
Array.from(event.key).length === 1 &&
|
|
(
|
|
(
|
|
event.ctrlKey === true &&
|
|
event.shiftKey === true &&
|
|
!altGraph &&
|
|
!event.altKeyProducesText
|
|
) ||
|
|
(
|
|
(event.ctrlKey === true || event.altKey === true) &&
|
|
activeOrBaseLegacyAscii(event) === null
|
|
) ||
|
|
event.metaKey === true ||
|
|
modifierState(event, "Hyper") ||
|
|
modifierState(event, "KittyMeta")
|
|
)
|
|
) || (
|
|
legacyC0Special &&
|
|
hasLegacyFunctionalModifier &&
|
|
!supportedLegacyC0Modifiers
|
|
) || (
|
|
flags !== 0 &&
|
|
hasLockModifier &&
|
|
Array.from(event.key).length === 1 &&
|
|
!modifierState(event, "AltGraph") &&
|
|
!event.altKeyProducesText &&
|
|
(
|
|
event.ctrlKey ||
|
|
event.altKey ||
|
|
event.metaKey ||
|
|
modifierState(event, "Hyper") ||
|
|
modifierState(event, "KittyMeta")
|
|
)
|
|
);
|
|
const baselineFunctionalEvent =
|
|
baselineMode &&
|
|
eventType !== 3 &&
|
|
definition?.kind === "csi-u" &&
|
|
event.key !== "Escape" &&
|
|
!LEGACY_C0_KEYS.has(event.key) &&
|
|
!modifierOrLock;
|
|
const enhancedLockFunctionalEvent =
|
|
flags !== 0 && definition !== null && hasLockModifier;
|
|
if (
|
|
baselineMode &&
|
|
!baselineDisambiguatedTextEvent &&
|
|
!baselineFunctionalEvent &&
|
|
!enhancedLockFunctionalEvent
|
|
) return null;
|
|
if (eventType === 3 && !reportEvents) return null;
|
|
|
|
// This intentionally matches Kitty's reference encoder: with flag 0b10,
|
|
// printable press/repeat events can still travel through the text path, but
|
|
// their release has no text payload and is reported as a CSI-u event. Flag
|
|
// 0b1000 additionally moves the printable press/repeat events to CSI-u.
|
|
const reportableTextEvent =
|
|
reportEvents && eventType === 3 && !LEGACY_C0_KEYS.has(event.key);
|
|
const disambiguatedTextEvent = baselineDisambiguatedTextEvent || (
|
|
shouldDisambiguateTextKey(event) && (
|
|
disambiguate || (reportEvents && eventType !== 1)
|
|
)
|
|
) || reportableTextEvent;
|
|
if (modifierOrLock && !reportAll) return null;
|
|
if (eventType === 3 && LEGACY_C0_KEYS.has(event.key) && !reportAll) return null;
|
|
if (eventType === 3 && !definition && !reportAll && !disambiguatedTextEvent) return null;
|
|
|
|
if (definition && definition.kind !== "csi-u") {
|
|
if (
|
|
eventType === 1 &&
|
|
!reportAll &&
|
|
!disambiguate &&
|
|
!reportEvents &&
|
|
!baselineFunctionalEvent &&
|
|
!hasLockModifier
|
|
) return null;
|
|
return encodeLegacyFunctional(
|
|
definition,
|
|
getKittyModifierValue(event),
|
|
eventType,
|
|
reportEvents,
|
|
baselineMode,
|
|
event.applicationCursorMode === true,
|
|
);
|
|
}
|
|
|
|
const keyCode = definition?.kind === "csi-u" ? definition.code : (
|
|
unicodeKeyCode ?? getUnicodeKeyCode(event)
|
|
);
|
|
if (keyCode === null || keyCode === undefined) return null;
|
|
|
|
if (reportAll) return encodeCsiU(event, keyCode, flags, definition !== null);
|
|
|
|
if (
|
|
definition?.kind === "csi-u" &&
|
|
LEGACY_C0_KEYS.has(event.key) &&
|
|
getKittyModifierValue(event, false) !== 1 &&
|
|
(disambiguate || reportEvents)
|
|
) {
|
|
return encodeCsiU(event, keyCode, flags, true);
|
|
}
|
|
|
|
if (definition?.kind === "csi-u" && event.key === "Escape") {
|
|
if (
|
|
disambiguate ||
|
|
(reportEvents && (
|
|
eventType !== 1 ||
|
|
getKittyModifierValue(event) !== 1
|
|
))
|
|
) {
|
|
return encodeCsiU(event, keyCode, flags, true);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if (
|
|
definition?.kind === "csi-u" &&
|
|
!LEGACY_C0_KEYS.has(event.key) &&
|
|
(disambiguate || reportEvents || baselineFunctionalEvent)
|
|
) {
|
|
return encodeCsiU(event, keyCode, flags, true);
|
|
}
|
|
|
|
if (disambiguatedTextEvent) {
|
|
return encodeCsiU(event, keyCode, flags, false);
|
|
}
|
|
|
|
if (eventType === 3) return encodeCsiU(event, keyCode, flags, definition !== null);
|
|
return null;
|
|
};
|
|
|
|
export const shouldTrackKittyKeyRelease = (
|
|
state: KittyKeyboardModeState,
|
|
event: KittyKeyboardEvent,
|
|
): boolean => encodeKittyKeyEvent(state, {
|
|
...event,
|
|
type: "keyup",
|
|
repeat: false,
|
|
}) !== null;
|
|
|
|
// Compatibility export for callers/tests introduced with the original partial implementation.
|
|
export const encodeKittyControlKey = encodeKittyKeyEvent;
|