Renderer
CliRenderer owns one terminal session, its root renderable, frame scheduling, input parsing, and native output boundary.
Create a renderer#
createCliRenderer() runs asynchronous terminal setup and returns a CliRenderer. The returned renderer has a root property.
import { TextRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: true,
targetFps: 30,
})
renderer.root.add(new TextRenderable(renderer, { content: "Hello" }))The renderer implements RenderContext, so imperative renderables receive the renderer as their first constructor argument. The root always tracks the renderer’s current render width and height.
Read Lifecycle and cleanup before you add application shutdown logic.
Choose a screen mode#
screenMode controls which terminal area OpenTUI owns.
| Mode | Behavior |
|---|---|
"alternate-screen" |
Default. Uses the terminal’s alternate screen and restores the main screen when the renderer exits. |
"main-screen" |
Uses a reserved region on the main screen. This is not an inline renderer. |
"split-footer" |
Uses a footer on the main screen. footerHeight defaults to 12, and OpenTUI caps the effective footer at the terminal height. |
In split-footer mode, renderer.width and renderer.height describe the footer render region. terminalWidth and terminalHeight describe the complete terminal.
externalOutputMode controls application writes through the configured stdout.write path. It does not change renderer frame bytes or stderr.
"passthrough"is the default outside split-footer mode."capture-stdout"is the split-footer default and is valid only in that mode.- Captured writes become ordered scrollback commits above the footer.
Writing to scrollback#
The renderer also supplies structured scrollback writers and surfaces. Use the canonical custom renderables guide for off-screen rendering and the Buffer API for buffer ownership.
Captured writes, writer snapshots, and surface commits wait in one queue until frames present them, in order. Captured stdout is never refused. One writer snapshot or surface commit larger than the Session output capacity divided by 24 bytes (about 349,000 cells by default) throws.
Custom streams#
Set stdin and stdout for an SSH channel, pseudo-terminal, or another transport. Initial dimensions use stdout.columns and stdout.rows, then width and height, then 80 by 24.
Call renderer.requestResize(width, height) when a custom terminal changes size. It debounces requests (100 ms by default, set with the debounceDelay option) and applies only the latest size. If frame output is pending, it waits for that output to finish. In split-footer mode, it skips the debounce.
Call renderer.resize(width, height) to resize at once. It still waits while a frame paints, while frame output is pending, or while split-footer scrollback is pending.
The resize event reports accepted size changes. OpenTUI listens for SIGWINCH only when it uses process.stdout.
Custom stdout uses Session-owned ordered output and defaults to remote: true. These remote custom streams do not forward local terminal environment values by default.
Set forwardEnvKeys only for values that the remote terminal should inherit. Read Terminal capabilities and Environment variables for forwarding rules.
Read How Core uses native for Session output ownership and completion.
Choose a render schedule#
The initial control state is demand-driven. Tree mutations call requestRender() and schedule a one-shot frame.
Call start() for continuous rendering. targetFps controls its steady rate, and maxFps caps immediate extra frames.
renderer.start()
renderer.targetFps = 60
renderer.maxFps = 120
renderer.pause()
renderer.start()pause() stops continuous rendering. It enters EXPLICIT_PAUSED, but later mutations can still request one-shot frames.
Use start() to resume after pause(). requestLive() cannot resume EXPLICIT_PAUSED or EXPLICIT_STOPPED.
Live rendering#
requestLive() and dropLive() are balanced ownership calls for custom loops. The first live request starts an idle renderer. The final drop returns an auto-started renderer to demand-driven mode.
A registered Timeline owns its live request when the timeline engine is attached. Do not call requestLive() for that timeline. See Animation and Timeline.
suspend() releases active terminal input modes and stops rendering. resume() restores the control state that existed before suspension.
Use await renderer.idle() to wait until demand-driven work settles. getSchedulerState() reports running, rendering, and scheduled-work state for diagnostics.
Cooperative frames#
Set nativeSceneWorkBudget to a positive unsigned 32-bit integer to let a frame yield to the event loop during preparation. The budget counts preparation visits, view-preparation entries, and feedback records between yields. Without this option, preparation does not yield.
If a change made while the frame waits at a yield affects layout, preparation restarts once. The rest of that frame runs without yields.
The budget does not limit elapsed time or cells. Yoga, sibling sorting, publication of the prepared tree, painting, and output encoding still run synchronously. Painting runs in one native call and never yields, so no event-loop work can change the scene while native code paints. The scheduler also cannot interrupt an application callback.
A resize can change the renderer size while a frame waits at a yield. Core then cancels that frame attempt and starts a new one at the new size. The new attempt keeps all accepted changes and the side effects of callbacks that already ran. Cancellation does not roll anything back. Output that the Session already queued keeps its order and its completion tracking.
Use the renderer-owned animation APIs rather than process-global animation callbacks when you host multiple sessions.
Read capability state#
Capability detection continues after createCliRenderer() returns. The first snapshot includes environment heuristics, but terminal replies arrive asynchronously.
Most capability fields are booleans. false can mean either unsupported or not detected yet. Treat the capabilities event as the source of updated snapshots.
OpenTUI accepts startup capability replies for five seconds. Snapshots can change several times in that window, and again when OpenTUI processes a later terminal response.
import { CliRenderEvents, type TerminalCapabilities } from "@opentui/core"
renderer.on(CliRenderEvents.CAPABILITIES, (capabilities: TerminalCapabilities) => {
console.log(capabilities.kitty_graphics)
})Use explicit state fields where available. For example, osc52_support distinguishes "unknown", "supported", and "unsupported".
Read Terminal capabilities for detection timing, remote sessions, and overrides.
Subscribe to central events#
Use renderer.on(event, listener) and remove long-lived listeners with off().
| Event | Payload | Meaning |
|---|---|---|
resize |
(width, height) |
The render region changed. |
frame |
{ frameId } |
A frame’s output completed. |
render:error |
{ error, renderable } |
An error stopped a frame. See below. |
handler:error |
{ error, event } |
A mouse handler threw. |
external_output |
CliRendererExternalOutputEvent |
Core queued a split-footer output snapshot. |
focus, blur |
none | The terminal reported window focus or blur. |
focused_renderable |
(current, previous) |
Renderable keyboard focus changed. |
focused_editor |
(current, previous) |
Focus moved to or from an editor renderable. |
theme_mode |
"dark" | "light" |
The detected terminal theme changed. |
palette |
TerminalColors |
A refreshed terminal palette changed. |
capabilities |
TerminalCapabilities |
A capability response changed the snapshot. |
selection |
Selection |
A text selection drag finished. |
debugOverlay:toggle |
boolean |
Debug-overlay visibility changed. |
memory:snapshot |
memory totals | Core collected a configured memory sample. |
destroy |
none | Renderer destruction started its published cleanup stage. |
The focus and blur events describe terminal-window focus. They do not focus or blur a renderable. See Interaction, focus, and selection.
render:error reports an error that stops a frame, such as an error from a hook, a post-process function, or native painting.
renderable is the node whose paint hook threw. It is undefined for other errors, including errors from onUpdate and onResize.
Without a render:error listener, the renderer reports the error through its normal error path.
An error from a setFrameCallback() callback does not stop the frame. Core logs it and does not emit render:error.
Use renderer-owned services#
The renderer exposes several terminal and application services. Their canonical guides define behavior and cleanup:
- Clipboard covers host clipboard access and OSC 52.
- Notifications covers terminal notification protocols.
- Console overlay covers captured logs and overlay behavior.
- Environment variables lists runtime configuration.
- Custom renderables covers frame hooks and custom output.
Next#
Read Renderables to add UI nodes. Read Rendering pipeline for the complete frame order, or How Core uses native for the scene integration.