(app: App, items: ParsedInput[], _unused1: undefined, _unused2: undefined)
| 442 | // Helper to process all keys within a single discrete update context. |
| 443 | // discreteUpdates expects (fn, a, b, c, d) -> fn(a, b, c, d) |
| 444 | function processKeysInBatch(app: App, items: ParsedInput[], _unused1: undefined, _unused2: undefined): void { |
| 445 | // Update interaction time for notification timeout tracking. |
| 446 | // This is called from the central input handler to avoid having multiple |
| 447 | // stdin listeners that can cause race conditions and dropped input. |
| 448 | // Terminal responses (kind: 'response') are automated, not user input. |
| 449 | // Mode-1003 no-button motion is also excluded — passive cursor drift is |
| 450 | // not engagement (would suppress idle notifications + defer housekeeping). |
| 451 | if (items.some(i => i.kind === 'key' || i.kind === 'mouse' && !((i.button & 0x20) !== 0 && (i.button & 0x03) === 3))) { |
| 452 | updateLastInteractionTime(); |
| 453 | } |
| 454 | for (const item of items) { |
| 455 | // Terminal responses (DECRPM, DA1, OSC replies, etc.) are not user |
| 456 | // input — route them to the querier to resolve pending promises. |
| 457 | if (item.kind === 'response') { |
| 458 | app.querier.onResponse(item.response); |
| 459 | continue; |
| 460 | } |
| 461 | |
| 462 | // Mouse click/drag events update selection state (fullscreen only). |
| 463 | // Terminal sends 1-indexed col/row; convert to 0-indexed for the |
| 464 | // screen buffer. Button bit 0x20 = drag (motion while button held). |
| 465 | if (item.kind === 'mouse') { |
| 466 | handleMouseEvent(app, item); |
| 467 | continue; |
| 468 | } |
| 469 | const sequence = item.sequence; |
| 470 | |
| 471 | // Handle terminal focus events (DECSET 1004) |
| 472 | if (sequence === FOCUS_IN) { |
| 473 | app.handleTerminalFocus(true); |
| 474 | const event = new TerminalFocusEvent('terminalfocus'); |
| 475 | app.internal_eventEmitter.emit('terminalfocus', event); |
| 476 | continue; |
| 477 | } |
| 478 | if (sequence === FOCUS_OUT) { |
| 479 | app.handleTerminalFocus(false); |
| 480 | // Defensive: if we lost the release event (mouse released outside |
| 481 | // terminal window — some emulators drop it rather than capturing the |
| 482 | // pointer), focus-out is the next observable signal that the drag is |
| 483 | // over. Without this, drag-to-scroll's timer runs until the scroll |
| 484 | // boundary is hit. |
| 485 | if (app.props.selection.isDragging) { |
| 486 | finishSelection(app.props.selection); |
| 487 | app.props.onSelectionChange(); |
| 488 | } |
| 489 | const event = new TerminalFocusEvent('terminalblur'); |
| 490 | app.internal_eventEmitter.emit('terminalblur', event); |
| 491 | continue; |
| 492 | } |
| 493 | |
| 494 | // Failsafe: if we receive input, the terminal must be focused |
| 495 | if (!getTerminalFocused()) { |
| 496 | setTerminalFocused(true); |
| 497 | } |
| 498 | |
| 499 | // Handle Ctrl+Z (suspend) using parsed key to support both raw (\x1a) and |
| 500 | // CSI u format (\x1b[122;5u) from Kitty keyboard protocol terminals |
| 501 | if (item.name === 'z' && item.ctrl && SUPPORTS_SUSPEND) { |
nothing calls this directly
no test coverage detected