UNPKG

ink

Version:
493 lines 13.4 kB
// Copied from https://github.com/enquirer/enquirer/blob/36785f3399a41cd61e9d28d1eb9c2fcd73d69b4c/lib/keypress.js import { Buffer } from 'node:buffer'; import { kittyModifiers } from './kitty-keyboard.js'; const metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/; const fnKeyRe = /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/; const keyName = { /* xterm/gnome ESC O letter */ OP: 'f1', OQ: 'f2', OR: 'f3', OS: 'f4', /* xterm/rxvt ESC [ number ~ */ '[11~': 'f1', '[12~': 'f2', '[13~': 'f3', '[14~': 'f4', /* from Cygwin and used in libuv */ '[[A': 'f1', '[[B': 'f2', '[[C': 'f3', '[[D': 'f4', '[[E': 'f5', /* common */ '[15~': 'f5', '[17~': 'f6', '[18~': 'f7', '[19~': 'f8', '[20~': 'f9', '[21~': 'f10', '[23~': 'f11', '[24~': 'f12', /* xterm ESC [ letter */ '[A': 'up', '[B': 'down', '[C': 'right', '[D': 'left', '[E': 'clear', '[F': 'end', '[H': 'home', /* xterm/gnome ESC O letter */ OA: 'up', OB: 'down', OC: 'right', OD: 'left', OE: 'clear', OF: 'end', OH: 'home', /* xterm/rxvt ESC [ number ~ */ '[1~': 'home', '[2~': 'insert', '[3~': 'delete', '[4~': 'end', '[5~': 'pageup', '[6~': 'pagedown', /* putty */ '[[5~': 'pageup', '[[6~': 'pagedown', /* rxvt */ '[7~': 'home', '[8~': 'end', /* rxvt keys with modifiers */ '[a': 'up', '[b': 'down', '[c': 'right', '[d': 'left', '[e': 'clear', '[2$': 'insert', '[3$': 'delete', '[5$': 'pageup', '[6$': 'pagedown', '[7$': 'home', '[8$': 'end', Oa: 'up', Ob: 'down', Oc: 'right', Od: 'left', Oe: 'clear', '[2^': 'insert', '[3^': 'delete', '[5^': 'pageup', '[6^': 'pagedown', '[7^': 'home', '[8^': 'end', /* misc. */ '[Z': 'tab', }; export const nonAlphanumericKeys = [...Object.values(keyName), 'backspace']; const isShiftKey = (code) => { return [ '[a', '[b', '[c', '[d', '[e', '[2$', '[3$', '[5$', '[6$', '[7$', '[8$', '[Z', ].includes(code); }; const isCtrlKey = (code) => { return [ 'Oa', 'Ob', 'Oc', 'Od', 'Oe', '[2^', '[3^', '[5^', '[6^', '[7^', '[8^', ].includes(code); }; // Kitty keyboard protocol: CSI codepoint ; modifiers [: eventType] [; text-as-codepoints] u const kittyKeyRe = /^\x1b\[(\d+)(?:;(\d+)(?::(\d+))?(?:;([\d:]+))?)?u$/; // Kitty-enhanced special keys: CSI number ; modifiers : eventType {letter|~} // These are legacy CSI sequences enhanced with the :eventType field. // Examples: \x1b[1;1:1A (up arrow press), \x1b[3;1:3~ (delete release) const kittySpecialKeyRe = /^\x1b\[(\d+);(\d+):(\d+)([A-Za-z~])$/; // Letter-terminated special key names (CSI 1 ; mods letter) const kittySpecialLetterKeys = { A: 'up', B: 'down', C: 'right', D: 'left', E: 'clear', F: 'end', H: 'home', P: 'f1', Q: 'f2', R: 'f3', S: 'f4', }; // Number-terminated special key names (CSI number ; mods ~) const kittySpecialNumberKeys = { 2: 'insert', 3: 'delete', 5: 'pageup', 6: 'pagedown', 7: 'home', 8: 'end', 11: 'f1', 12: 'f2', 13: 'f3', 14: 'f4', 15: 'f5', 17: 'f6', 18: 'f7', 19: 'f8', 20: 'f9', 21: 'f10', 23: 'f11', 24: 'f12', }; // Map of special codepoints to key names in kitty protocol const kittyCodepointNames = { 27: 'escape', // 13 (return) and 32 (space) are handled before this lookup // in parseKittyKeypress so they can be marked as printable. 9: 'tab', 127: 'delete', 8: 'backspace', 57358: 'capslock', 57359: 'scrolllock', 57360: 'numlock', 57361: 'printscreen', 57362: 'pause', 57363: 'menu', 57376: 'f13', 57377: 'f14', 57378: 'f15', 57379: 'f16', 57380: 'f17', 57381: 'f18', 57382: 'f19', 57383: 'f20', 57384: 'f21', 57385: 'f22', 57386: 'f23', 57387: 'f24', 57388: 'f25', 57389: 'f26', 57390: 'f27', 57391: 'f28', 57392: 'f29', 57393: 'f30', 57394: 'f31', 57395: 'f32', 57396: 'f33', 57397: 'f34', 57398: 'f35', 57399: 'kp0', 57400: 'kp1', 57401: 'kp2', 57402: 'kp3', 57403: 'kp4', 57404: 'kp5', 57405: 'kp6', 57406: 'kp7', 57407: 'kp8', 57408: 'kp9', 57409: 'kpdecimal', 57410: 'kpdivide', 57411: 'kpmultiply', 57412: 'kpsubtract', 57413: 'kpadd', 57414: 'kpenter', 57415: 'kpequal', 57416: 'kpseparator', 57417: 'kpleft', 57418: 'kpright', 57419: 'kpup', 57420: 'kpdown', 57421: 'kppageup', 57422: 'kppagedown', 57423: 'kphome', 57424: 'kpend', 57425: 'kpinsert', 57426: 'kpdelete', 57427: 'kpbegin', 57428: 'mediaplay', 57429: 'mediapause', 57430: 'mediaplaypause', 57431: 'mediareverse', 57432: 'mediastop', 57433: 'mediafastforward', 57434: 'mediarewind', 57435: 'mediatracknext', 57436: 'mediatrackprevious', 57437: 'mediarecord', 57438: 'lowervolume', 57439: 'raisevolume', 57440: 'mutevolume', 57441: 'leftshift', 57442: 'leftcontrol', 57443: 'leftalt', 57444: 'leftsuper', 57445: 'lefthyper', 57446: 'leftmeta', 57447: 'rightshift', 57448: 'rightcontrol', 57449: 'rightalt', 57450: 'rightsuper', 57451: 'righthyper', 57452: 'rightmeta', 57453: 'isoLevel3Shift', 57454: 'isoLevel5Shift', }; // Valid Unicode codepoint range, excluding surrogates const isValidCodepoint = (cp) => cp >= 0 && cp <= 0x10_ffff && !(cp >= 0xd8_00 && cp <= 0xdf_ff); const safeFromCodePoint = (cp) => isValidCodepoint(cp) ? String.fromCodePoint(cp) : '?'; function resolveEventType(value) { if (value === 3) return 'release'; if (value === 2) return 'repeat'; return 'press'; } function parseKittyModifiers(modifiers) { return { ctrl: !!(modifiers & kittyModifiers.ctrl), shift: !!(modifiers & kittyModifiers.shift), meta: !!(modifiers & kittyModifiers.meta), option: !!(modifiers & kittyModifiers.alt), super: !!(modifiers & kittyModifiers.super), hyper: !!(modifiers & kittyModifiers.hyper), capsLock: !!(modifiers & kittyModifiers.capsLock), numLock: !!(modifiers & kittyModifiers.numLock), }; } const parseKittyKeypress = (s) => { const match = kittyKeyRe.exec(s); if (!match) return null; const codepoint = parseInt(match[1], 10); const modifiers = match[2] ? Math.max(0, parseInt(match[2], 10) - 1) : 0; const eventType = match[3] ? parseInt(match[3], 10) : 1; const textField = match[4]; // Bail on invalid primary codepoint if (!isValidCodepoint(codepoint)) { return null; } // Parse text-as-codepoints field (colon-separated Unicode codepoints) let text; if (textField) { text = textField .split(':') .map(cp => safeFromCodePoint(parseInt(cp, 10))) .join(''); } // Determine key name from codepoint let name; let isPrintable; if (codepoint === 32) { name = 'space'; isPrintable = true; } else if (codepoint === 13) { name = 'return'; isPrintable = true; } else if (kittyCodepointNames[codepoint]) { name = kittyCodepointNames[codepoint]; isPrintable = false; } else if (codepoint >= 1 && codepoint <= 26) { // Ctrl+letter comes as codepoint 1-26 name = String.fromCodePoint(codepoint + 96); // 'a' is 97 isPrintable = false; } else { name = safeFromCodePoint(codepoint).toLowerCase(); isPrintable = true; } // Default text to the character from the codepoint when not explicitly // provided by the protocol, so keys like space and return produce their // expected text input (' ' and '\r' respectively). if (isPrintable && !text) { text = safeFromCodePoint(codepoint); } return { name, ...parseKittyModifiers(modifiers), eventType: resolveEventType(eventType), sequence: s, raw: s, isKittyProtocol: true, isPrintable, text, }; }; // Parse kitty-enhanced special key sequences (arrow keys, function keys, etc.) // These use the legacy CSI format but with an added :eventType field. const parseKittySpecialKey = (s) => { const match = kittySpecialKeyRe.exec(s); if (!match) return null; const number = parseInt(match[1], 10); const modifiers = Math.max(0, parseInt(match[2], 10) - 1); const eventType = parseInt(match[3], 10); const terminator = match[4]; const name = terminator === '~' ? kittySpecialNumberKeys[number] : kittySpecialLetterKeys[terminator]; if (!name) return null; return { name, ...parseKittyModifiers(modifiers), eventType: resolveEventType(eventType), sequence: s, raw: s, isKittyProtocol: true, isPrintable: false, }; }; const parseKeypress = (s = '') => { let parts; if (Buffer.isBuffer(s)) { if (s[0] > 127 && s[1] === undefined) { s[0] -= 128; s = '\x1b' + String(s); } else { s = String(s); } } else if (s !== undefined && typeof s !== 'string') { s = String(s); } else if (!s) { s = ''; } // Try kitty keyboard protocol parsers first const kittyResult = parseKittyKeypress(s); if (kittyResult) return kittyResult; const kittySpecialResult = parseKittySpecialKey(s); if (kittySpecialResult) return kittySpecialResult; // If the input matched the kitty CSI-u pattern but was rejected (e.g., // invalid codepoint), return a safe empty keypress instead of falling // through to legacy parsing which can produce unsafe states (undefined name) if (kittyKeyRe.test(s)) { return { name: '', ctrl: false, meta: false, shift: false, option: false, sequence: s, raw: s, isKittyProtocol: true, isPrintable: false, }; } const key = { name: '', ctrl: false, meta: false, shift: false, option: false, sequence: s, raw: s, }; key.sequence = key.sequence || s || key.name; if (s === '\r' || s === '\x1b\r') { // carriage return (or option+return on macOS) key.raw = undefined; key.name = 'return'; key.option = s.length === 2; } else if (s === '\n') { // enter, should have been called linefeed key.name = 'enter'; } else if (s === '\t') { // tab key.name = 'tab'; } else if (s === '\b' || s === '\x1b\b') { // backspace or ctrl+h key.name = 'backspace'; key.meta = s.charAt(0) === '\x1b'; } else if (s === '\x7f' || s === '\x1b\x7f') { // TODO(vadimdemedes): `enquirer` detects delete key as backspace, but I had to split them up to avoid breaking changes in Ink. Merge them back together in the next major version. // delete key.name = 'delete'; key.meta = s.charAt(0) === '\x1b'; } else if (s === '\x1b' || s === '\x1b\x1b') { // escape key key.name = 'escape'; key.meta = s.length === 2; } else if (s === ' ' || s === '\x1b ') { key.name = 'space'; key.meta = s.length === 2; } else if (s.length === 1 && s <= '\x1a') { // ctrl+letter key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); key.ctrl = true; } else if (s.length === 1 && s >= '0' && s <= '9') { // number key.name = 'number'; } else if (s.length === 1 && s >= 'a' && s <= 'z') { // lowercase letter key.name = s; } else if (s.length === 1 && s >= 'A' && s <= 'Z') { // shift+letter key.name = s.toLowerCase(); key.shift = true; } else if ((parts = metaKeyCodeRe.exec(s))) { // meta+character key key.meta = true; key.shift = /^[A-Z]$/.test(parts[1]); } else if ((parts = fnKeyRe.exec(s))) { const segs = [...s]; if (segs[0] === '\u001b' && segs[1] === '\u001b') { key.option = true; } // ansi escape sequence // reassemble the key code leaving out leading \x1b's, // the modifier key bitflag and any meaningless "1;" sequence const code = [parts[1], parts[2], parts[4], parts[6]] .filter(Boolean) .join(''); const modifier = (parts[3] || parts[5] || 1) - 1; // Parse the key modifier key.ctrl = !!(modifier & 4); key.meta = !!(modifier & 10); key.shift = !!(modifier & 1); key.code = code; key.name = keyName[code]; key.shift = isShiftKey(code) || key.shift; key.ctrl = isCtrlKey(code) || key.ctrl; } return key; }; export default parseKeypress; //# sourceMappingURL=parse-keypress.js.map