- WT_SESSION leaks into tmux/screen/Zellij panes, so the automatic raw 0x08 -> ctrl+backspace override deleted a word on plain Backspace. - The heuristic now requires isWindowsTerminalSession() and not isInsideTerminalMultiplexer(); PI_TUI_RAW_BACKSPACE_IS_CTRL=1 stays an unconditional opt-in. - Review follow-up for PR #6784.
567 lines
17 KiB
TypeScript
567 lines
17 KiB
TypeScript
/**
|
|
* Keyboard input handling for terminal applications.
|
|
*
|
|
* Supports both legacy terminal sequences and Kitty keyboard protocol.
|
|
* See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
* Reference: https://github.com/sst/opentui/blob/7da92b4088aebfe27b9f691c04163a48821e49fd/packages/core/src/lib/parse.keypress.ts
|
|
*
|
|
* Symbol keys are also supported, however some ctrl+symbol combos
|
|
* overlap with ASCII codes, e.g. ctrl+[ = ESC.
|
|
* See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-ctrl-mapping-of-ascii-keys
|
|
* Those can still be * used for ctrl+shift combos
|
|
*
|
|
* API:
|
|
* - matchesKey(data, keyId) - Check if input matches a key identifier
|
|
* - parseKey(data) - Parse input and return the key identifier
|
|
* - Key - Helper object for creating typed key identifiers
|
|
* - setKittyProtocolActive(active) - Set global Kitty protocol state
|
|
* - isKittyProtocolActive() - Query global Kitty protocol state
|
|
*/
|
|
|
|
import type { KeyEventType } from "@oh-my-pi/pi-natives";
|
|
import {
|
|
matchesKey as matchesKeyNative,
|
|
parseKey as parseKeyNative,
|
|
parseKittySequence as parseKittySequenceNative,
|
|
} from "@oh-my-pi/pi-natives";
|
|
import { isInsideTerminalMultiplexer } from "./terminal-capabilities";
|
|
|
|
// =============================================================================
|
|
// Platform Detection
|
|
// =============================================================================
|
|
|
|
/** Whether the local process is running directly under Windows Terminal. */
|
|
export function isWindowsTerminalSession(): boolean {
|
|
return (
|
|
Boolean(process.env.WT_SESSION) && !process.env.SSH_CONNECTION && !process.env.SSH_CLIENT && !process.env.SSH_TTY
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Match ambiguous legacy Backspace bytes against an expected modifier mask.
|
|
*
|
|
* Windows Terminal encodes Ctrl+Backspace as raw `0x08` (BS) and plain
|
|
* Backspace as `0x7f` (DEL). Remote/container sessions lose terminal identity,
|
|
* and multiplexers (tmux/screen/Zellij) inherit `WT_SESSION` while emitting
|
|
* raw `0x08` for plain Backspace themselves, so the automatic heuristic is
|
|
* limited to direct Windows Terminal sessions. `PI_TUI_RAW_BACKSPACE_IS_CTRL=1`
|
|
* explicitly opts into the mapping everywhere.
|
|
*/
|
|
export function matchesRawBackspace(data: string, expectedModifier: number): boolean {
|
|
if (data === "\x7f") return expectedModifier === 0;
|
|
if (data !== "\x08") return false;
|
|
const rawBackspaceIsCtrl =
|
|
process.env.PI_TUI_RAW_BACKSPACE_IS_CTRL === "1" ||
|
|
(isWindowsTerminalSession() && !isInsideTerminalMultiplexer(process.env));
|
|
return rawBackspaceIsCtrl ? expectedModifier === 4 : expectedModifier === 0;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Global Kitty Protocol State
|
|
// =============================================================================
|
|
|
|
let kittyProtocolActive = false;
|
|
|
|
/**
|
|
* Set the global Kitty keyboard protocol state.
|
|
* Called by ProcessTerminal after detecting protocol support.
|
|
*/
|
|
export function setKittyProtocolActive(active: boolean): void {
|
|
kittyProtocolActive = active;
|
|
}
|
|
|
|
/**
|
|
* Query whether Kitty keyboard protocol is currently active.
|
|
*/
|
|
export function isKittyProtocolActive(): boolean {
|
|
return kittyProtocolActive;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Type-Safe Key Identifiers
|
|
// =============================================================================
|
|
|
|
type Letter =
|
|
| "a"
|
|
| "b"
|
|
| "c"
|
|
| "d"
|
|
| "e"
|
|
| "f"
|
|
| "g"
|
|
| "h"
|
|
| "i"
|
|
| "j"
|
|
| "k"
|
|
| "l"
|
|
| "m"
|
|
| "n"
|
|
| "o"
|
|
| "p"
|
|
| "q"
|
|
| "r"
|
|
| "s"
|
|
| "t"
|
|
| "u"
|
|
| "v"
|
|
| "w"
|
|
| "x"
|
|
| "y"
|
|
| "z";
|
|
|
|
type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
|
|
|
|
type SymbolKey =
|
|
| "`"
|
|
| "-"
|
|
| "="
|
|
| "["
|
|
| "]"
|
|
| "\\"
|
|
| ";"
|
|
| "'"
|
|
| ","
|
|
| "."
|
|
| "/"
|
|
| "!"
|
|
| "@"
|
|
| "#"
|
|
| "$"
|
|
| "%"
|
|
| "^"
|
|
| "&"
|
|
| "*"
|
|
| "("
|
|
| ")"
|
|
| "_"
|
|
| "+"
|
|
| "|"
|
|
| "~"
|
|
| "{"
|
|
| "}"
|
|
| ":"
|
|
| "<"
|
|
| ">"
|
|
| "?";
|
|
|
|
type SpecialKey =
|
|
| "escape"
|
|
| "esc"
|
|
| "enter"
|
|
| "return"
|
|
| "tab"
|
|
| "space"
|
|
| "backspace"
|
|
| "delete"
|
|
| "insert"
|
|
| "clear"
|
|
| "home"
|
|
| "end"
|
|
| "pageUp"
|
|
| "pageDown"
|
|
| "up"
|
|
| "down"
|
|
| "left"
|
|
| "right"
|
|
| "f1"
|
|
| "f2"
|
|
| "f3"
|
|
| "f4"
|
|
| "f5"
|
|
| "f6"
|
|
| "f7"
|
|
| "f8"
|
|
| "f9"
|
|
| "f10"
|
|
| "f11"
|
|
| "f12";
|
|
|
|
type BaseKey = Letter | Digit | SymbolKey | SpecialKey;
|
|
type ModifierName = "ctrl" | "shift" | "alt" | "super";
|
|
|
|
type ModifiedKeyId<Key extends string, RemainingModifiers extends ModifierName = ModifierName> = {
|
|
[M in RemainingModifiers]: `${M}+${Key}` | `${M}+${ModifiedKeyId<Key, Exclude<RemainingModifiers, M>>}`;
|
|
}[RemainingModifiers];
|
|
|
|
/**
|
|
* Union type of all valid key identifiers.
|
|
* Provides autocomplete and catches typos at compile time.
|
|
*/
|
|
export type KeyId = BaseKey | ModifiedKeyId<BaseKey>;
|
|
|
|
/**
|
|
* Typed helper for constructing key identifiers with autocomplete.
|
|
*
|
|
* The runtime values are just the canonical key-name strings (so `Key.enter`
|
|
* is literally `"enter"`); the value of `Key` over a bag of magic strings is
|
|
* that each property is typed to the exact `KeyId` literal it produces and the
|
|
* modifier methods return precisely-typed concatenations (e.g. `Key.ctrl("c")`
|
|
* is `"ctrl+c"`, not just `string`). This mirrors the upstream
|
|
* `@mariozechner/pi-tui` `Key` export verbatim so plugins built against any
|
|
* scope alias (`@mariozechner`, `@earendil-works`, `@oh-my-pi`) keep working
|
|
* once the specifier shim remaps them to this package.
|
|
*/
|
|
export const Key = {
|
|
escape: "escape",
|
|
esc: "esc",
|
|
enter: "enter",
|
|
return: "return",
|
|
tab: "tab",
|
|
space: "space",
|
|
backspace: "backspace",
|
|
delete: "delete",
|
|
insert: "insert",
|
|
clear: "clear",
|
|
home: "home",
|
|
end: "end",
|
|
pageUp: "pageUp",
|
|
pageDown: "pageDown",
|
|
up: "up",
|
|
down: "down",
|
|
left: "left",
|
|
right: "right",
|
|
f1: "f1",
|
|
f2: "f2",
|
|
f3: "f3",
|
|
f4: "f4",
|
|
f5: "f5",
|
|
f6: "f6",
|
|
f7: "f7",
|
|
f8: "f8",
|
|
f9: "f9",
|
|
f10: "f10",
|
|
f11: "f11",
|
|
f12: "f12",
|
|
backtick: "`",
|
|
hyphen: "-",
|
|
equals: "=",
|
|
leftbracket: "[",
|
|
rightbracket: "]",
|
|
backslash: "\\",
|
|
semicolon: ";",
|
|
quote: "'",
|
|
comma: ",",
|
|
period: ".",
|
|
slash: "/",
|
|
exclamation: "!",
|
|
at: "@",
|
|
hash: "#",
|
|
dollar: "$",
|
|
percent: "%",
|
|
caret: "^",
|
|
ampersand: "&",
|
|
asterisk: "*",
|
|
leftparen: "(",
|
|
rightparen: ")",
|
|
underscore: "_",
|
|
plus: "+",
|
|
pipe: "|",
|
|
tilde: "~",
|
|
leftbrace: "{",
|
|
rightbrace: "}",
|
|
colon: ":",
|
|
lessthan: "<",
|
|
greaterthan: ">",
|
|
question: "?",
|
|
ctrl: <K extends BaseKey>(key: K) => `ctrl+${key}` as const,
|
|
shift: <K extends BaseKey>(key: K) => `shift+${key}` as const,
|
|
alt: <K extends BaseKey>(key: K) => `alt+${key}` as const,
|
|
super: <K extends BaseKey>(key: K) => `super+${key}` as const,
|
|
ctrlShift: <K extends BaseKey>(key: K) => `ctrl+shift+${key}` as const,
|
|
shiftCtrl: <K extends BaseKey>(key: K) => `shift+ctrl+${key}` as const,
|
|
ctrlAlt: <K extends BaseKey>(key: K) => `ctrl+alt+${key}` as const,
|
|
altCtrl: <K extends BaseKey>(key: K) => `alt+ctrl+${key}` as const,
|
|
shiftAlt: <K extends BaseKey>(key: K) => `shift+alt+${key}` as const,
|
|
altShift: <K extends BaseKey>(key: K) => `alt+shift+${key}` as const,
|
|
ctrlSuper: <K extends BaseKey>(key: K) => `ctrl+super+${key}` as const,
|
|
superCtrl: <K extends BaseKey>(key: K) => `super+ctrl+${key}` as const,
|
|
shiftSuper: <K extends BaseKey>(key: K) => `shift+super+${key}` as const,
|
|
superShift: <K extends BaseKey>(key: K) => `super+shift+${key}` as const,
|
|
altSuper: <K extends BaseKey>(key: K) => `alt+super+${key}` as const,
|
|
superAlt: <K extends BaseKey>(key: K) => `super+alt+${key}` as const,
|
|
ctrlShiftAlt: <K extends BaseKey>(key: K) => `ctrl+shift+alt+${key}` as const,
|
|
ctrlShiftSuper: <K extends BaseKey>(key: K) => `ctrl+shift+super+${key}` as const,
|
|
} as const;
|
|
|
|
// =============================================================================
|
|
// Kitty Protocol Parsing
|
|
// =============================================================================
|
|
|
|
interface ParsedKittySequence {
|
|
codepoint: number;
|
|
shiftedKey?: number; // Shifted version of the key (when shift is pressed)
|
|
baseLayoutKey?: number; // Key in standard PC-101 layout (for non-Latin layouts)
|
|
modifier: number;
|
|
eventType?: KeyEventType;
|
|
}
|
|
|
|
// Regex for Kitty protocol event type detection
|
|
// Matches CSI sequences with :2 (repeat) or :3 (release) event type
|
|
// Format: \x1b[...;modifier:event_type<terminator> where terminator is u, ~, or A-F/H
|
|
const KITTY_RELEASE_PATTERN = /^\x1b\[[\d:;]*:3[u~ABCDHF]$/;
|
|
const KITTY_REPEAT_PATTERN = /^\x1b\[[\d:;]*:2[u~ABCDHF]$/;
|
|
const KITTY_CSI_U_PATTERN = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?(?:;([\d:]*))?u$/;
|
|
const KITTY_MOD_SHIFT = 1;
|
|
const KITTY_MOD_ALT = 2;
|
|
const KITTY_MOD_CTRL = 4;
|
|
const KITTY_MOD_SUPER = 8;
|
|
const KITTY_MOD_NUM_LOCK = 128;
|
|
const KITTY_LOCK_MASK = 64 + KITTY_MOD_NUM_LOCK; // Caps Lock + Num Lock
|
|
const MODIFY_OTHER_KEYS_PATTERN = /^\x1b\[27;(\d+);(\d+)~$/;
|
|
const KITTY_KEYPAD_OPERATOR_TEXT: Record<number, string> = {
|
|
57410: "/",
|
|
57411: "*",
|
|
57412: "-",
|
|
57413: "+",
|
|
57415: "=",
|
|
};
|
|
const KITTY_NUMPAD_TEXT: Record<number, string> = {
|
|
57399: "0",
|
|
57400: "1",
|
|
57401: "2",
|
|
57402: "3",
|
|
57403: "4",
|
|
57404: "5",
|
|
57405: "6",
|
|
57406: "7",
|
|
57407: "8",
|
|
57408: "9",
|
|
57409: ".",
|
|
};
|
|
|
|
/**
|
|
* Check if the input is a key release event.
|
|
* Only meaningful when Kitty keyboard protocol with flag 2 is active.
|
|
* Returns false if Kitty protocol is not active.
|
|
*/
|
|
export function isKeyRelease(data: string): boolean {
|
|
// Only detect release events when Kitty protocol is active
|
|
if (!kittyProtocolActive) {
|
|
return false;
|
|
}
|
|
|
|
// Don't treat bracketed paste content as key release
|
|
if (data.includes("\x1b[200~")) {
|
|
return false;
|
|
}
|
|
|
|
// Match the full CSI sequence pattern for release events
|
|
return KITTY_RELEASE_PATTERN.test(data);
|
|
}
|
|
|
|
/**
|
|
* Check if the input is a key repeat event.
|
|
* Only meaningful when Kitty keyboard protocol with flag 2 is active.
|
|
* Returns false if Kitty protocol is not active.
|
|
*/
|
|
export function isKeyRepeat(data: string): boolean {
|
|
// Only detect repeat events when Kitty protocol is active
|
|
if (!kittyProtocolActive) {
|
|
return false;
|
|
}
|
|
|
|
// Don't treat bracketed paste content as key repeat
|
|
if (data.includes("\x1b[200~")) {
|
|
return false;
|
|
}
|
|
|
|
// Match the full CSI sequence pattern for repeat events
|
|
return KITTY_REPEAT_PATTERN.test(data);
|
|
}
|
|
|
|
export function parseKittySequence(data: string): ParsedKittySequence | null {
|
|
const result = parseKittySequenceNative(data);
|
|
if (!result) return null;
|
|
return {
|
|
codepoint: result.codepoint,
|
|
shiftedKey: result.shiftedKey ?? undefined,
|
|
baseLayoutKey: result.baseLayoutKey ?? undefined,
|
|
modifier: result.modifier,
|
|
eventType: result.eventType,
|
|
};
|
|
}
|
|
|
|
function hasControlChars(data: string): boolean {
|
|
return [...data].some(ch => {
|
|
const code = ch.charCodeAt(0);
|
|
return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
|
|
});
|
|
}
|
|
|
|
function decodeKittyPrintable(data: string): string | undefined {
|
|
const match = data.match(KITTY_CSI_U_PATTERN);
|
|
if (!match) return undefined;
|
|
|
|
const codepoint = Number.parseInt(match[1] ?? "", 10);
|
|
if (!Number.isFinite(codepoint)) return undefined;
|
|
|
|
if (match[5] === "3") return undefined;
|
|
|
|
const shiftedKey = match[2] && match[2].length > 0 ? Number.parseInt(match[2], 10) : undefined;
|
|
const modValue = match[4] ? Number.parseInt(match[4], 10) : 1;
|
|
const modifier = Number.isFinite(modValue) ? modValue - 1 : 0;
|
|
const effectiveMod = modifier & ~KITTY_LOCK_MASK;
|
|
const supportedModifierMask = KITTY_MOD_SHIFT | KITTY_MOD_ALT | KITTY_MOD_CTRL | KITTY_MOD_SUPER;
|
|
|
|
if (effectiveMod & ~supportedModifierMask) return undefined;
|
|
if (effectiveMod & (KITTY_MOD_ALT | KITTY_MOD_CTRL | KITTY_MOD_SUPER)) return undefined;
|
|
|
|
const textField = match[6];
|
|
if (textField && textField.length > 0) {
|
|
const codepoints = textField
|
|
.split(":")
|
|
.filter(Boolean)
|
|
.map(value => Number.parseInt(value, 10))
|
|
.filter(value => Number.isFinite(value) && value >= 32 && value !== 127);
|
|
if (codepoints.length > 0) {
|
|
try {
|
|
return String.fromCodePoint(...codepoints);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|
|
const keypadOperatorText = KITTY_KEYPAD_OPERATOR_TEXT[codepoint];
|
|
if (keypadOperatorText) return keypadOperatorText;
|
|
|
|
if (effectiveMod === 0) {
|
|
const numpadText = KITTY_NUMPAD_TEXT[codepoint];
|
|
if (numpadText) return numpadText;
|
|
}
|
|
|
|
let effectiveCodepoint = codepoint;
|
|
if (effectiveMod & KITTY_MOD_SHIFT && typeof shiftedKey === "number") {
|
|
effectiveCodepoint = shiftedKey;
|
|
}
|
|
|
|
if (effectiveCodepoint >= 0xe000 && effectiveCodepoint <= 0xf8ff) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32 || effectiveCodepoint === 127) return undefined;
|
|
|
|
try {
|
|
return String.fromCodePoint(effectiveCodepoint);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract printable text from raw terminal input.
|
|
*
|
|
* Handles Kitty CSI-u text-producing keys so text-entry components can treat
|
|
* keypad digits, keypad operators, and shifted symbols the same as direct character input.
|
|
*/
|
|
export function extractPrintableText(data: string): string | undefined {
|
|
const printable = decodePrintableKey(data);
|
|
if (printable !== undefined) return printable;
|
|
if (data.length === 0 || hasControlChars(data)) return undefined;
|
|
return data;
|
|
}
|
|
|
|
interface ParsedModifyOtherKeysSequence {
|
|
codepoint: number;
|
|
modifier: number;
|
|
}
|
|
|
|
/**
|
|
* Parse an xterm `modifyOtherKeys` format sequence: `CSI 27 ; modifiers ; keycode ~`.
|
|
* Modifier values are 1-indexed in the wire format; we normalize to a 0-based bitmask.
|
|
*/
|
|
function parseModifyOtherKeysSequence(data: string): ParsedModifyOtherKeysSequence | null {
|
|
const match = data.match(MODIFY_OTHER_KEYS_PATTERN);
|
|
if (!match) return null;
|
|
const modValue = Number.parseInt(match[1] ?? "", 10);
|
|
const codepoint = Number.parseInt(match[2] ?? "", 10);
|
|
if (!Number.isFinite(modValue) || !Number.isFinite(codepoint)) return null;
|
|
return { codepoint, modifier: modValue - 1 };
|
|
}
|
|
|
|
/**
|
|
* Decode an xterm modifyOtherKeys sequence into the printable character it represents.
|
|
*
|
|
* Only sequences with no modifiers or Shift alone produce text; Ctrl/Alt/Super combos
|
|
* are treated as bindings, not text input.
|
|
*/
|
|
function decodeModifyOtherKeysPrintable(data: string): string | undefined {
|
|
const parsed = parseModifyOtherKeysSequence(data);
|
|
if (!parsed) return undefined;
|
|
const modifier = parsed.modifier & ~KITTY_LOCK_MASK;
|
|
if ((modifier & ~KITTY_MOD_SHIFT) !== 0) return undefined;
|
|
if (!Number.isFinite(parsed.codepoint) || parsed.codepoint < 32 || parsed.codepoint === 127) return undefined;
|
|
try {
|
|
return String.fromCodePoint(parsed.codepoint);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decode terminal input into the printable character it represents.
|
|
*
|
|
* Tries Kitty CSI-u first, then falls back to xterm modifyOtherKeys. Returns
|
|
* undefined for control sequences and modifier-only events.
|
|
*/
|
|
export function decodePrintableKey(data: string): string | undefined {
|
|
return decodeKittyPrintable(data) ?? decodeModifyOtherKeysPrintable(data);
|
|
}
|
|
|
|
/**
|
|
* Decode a Kitty CSI-u keypad sequence (numpad digits / keypad operators) into the
|
|
* text it produces, or `undefined` for any non-keypad sequence.
|
|
*
|
|
* The native key matcher classifies bare numpad codepoints (those without a NumLock
|
|
* modifier bit) as navigation keys, but terminals such as the VS Code integrated
|
|
* terminal emit those codepoints for real digit input. Restricting the fast path to
|
|
* keypad codepoints keeps canonical named keys (space, backspace, shifted keys, and
|
|
* modifyOtherKeys sequences) flowing through native normalization.
|
|
*/
|
|
function decodeKittyKeypadText(data: string): string | undefined {
|
|
const match = data.match(KITTY_CSI_U_PATTERN);
|
|
if (!match) return undefined;
|
|
const codepoint = Number.parseInt(match[1] ?? "", 10);
|
|
if (!(codepoint in KITTY_NUMPAD_TEXT) && !(codepoint in KITTY_KEYPAD_OPERATOR_TEXT)) return undefined;
|
|
return decodeKittyPrintable(data);
|
|
}
|
|
|
|
function matchesKeypadKey(data: string, keyId: KeyId): boolean | undefined {
|
|
const printable = decodeKittyKeypadText(data);
|
|
if (printable === undefined) return undefined;
|
|
return printable === keyId;
|
|
}
|
|
|
|
/**
|
|
* Match input data against a key identifier string.
|
|
*
|
|
* Supported key identifiers:
|
|
* - Single keys: "escape", "tab", "enter", "backspace", "delete", "home", "end", "space"
|
|
* - Arrow keys: "up", "down", "left", "right"
|
|
* - Ctrl combinations: "ctrl+c", "ctrl+z", etc.
|
|
* - Shift combinations: "shift+tab", "shift+enter"
|
|
* - Alt combinations: "alt+enter", "alt+backspace"
|
|
* - Combined modifiers: "shift+ctrl+p", "ctrl+alt+x"
|
|
*
|
|
* Use the Key helper for autocomplete: Key.ctrl("c"), Key.escape, Key.ctrlShift("p")
|
|
*
|
|
* @param data - Raw input data from terminal
|
|
* @param keyId - Key identifier (e.g., "ctrl+c", "escape", Key.ctrl("c"))
|
|
*/
|
|
export function matchesKey(data: string, keyId: KeyId): boolean {
|
|
if (matchesRawBackspace(data, 4)) return keyId === "ctrl+backspace";
|
|
return matchesKeypadKey(data, keyId) ?? matchesKeyNative(data, keyId, kittyProtocolActive);
|
|
}
|
|
|
|
/**
|
|
* Parse terminal input and return a normalized key identifier.
|
|
*
|
|
* Returns key names like "escape", "ctrl+c", "shift+tab", "alt+enter".
|
|
* Returns undefined if the input is not a recognized key sequence.
|
|
*
|
|
* @param data - Raw input data from terminal
|
|
*/
|
|
export function parseKey(data: string): string | undefined {
|
|
if (matchesRawBackspace(data, 4)) return "ctrl+backspace";
|
|
return decodeKittyKeypadText(data) ?? parseKeyNative(data, kittyProtocolActive) ?? undefined;
|
|
}
|