(private readonly options: Options)
| 201 | y: number; |
| 202 | } | null = null; |
| 203 | constructor(private readonly options: Options) { |
| 204 | autoBind(this); |
| 205 | this.exitPromise = new Promise((resolve, reject) => { |
| 206 | this.resolveExitPromise = resolve; |
| 207 | this.rejectExitPromise = reject; |
| 208 | }); |
| 209 | if (this.options.patchConsole) { |
| 210 | this.restoreConsole = this.patchConsole(); |
| 211 | this.restoreStderr = this.patchStderr(); |
| 212 | } |
| 213 | this.terminal = { |
| 214 | stdout: options.stdout, |
| 215 | stderr: options.stderr |
| 216 | }; |
| 217 | this.terminalColumns = options.stdout.columns || 80; |
| 218 | this.terminalRows = options.stdout.rows || 24; |
| 219 | this.altScreenParkPatch = makeAltScreenParkPatch(this.terminalRows); |
| 220 | this.stylePool = new StylePool(); |
| 221 | this.charPool = new CharPool(); |
| 222 | this.hyperlinkPool = new HyperlinkPool(); |
| 223 | this.frontFrame = emptyFrame(this.terminalRows, this.terminalColumns, this.stylePool, this.charPool, this.hyperlinkPool); |
| 224 | this.backFrame = emptyFrame(this.terminalRows, this.terminalColumns, this.stylePool, this.charPool, this.hyperlinkPool); |
| 225 | this.logicalFrontFrame = this.frontFrame; |
| 226 | this.logicalBackFrame = this.backFrame; |
| 227 | this.log = new LogUpdate({ |
| 228 | isTTY: options.stdout.isTTY as boolean | undefined || false, |
| 229 | stylePool: this.stylePool |
| 230 | }); |
| 231 | |
| 232 | // scheduleRender is called from the reconciler's resetAfterCommit, which |
| 233 | // runs BEFORE React's layout phase (ref attach + useLayoutEffect). Any |
| 234 | // state set in layout effects — notably the cursorDeclaration from |
| 235 | // useDeclaredCursor — would lag one commit behind if we rendered |
| 236 | // synchronously. Deferring to a microtask runs onRender after layout |
| 237 | // effects have committed, so the native cursor tracks the caret without |
| 238 | // a one-keystroke lag. Same event-loop tick, so throughput is unchanged. |
| 239 | // Test env uses onImmediateRender (direct onRender, no throttle) so |
| 240 | // existing synchronous lastFrame() tests are unaffected. |
| 241 | const deferredRender = (): void => { |
| 242 | const queuedAt = performance.now(); |
| 243 | queueMicrotask(() => { |
| 244 | // If a direct caller (forceRedraw, pause, resume, drainTimer, etc.) |
| 245 | // already ran onRender after this microtask was queued, skip the |
| 246 | // redundant scheduled render. |
| 247 | if (this.lastRenderAt >= queuedAt) return; |
| 248 | this.onRender(); |
| 249 | }); |
| 250 | }; |
| 251 | this.scheduleRender = throttle(deferredRender, FRAME_INTERVAL_MS, { |
| 252 | leading: true, |
| 253 | trailing: true |
| 254 | }); |
| 255 | |
| 256 | // Ignore last render after unmounting a tree to prevent empty output before exit |
| 257 | this.isUnmounted = false; |
| 258 | |
| 259 | // Unmount when process exits |
| 260 | this.unsubscribeExit = onExit(this.unmount, { |
nothing calls this directly
no test coverage detected