()
| 418 | this.options.stdout.write('\x1b[?1004h' + (supportsExtendedKeys() ? DISABLE_KITTY_KEYBOARD + ENABLE_KITTY_KEYBOARD + ENABLE_MODIFY_OTHER_KEYS : '')); |
| 419 | } |
| 420 | onRender() { |
| 421 | if (this.isUnmounted || this.isPaused) { |
| 422 | return; |
| 423 | } |
| 424 | // Entering a render cancels any pending drain tick — this render will |
| 425 | // handle the drain (and re-schedule below if needed). Prevents a |
| 426 | // wheel-event-triggered render AND a drain-timer render both firing. |
| 427 | if (this.drainTimer !== null) { |
| 428 | clearTimeout(this.drainTimer); |
| 429 | this.drainTimer = null; |
| 430 | } |
| 431 | |
| 432 | // Flush deferred interaction-time update before rendering so we call |
| 433 | // Date.now() at most once per frame instead of once per keypress. |
| 434 | // Done before the render to avoid dirtying state that would trigger |
| 435 | // an extra React re-render cycle. |
| 436 | flushInteractionTime(); |
| 437 | const renderStart = performance.now(); |
| 438 | const terminalWidth = this.options.stdout.columns || 80; |
| 439 | const terminalRows = this.options.stdout.rows || 24; |
| 440 | const frame = this.renderer({ |
| 441 | frontFrame: this.frontFrame, |
| 442 | backFrame: this.backFrame, |
| 443 | isTTY: this.options.stdout.isTTY, |
| 444 | terminalWidth, |
| 445 | terminalRows, |
| 446 | altScreen: this.altScreenActive, |
| 447 | prevFrameContaminated: this.prevFrameContaminated |
| 448 | }); |
| 449 | const rendererMs = performance.now() - renderStart; |
| 450 | |
| 451 | // Sticky/auto-follow scrolled the ScrollBox this frame. Translate the |
| 452 | // selection by the same delta so the highlight stays anchored to the |
| 453 | // TEXT (native terminal behavior — the selection walks up the screen |
| 454 | // as content scrolls, eventually clipping at the top). frontFrame |
| 455 | // still holds the PREVIOUS frame's screen (swap is at ~500 below), so |
| 456 | // captureScrolledRows reads the rows that are about to scroll out |
| 457 | // before they're overwritten — the text stays copyable until the |
| 458 | // selection scrolls entirely off. During drag, focus tracks the mouse |
| 459 | // (screen-local) so only anchor shifts — selection grows toward the |
| 460 | // mouse as the anchor walks up. After release, both ends are text- |
| 461 | // anchored and move as a block. |
| 462 | const follow = consumeFollowScroll(); |
| 463 | if (follow && this.selection.anchor && |
| 464 | // Only translate if the selection is ON scrollbox content. Selections |
| 465 | // in the footer/prompt/StickyPromptHeader are on static text — the |
| 466 | // scroll doesn't move what's under them. Without this guard, a |
| 467 | // footer selection would be shifted by -delta then clamped to |
| 468 | // viewportBottom, teleporting it into the scrollbox. Mirror the |
| 469 | // bounds check the deleted check() in ScrollKeybindingHandler had. |
| 470 | this.selection.anchor.row >= follow.viewportTop && this.selection.anchor.row <= follow.viewportBottom) { |
| 471 | const { |
| 472 | delta, |
| 473 | viewportTop, |
| 474 | viewportBottom |
| 475 | } = follow; |
| 476 | // captureScrolledRows and shift* are a pair: capture grabs rows about |
| 477 | // to scroll off, shift moves the selection endpoint so the same rows |
no test coverage detected