( prevState: KeyParseState, input: Buffer | string | null = '', )
| 211 | } |
| 212 | |
| 213 | export function parseMultipleKeypresses( |
| 214 | prevState: KeyParseState, |
| 215 | input: Buffer | string | null = '', |
| 216 | ): [ParsedInput[], KeyParseState] { |
| 217 | const isFlush = input === null |
| 218 | const inputString = isFlush ? '' : inputToString(input) |
| 219 | |
| 220 | // Get or create tokenizer |
| 221 | const tokenizer = prevState._tokenizer ?? createTokenizer({ x10Mouse: true }) |
| 222 | |
| 223 | // Tokenize the input |
| 224 | const tokens = isFlush ? tokenizer.flush() : tokenizer.feed(inputString) |
| 225 | |
| 226 | // Convert tokens to parsed keys, handling paste mode |
| 227 | const keys: ParsedInput[] = [] |
| 228 | let inPaste = prevState.mode === 'IN_PASTE' |
| 229 | let pasteBuffer = prevState.pasteBuffer |
| 230 | |
| 231 | for (const token of tokens) { |
| 232 | if (token.type === 'sequence') { |
| 233 | if (token.value === PASTE_START) { |
| 234 | inPaste = true |
| 235 | pasteBuffer = '' |
| 236 | } else if (token.value === PASTE_END) { |
| 237 | // Always emit a paste key, even for empty pastes. This allows |
| 238 | // downstream handlers to detect empty pastes (e.g., for clipboard |
| 239 | // image handling on macOS). The paste content may be empty string. |
| 240 | keys.push(createPasteKey(pasteBuffer)) |
| 241 | inPaste = false |
| 242 | pasteBuffer = '' |
| 243 | } else if (inPaste) { |
| 244 | // Sequences inside paste are treated as literal text |
| 245 | pasteBuffer += token.value |
| 246 | } else { |
| 247 | const response = parseTerminalResponse(token.value) |
| 248 | if (response) { |
| 249 | keys.push({ kind: 'response', sequence: token.value, response }) |
| 250 | } else { |
| 251 | const mouse = parseMouseEvent(token.value) |
| 252 | if (mouse) { |
| 253 | keys.push(mouse) |
| 254 | } else { |
| 255 | keys.push(parseKeypress(token.value)) |
| 256 | } |
| 257 | } |
| 258 | } |
| 259 | } else if (token.type === 'text') { |
| 260 | if (inPaste) { |
| 261 | pasteBuffer += token.value |
| 262 | } else if ( |
| 263 | /^\[<\d+;\d+;\d+[Mm]$/.test(token.value) || |
| 264 | /^\[M[\x60-\x7f][\x20-\uffff]{2}$/.test(token.value) |
| 265 | ) { |
| 266 | // Orphaned SGR/X10 mouse tail (fullscreen only — mouse tracking is off |
| 267 | // otherwise). A heavy render blocked the event loop past App's 50ms |
| 268 | // flush timer, so the buffered ESC was flushed as a lone Escape and |
| 269 | // the continuation `[<btn;col;rowM` arrived as text. Re-synthesize |
| 270 | // with the ESC prefix so the scroll event still fires instead of |
no test coverage detected