source dump of claude code
at main 205 lines 7.3 kB view raw
1import { nonAlphanumericKeys, type ParsedKey } from '../parse-keypress.js' 2import { Event } from './event.js' 3 4export type Key = { 5 upArrow: boolean 6 downArrow: boolean 7 leftArrow: boolean 8 rightArrow: boolean 9 pageDown: boolean 10 pageUp: boolean 11 wheelUp: boolean 12 wheelDown: boolean 13 home: boolean 14 end: boolean 15 return: boolean 16 escape: boolean 17 ctrl: boolean 18 shift: boolean 19 fn: boolean 20 tab: boolean 21 backspace: boolean 22 delete: boolean 23 meta: boolean 24 super: boolean 25} 26 27function parseKey(keypress: ParsedKey): [Key, string] { 28 const key: Key = { 29 upArrow: keypress.name === 'up', 30 downArrow: keypress.name === 'down', 31 leftArrow: keypress.name === 'left', 32 rightArrow: keypress.name === 'right', 33 pageDown: keypress.name === 'pagedown', 34 pageUp: keypress.name === 'pageup', 35 wheelUp: keypress.name === 'wheelup', 36 wheelDown: keypress.name === 'wheeldown', 37 home: keypress.name === 'home', 38 end: keypress.name === 'end', 39 return: keypress.name === 'return', 40 escape: keypress.name === 'escape', 41 fn: keypress.fn, 42 ctrl: keypress.ctrl, 43 shift: keypress.shift, 44 tab: keypress.name === 'tab', 45 backspace: keypress.name === 'backspace', 46 delete: keypress.name === 'delete', 47 // `parseKeypress` parses \u001B\u001B[A (meta + up arrow) as meta = false 48 // but with option = true, so we need to take this into account here 49 // to avoid breaking changes in Ink. 50 // TODO(vadimdemedes): consider removing this in the next major version. 51 meta: keypress.meta || keypress.name === 'escape' || keypress.option, 52 // Super (Cmd on macOS / Win key) — only arrives via kitty keyboard 53 // protocol CSI u sequences. Distinct from meta (Alt/Option) so 54 // bindings like cmd+c can be expressed separately from opt+c. 55 super: keypress.super, 56 } 57 58 let input = keypress.ctrl ? keypress.name : keypress.sequence 59 60 // Handle undefined input case 61 if (input === undefined) { 62 input = '' 63 } 64 65 // When ctrl is set, keypress.name for space is the literal word "space". 66 // Convert to actual space character for consistency with the CSI u branch 67 // (which maps 'space' → ' '). Without this, ctrl+space leaks the literal 68 // word "space" into text input. 69 if (keypress.ctrl && input === 'space') { 70 input = ' ' 71 } 72 73 // Suppress unrecognized escape sequences that were parsed as function keys 74 // (matched by FN_KEY_RE) but have no name in the keyName map. 75 // Examples: ESC[25~ (F13/Right Alt on Windows), ESC[26~ (F14), etc. 76 // Without this, the ESC prefix is stripped below and the remainder (e.g., 77 // "[25~") leaks into the input as literal text. 78 if (keypress.code && !keypress.name) { 79 input = '' 80 } 81 82 // Suppress ESC-less SGR mouse fragments. When a heavy React commit blocks 83 // the event loop past App's 50ms NORMAL_TIMEOUT flush, a CSI split across 84 // stdin chunks gets its buffered ESC flushed as a lone Escape key, and the 85 // continuation arrives as a text token with name='' — which falls through 86 // all of parseKeypress's ESC-anchored regexes and the nonAlphanumericKeys 87 // clear below (name is falsy). The fragment then leaks into the prompt as 88 // literal `[<64;74;16M`. This is the same defensive sink as the F13 guard 89 // above; the underlying tokenizer-flush race is upstream of this layer. 90 if (!keypress.name && /^\[<\d+;\d+;\d+[Mm]/.test(input)) { 91 input = '' 92 } 93 94 // Strip meta if it's still remaining after `parseKeypress` 95 // TODO(vadimdemedes): remove this in the next major version. 96 if (input.startsWith('\u001B')) { 97 input = input.slice(1) 98 } 99 100 // Track whether we've already processed this as a special sequence 101 // that converted input to the key name (CSI u or application keypad mode). 102 // For these, we don't want to clear input with nonAlphanumericKeys check. 103 let processedAsSpecialSequence = false 104 105 // Handle CSI u sequences (Kitty keyboard protocol): after stripping ESC, 106 // we're left with "[codepoint;modifieru" (e.g., "[98;3u" for Alt+b). 107 // Use the parsed key name instead for input handling. Require a digit 108 // after [ — real CSI u is always [<digits>…u, and a bare startsWith('[') 109 // false-matches X10 mouse at row 85 (Cy = 85+32 = 'u'), leaking the 110 // literal text "mouse" into the prompt via processedAsSpecialSequence. 111 if (/^\[\d/.test(input) && input.endsWith('u')) { 112 if (!keypress.name) { 113 // Unmapped Kitty functional key (Caps Lock 57358, F13–F35, KP nav, 114 // bare modifiers, etc.) — keycodeToName() returned undefined. Swallow 115 // so the raw "[57358u" doesn't leak into the prompt. See #38781. 116 input = '' 117 } else { 118 // 'space' → ' '; 'escape' → '' (key.escape carries it; 119 // processedAsSpecialSequence bypasses the nonAlphanumericKeys 120 // clear below, so we must handle it explicitly here); 121 // otherwise use key name. 122 input = 123 keypress.name === 'space' 124 ? ' ' 125 : keypress.name === 'escape' 126 ? '' 127 : keypress.name 128 } 129 processedAsSpecialSequence = true 130 } 131 132 // Handle xterm modifyOtherKeys sequences: after stripping ESC, we're left 133 // with "[27;modifier;keycode~" (e.g., "[27;3;98~" for Alt+b). Same 134 // extraction as CSI u — without this, printable-char keycodes (single-letter 135 // names) skip the nonAlphanumericKeys clear and leak "[27;..." as input. 136 if (input.startsWith('[27;') && input.endsWith('~')) { 137 if (!keypress.name) { 138 // Unmapped modifyOtherKeys keycode — swallow for consistency with 139 // the CSI u handler above. Practically untriggerable today (xterm 140 // modifyOtherKeys only sends ASCII keycodes, all mapped), but 141 // guards against future terminal behavior. 142 input = '' 143 } else { 144 input = 145 keypress.name === 'space' 146 ? ' ' 147 : keypress.name === 'escape' 148 ? '' 149 : keypress.name 150 } 151 processedAsSpecialSequence = true 152 } 153 154 // Handle application keypad mode sequences: after stripping ESC, 155 // we're left with "O<letter>" (e.g., "Op" for numpad 0, "Oy" for numpad 9). 156 // Use the parsed key name (the digit character) for input handling. 157 if ( 158 input.startsWith('O') && 159 input.length === 2 && 160 keypress.name && 161 keypress.name.length === 1 162 ) { 163 input = keypress.name 164 processedAsSpecialSequence = true 165 } 166 167 // Clear input for non-alphanumeric keys (arrows, function keys, etc.) 168 // Skip this for CSI u and application keypad mode sequences since 169 // those were already converted to their proper input characters. 170 if ( 171 !processedAsSpecialSequence && 172 keypress.name && 173 nonAlphanumericKeys.includes(keypress.name) 174 ) { 175 input = '' 176 } 177 178 // Set shift=true for uppercase letters (A-Z) 179 // Must check it's actually a letter, not just any char unchanged by toUpperCase 180 if ( 181 input.length === 1 && 182 typeof input[0] === 'string' && 183 input[0] >= 'A' && 184 input[0] <= 'Z' 185 ) { 186 key.shift = true 187 } 188 189 return [key, input] 190} 191 192export class InputEvent extends Event { 193 readonly keypress: ParsedKey 194 readonly key: Key 195 readonly input: string 196 197 constructor(keypress: ParsedKey) { 198 super() 199 const [key, input] = parseKey(keypress) 200 201 this.keypress = keypress 202 this.key = key 203 this.input = input 204 } 205}