(
prev: Frame,
next: Frame,
altScreen = false,
decstbmSafe = true,
)
| 121 | } |
| 122 | |
| 123 | render( |
| 124 | prev: Frame, |
| 125 | next: Frame, |
| 126 | altScreen = false, |
| 127 | decstbmSafe = true, |
| 128 | ): Diff { |
| 129 | if (!this.options.isTTY) { |
| 130 | return this.renderFullFrame(next) |
| 131 | } |
| 132 | |
| 133 | const startTime = performance.now() |
| 134 | const stylePool = this.options.stylePool |
| 135 | |
| 136 | // Since we assume the cursor is at the bottom on the screen, we only need |
| 137 | // to clear when the viewport gets shorter (i.e. the cursor position drifts) |
| 138 | // or when it gets thinner (and text wraps). We _could_ figure out how to |
| 139 | // not reset here but that would involve predicting the current layout |
| 140 | // _after_ the viewport change which means calcuating text wrapping. |
| 141 | // Resizing is a rare enough event that it's not practically a big issue. |
| 142 | if ( |
| 143 | next.viewport.height < prev.viewport.height || |
| 144 | (prev.viewport.width !== 0 && next.viewport.width !== prev.viewport.width) |
| 145 | ) { |
| 146 | return fullResetSequence_CAUSES_FLICKER(next, 'resize', stylePool) |
| 147 | } |
| 148 | |
| 149 | // DECSTBM scroll optimization: when a ScrollBox's scrollTop changed, |
| 150 | // shift content with a hardware scroll (CSI top;bot r + CSI n S/T) |
| 151 | // instead of rewriting the whole scroll region. The shiftRows on |
| 152 | // prev.screen simulates the shift so the diff loop below naturally |
| 153 | // finds only the rows that scrolled IN as diffs. prev.screen is |
| 154 | // about to become backFrame (reused next render) so mutation is safe. |
| 155 | // CURSOR_HOME after RESET_SCROLL_REGION is defensive — DECSTBM reset |
| 156 | // homes cursor per spec but terminal implementations vary. |
| 157 | // |
| 158 | // decstbmSafe: caller passes false when the DECSTBM→diff sequence |
| 159 | // can't be made atomic (no DEC 2026 / BSU/ESU). Without atomicity the |
| 160 | // outer terminal renders the intermediate state — region scrolled, |
| 161 | // edge rows not yet painted — a visible vertical jump on every frame |
| 162 | // where scrollTop moves. Falling through to the diff loop writes all |
| 163 | // shifted rows: more bytes, no intermediate state. next.screen from |
| 164 | // render-node-to-output's blit+shift is correct either way. |
| 165 | let scrollPatch: Diff = [] |
| 166 | if (altScreen && next.scrollHint && decstbmSafe) { |
| 167 | const { top, bottom, delta } = next.scrollHint |
| 168 | if ( |
| 169 | top >= 0 && |
| 170 | bottom < prev.screen.height && |
| 171 | bottom < next.screen.height |
| 172 | ) { |
| 173 | shiftRows(prev.screen, top, bottom, delta) |
| 174 | scrollPatch = [ |
| 175 | { |
| 176 | type: 'stdout', |
| 177 | content: |
| 178 | setScrollRegion(top + 1, bottom + 1) + |
| 179 | (delta > 0 ? csiScrollUp(delta) : csiScrollDown(-delta)) + |
| 180 | RESET_SCROLL_REGION + |
nothing calls this directly
no test coverage detected