Renderer

The CliRenderer drives OpenTUI. It manages terminal output, handles input events, runs the rendering loop, and provides context for creating renderables.

Creating a renderer

Use the async factory function to create a renderer:

import { createCliRenderer } from "@opentui/core"

const renderer = await createCliRenderer({
  exitOnCtrlC: true,
  targetFps: 30,
})

The factory function:

  1. Loads the native Zig rendering library
  2. Configures terminal settings (mouse, keyboard protocol, and alternate screen)
  3. Returns an initialized CliRenderer instance

Configuration options

OptionTypeDefaultDescription
exitOnCtrlCbooleantrueExit the process when Ctrl+C is pressed
targetFpsnumber30Target frames per second for the render loop
maxFpsnumber60Maximum FPS for immediate re-renders
useMousebooleantrueEnable mouse input and tracking
enableMouseMovementbooleantrueTrack mouse movement (not just clicks)
useAlternateScreenbooleantrueUse terminal alternate screen buffer
backgroundColorColorInputtransparentDefault background color
consoleOptionsConsoleOptions-Options for the built-in console overlay
openConsoleOnErrorbooleantrueAuto-open console when errors occur (dev only)

The root renderable

Every renderer has a root property—a special RootRenderable at the top of the component tree:

import { Box, Text } from "@opentui/core"

// Add components to the root
renderer.root.add(Box({ width: 40, height: 10, borderStyle: "rounded" }, Text({ content: "Hello, OpenTUI!" })))

The root renderable fills the entire terminal and adjusts automatically when you resize the terminal.

Render loop control

The renderer supports several control modes:

Automatic mode (default)

Without calling start(), the renderer re-renders only when the component tree changes:

const renderer = await createCliRenderer()
renderer.root.add(Text({ content: "Static content" })) // Triggers render

Continuous mode

Call start() to run the render loop continuously at the target FPS:

renderer.start() // Start continuous rendering
renderer.stop() // Stop the render loop

Live rendering

For animations, call requestLive() to enable continuous rendering:

// Request live mode (increments internal counter)
renderer.requestLive()

// When animation completes, drop the request
renderer.dropLive()

Multiple components can request animations simultaneously—the renderer stays live until all requests drop.

Pause and suspend

renderer.pause() // Pause rendering (can resume)
renderer.resume() // Resume from paused state

renderer.suspend() // Fully suspend (disables mouse, input, raw mode)
renderer.resume() // Resume from suspended state

Key properties

PropertyTypeDescription
rootRootRenderableRoot of the component tree
widthnumberCurrent render width in columns
heightnumberCurrent render height in rows
consoleTerminalConsoleBuilt-in console overlay
keyInputKeyHandlerKeyboard input handler
isRunningbooleanWhether the render loop is active
isDestroyedbooleanWhether the renderer has been destroyed
currentFocusedRenderableRenderable | nullCurrently focused component

Events

The renderer emits events that you can listen to:

// Terminal resized
renderer.on("resize", (width, height) => {
  console.log(`Terminal size: ${width}x${height}`)
})

// Renderer destroyed
renderer.on("destroy", () => {
  console.log("Renderer destroyed")
})

// Text selection completed
renderer.on("selection", (selection) => {
  console.log("Selected text:", selection.getSelectedText())
})

Cursor control

Control the cursor position and style:

// Position and visibility
renderer.setCursorPosition(10, 5, true)

// Cursor style
renderer.setCursorStyle("block", true) // Blinking block
renderer.setCursorStyle("underline", false) // Steady underline
renderer.setCursorStyle("line", true) // Blinking line

// Cursor color
renderer.setCursorColor(RGBA.fromHex("#FF0000"))

Input handling

Add custom input handlers:

renderer.addInputHandler((sequence) => {
  if (sequence === "\x1b[A") {
    // Up arrow - handle and consume
    return true
  }
  return false // Let other handlers process
})

By default, addInputHandler() appends handlers to the chain, running them after built-in handlers. Use prependInputHandler() to add a handler at the start of the chain, running it before built-in handlers.

Debug overlay

Toggle the debug overlay to show FPS, memory usage, and other stats:

renderer.toggleDebugOverlay()

// Or configure it
import { DebugOverlayCorner } from "@opentui/core"

renderer.configureDebugOverlay({
  enabled: true,
  corner: DebugOverlayCorner.topRight,
})

Cleanup

Always destroy the renderer when you finish to restore terminal state:

renderer.destroy()

Destroying the renderer restores the terminal to its original state, disables mouse tracking, and cleans up resources.

Environment variables

VariableDescription
OTUI_USE_ALTERNATE_SCREENOverride alternate screen setting
OTUI_SHOW_STATSShow debug overlay at startup
OTUI_DEBUGEnable debug input capture
OTUI_NO_NATIVE_RENDERDisable native rendering (for debugging)
OTUI_DUMP_CAPTURESDump captured output when the renderer exits
OTUI_OVERRIDE_STDOUTOverride stdout stream (for debugging)
OTUI_USE_CONSOLEEnable/disable built-in console
SHOW_CONSOLEShow console at startup