19 KiB
Core API Reference
Renderer
createCliRenderer(config?)
Creates and initializes the CLI renderer.
import { createCliRenderer, type CliRendererConfig } from "@opentui/core"
const renderer = await createCliRenderer({
targetFps: 30, // Continuous rendering target (default: 30)
maxFps: 60, // Cap immediate re-renders (default: 60)
exitOnCtrlC: true, // Exit process on Ctrl+C
consoleOptions: { // Debug console overlay
position: ConsolePosition.BOTTOM,
sizePercent: 30,
startInDebugMode: false,
},
onDestroy: () => {}, // Cleanup callback
})
Custom stdin/stdout (SSH, PTY, xterm.js)
CliRendererConfig accepts custom streams so the renderer can drive a transport
other than the local terminal. When stdout is not process.stdout, native
frame bytes are routed through an internal NativeSpanFeed.
const renderer = await createCliRenderer({
stdin, // NodeJS.ReadStream (default: process.stdin)
stdout, // NodeJS.WriteStream (default: process.stdout)
width: cols, // Fallback columns for non-TTY / custom stdout
height: rows, // Fallback rows for non-TTY / custom stdout
remote: true, // Treat output as a remote terminal (auto-detects SSH/mosh for process.stdout)
forwardEnvKeys: [], // Local env names forwarded to remote capability detection
exitOnCtrlC: false,
})
// SIGWINCH is only auto-registered for process.stdout — call resize() manually
// when an external terminal reports a new size:
renderer.resize(newCols, newRows)
// Each stdin/stdout object may be owned by one renderer at a time. destroy()
// releases ownership and restores stdout.write. Allow a microtask to flush
// feed-backed bytes before closing the transport:
renderer.destroy()
await new Promise<void>((resolve) => queueMicrotask(resolve))
Size resolution order: stdout.columns/rows → config.width/height → 80x24.
Env overrides: OTUI_OVERRIDE_STDOUT (force stdout routing),
OTUI_USE_ALTERNATE_SCREEN.
A feed-backed custom stdout defaults to remote: true and forwards no local
environment values. Pass only terminal-related names that genuinely describe
the remote terminal in forwardEnvKeys.
CliRenderer Instance
renderer.root // Root renderable node
renderer.width // Terminal width in columns
renderer.height // Terminal height in rows
renderer.keyInput // Keyboard event emitter
renderer.console // Console overlay controller
renderer.start() // Start render loop
renderer.stop() // Stop render loop
renderer.destroy() // Cleanup and exit alternate screen
renderer.requestRender() // Request a re-render
renderer.setCursorStyle(options) // Set cursor style
renderer.setCursorColor(color) // Set cursor color
renderer.setMousePointer(style) // Set mouse pointer shape
Cursor & Mouse Pointer
import { type CursorStyleOptions, type MousePointerStyle } from "@opentui/core"
// Set cursor style (options object)
renderer.setCursorStyle({
style: "block", // "block" | "line" | "underline" | "default"
blinking: true, // Cursor blink
color: RGBA.fromHex("#FF0000"), // Cursor color
cursor: "pointer", // Mouse pointer shape
})
// Set mouse pointer shape (OSC 22)
renderer.setMousePointer("pointer")
// Available: "default" | "pointer" | "text" | "crosshair" | "move" | "not-allowed"
Renderer Events
renderer.on("resize", (width, height) => {}) // Terminal resized
renderer.on("focus", () => {}) // Terminal window gained focus
renderer.on("blur", () => {}) // Terminal window lost focus
renderer.on("theme_mode", (mode) => {}) // "dark" | "light"
renderer.on("capabilities", (caps) => {}) // Terminal capabilities detected
renderer.on("selection", (selection) => {}) // Text selection finished (mouse-up)
renderer.on("destroy", () => {}) // Renderer destroyed
renderer.on("memory:snapshot", (snapshot) => {}) // Memory snapshot
renderer.on("debugOverlay:toggle", () => {}) // Debug overlay toggled
renderer.on("frame", ({ frameId }) => {}) // A frame was committed
renderer.on("focused_renderable", (current, previous) => {}) // Focus moved
renderer.on("render:error", ({ error, renderable }) => {}) // Render pass threw
renderer.on("handler:error", ({ error, event }) => {}) // Mouse handler threw
If neither error event has a listener, OpenTUI logs the error. During a render
error, renderable identifies the node that was executing when available.
Mouse events expose both their original target and bubbling currentTarget.
Scheduler & Idle
await renderer.idle() // Resolves when no render pass/scheduled render is pending
renderer.getSchedulerState() // { isRunning, isRendering, hasScheduledRender }
renderer.resize(width, height) // Apply an external terminal resize
Desktop Notifications (OSC)
Send a terminal notification via OSC 9 / 777 / 99. Returns true only when a
supported protocol was detected.
if (renderer.capabilities?.notifications) {
renderer.triggerNotification("Tests passed", "CI") // (message, title?)
}
tmux requires set -g allow-passthrough on; Zellij uses OSC 99. Env overrides:
OPENTUI_NOTIFICATION_PROTOCOL (osc9/osc777/osc99/none),
OPENTUI_NOTIFICATIONS=0.
Audio
Native playback, streaming, capture, and recording exported from
@opentui/core.
Loaded Sounds
import { Audio } from "@opentui/core"
const audio = Audio.create({ autoStart: false }) // or setupAudio(options?)
audio.on("error", (error, context) => console.error(`${context.action}: ${error.message}`))
const sound = await audio.loadSoundFile("click.wav")
if (sound != null && audio.start()) {
audio.play(sound, { volume: 0.8, pan: 0, loop: false })
}
// Keep the engine alive while playback is active. Stop/dispose it during
// application cleanup, not immediately after play().
// audio.stop()
// audio.dispose()
Key methods: start(), stop(), loadSound(data), loadSoundFile(path),
play(sound, options?), stopVoice(voice), group(name), setGroupVolume(),
setMasterVolume(), listPlaybackDevices(), getStats(), dispose().
AudioPlayOptions: { volume?, pan?, loop?, groupId? } (32 voice slots).
Streaming MP3 or FLAC
Start playback before creating a stream. Setup errors reject the entry method;
later errors are AudioStream events, so attach an error listener immediately.
import { Audio, type AudioStream } from "@opentui/core"
const audio = Audio.create({ autoStart: false })
const abortController = new AbortController()
audio.on("error", (error, context) => console.error(context.action, error))
if (!audio.start()) throw new Error("No playback device")
const stream: AudioStream = await audio.playStreamUrl("https://example.com/radio.mp3", {
format: "mp3", // "mp3" | "flac"
buffer: { capacityMs: 2000, startupMs: 1000, resumeMs: 1000 },
reconnect: { maxRetries: 5 },
signal: abortController.signal,
})
stream.on("error", (error, context) => console.error(context.action, error))
stream.on("metadata", (metadata) => console.log(metadata)) // ICY metadata for URL streams
await stream.closed
Choose the source API by ownership:
| Method | Source policy |
|---|---|
playStream(source, options?) |
One ReadableStream<Uint8Array> or AsyncIterable<Uint8Array> to EOF |
playStreamUrl(url, options?) |
Fetch, content-type validation, ICY metadata, optional reconnect |
playStreamSource(connector, options?) |
Custom connection and per-connection demuxer |
AudioStream exposes state, closed, getStats(), getMetadata(),
setVolume(), setPan(), setGroup(), and dispose(). Use
createIcyStreamDemuxer() for ICY framing on custom transports. Streaming does
not support WAV, AAC, Ogg, Opus, HLS, seeking, or pause.
Input Capture and WAV Recording
Capture is independent of playback. One Audio engine permits one capture
owner at a time.
const capture = await audio.openCapture({ channels: 1, chunkFrames: 2048 })
capture.on("error", (error, context) => console.error(context.action, error))
const consumption = (async () => {
for await (const pcm of capture.readable) {
processFloat32Pcm(pcm, capture.channels)
}
})()
await new Promise((resolve) => setTimeout(resolve, 1000))
capture.stop() // Gracefully drains unread PCM
await consumption
await capture.closed
const recorder = await audio.recordToFile("recording.wav", { channels: 1 })
recorder.on("error", (error, context) => console.error(context.action, error))
await new Promise((resolve) => setTimeout(resolve, 1000))
recorder.stop() // Finalizes and publishes PCM16 WAV
await recorder.closed
openCapture() returns an AudioCaptureStream with readable, state,
getStats(), stop(), dispose(), and closed. recordToFile() returns an
AudioRecorder with the same lifecycle plus filePath and format: "wav".
For polling, use startCapture(), readCaptureFrames(), getCaptureStats(),
and stopCapture(). Device APIs are listCaptureDevices(),
selectCaptureDevice(), and clearCaptureDeviceSelection(). Microphone
permissions and device availability are platform-dependent.
Console Overlay
renderer.console.show() // Show console overlay
renderer.console.hide() // Hide console overlay
renderer.console.toggle() // Toggle visibility/focus
renderer.console.clear() // Clear console contents
Renderables
All renderables extend the base Renderable class and share common properties.
Common Properties
interface CommonProps {
id?: string // Identifier; duplicate IDs are allowed
// Positioning
position?: "relative" | "absolute"
left?: number | string
top?: number | string
right?: number | string
bottom?: number | string
// Dimensions
width?: number | string | "auto"
height?: number | string | "auto"
minWidth?: number
minHeight?: number
maxWidth?: number
maxHeight?: number
// Flexbox
flexDirection?: "row" | "column" | "row-reverse" | "column-reverse"
flexGrow?: number
flexShrink?: number
flexBasis?: number | string
flexWrap?: "nowrap" | "wrap" | "wrap-reverse"
justifyContent?: "flex-start" | "flex-end" | "center" | "space-between" | "space-around" | "space-evenly"
alignItems?: "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
alignSelf?: "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
alignContent?: "flex-start" | "flex-end" | "center" | "stretch" | "space-between" | "space-around"
// Spacing
padding?: number
paddingTop?: number
paddingRight?: number
paddingBottom?: number
paddingLeft?: number
margin?: number
marginTop?: number
marginRight?: number
marginBottom?: number
marginLeft?: number
gap?: number
// Display
visible?: boolean
overflow?: "visible" | "hidden" | "scroll"
zIndex?: number
}
Renderable Methods
renderable.add(child) // Add child renderable
renderable.remove(child) // Remove child renderable
renderable.getRenderable(id) // Find child by ID
renderable.focus() // Focus this renderable
renderable.blur() // Remove focus
renderable.destroy() // Destroy and cleanup
renderable.on(event, handler) // Add event listener
renderable.off(event, handler) // Remove event listener
renderable.emit(event, ...args) // Emit event
TextRenderable
Display styled text content.
import { TextRenderable, TextAttributes, t, bold, fg, underline } from "@opentui/core"
const text = new TextRenderable(renderer, {
id: "text",
content: "Hello World",
fg: "#FFFFFF", // Foreground color
bg: "#000000", // Background color
attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,
selectable: true, // Allow text selection
})
// Styled text with template literals
const styled = new TextRenderable(renderer, {
content: t`${bold("Bold")} and ${fg("#FF0000")(underline("red underlined"))}`,
})
TextAttributes flags:
TextAttributes.BOLDTextAttributes.DIMTextAttributes.ITALICTextAttributes.UNDERLINETextAttributes.BLINKTextAttributes.INVERSETextAttributes.HIDDENTextAttributes.STRIKETHROUGH
createTextAttributes({ bold, italic, underline, dim, blink, inverse, reverse, hidden, strikethrough }) builds the bit mask. reverse is an alias for
inverse.
Box, Input, Select, Tab Select, ScrollBox, ASCII Font
Every component is new <Name>Renderable(renderer, options), composed with
.add(). Full option props for each live in the shared
components references (e.g. Box titles →
containers.md; minLength /
showSelectionIndicator → inputs.md). This section
covers only the Core-specific surface: imperative composition and event
enums.
import { BoxRenderable, TextRenderable } from "@opentui/core"
const box = new BoxRenderable(renderer, { id: "box", border: true, title: "Panel" })
box.add(new TextRenderable(renderer, { content: "Hello" })) // Compose imperatively
box.focus() // Focusable boxes only
Events (Core uses enums; React/Solid use onChange/onSelect props):
import {
InputRenderableEvents,
SelectRenderableEvents,
TabSelectRenderableEvents,
} from "@opentui/core"
input.on(InputRenderableEvents.CHANGE, (value: string) => {})
// ITEM_SELECTED = Enter (confirm selection); SELECTION_CHANGED = arrow keys (browse)
select.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {})
select.on(SelectRenderableEvents.SELECTION_CHANGED, (index, option) => {})
tabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index, option) => {})
The ITEM_SELECTED / SELECTION_CHANGED distinction is identical for Select and
Tab Select. Inputs must be focused to receive keys (input.focus()).
FrameBufferRenderable
Low-level 2D rendering surface.
import { FrameBufferRenderable, RGBA } from "@opentui/core"
const canvas = new FrameBufferRenderable(renderer, {
id: "canvas",
width: 50,
height: 20,
})
// Direct pixel manipulation
canvas.frameBuffer.fillRect(10, 5, 20, 8, RGBA.fromHex("#FF0000"))
canvas.frameBuffer.drawText("Custom", 12, 7, RGBA.fromHex("#FFFFFF"))
canvas.frameBuffer.setCell(x, y, char, fg, bg)
canvas.frameBuffer.drawImage(nativeImage, x, y, width, height)
See Image Component for
NativeImage ownership and terminal graphics protocols.
Constructs (VNode API)
Declarative wrappers that create VNodes instead of direct instances.
import { Text, Box, Input, Select, instantiate, delegate } from "@opentui/core"
// Create VNode tree
const ui = Box(
{ border: true, padding: 1 },
Text({ content: "Hello" }),
Input({ placeholder: "Type here..." }),
)
// Instantiate onto renderer
renderer.root.add(ui)
// Delegate focus to nested element
const form = delegate(
{ focus: "email-input" },
Box(
{},
Text({ content: "Email:" }),
Input({ id: "email-input", placeholder: "you@example.com" }),
),
)
form.focus() // Focuses the input, not the box
Colors (RGBA)
The RGBA class is exported from @opentui/core but works across all frameworks (Core, React, Solid). Use it for programmatic color manipulation.
Creating Colors
import { RGBA, parseColor } from "@opentui/core"
// From hex string (most common)
RGBA.fromHex("#FF0000") // Full hex
RGBA.fromHex("#F00") // Short hex
// From integers (0-255 range)
RGBA.fromInts(255, 0, 0, 255) // r, g, b, a - fully opaque red
RGBA.fromInts(255, 0, 0, 128) // 50% transparent red
RGBA.fromInts(0, 0, 0, 0) // Fully transparent
// From normalized floats (0.0-1.0 range)
RGBA.fromValues(1.0, 0.0, 0.0, 1.0) // Fully opaque red
RGBA.fromValues(0.1, 0.1, 0.1, 0.7) // Dark gray, 70% opaque
RGBA.fromValues(0.0, 0.5, 1.0, 1.0) // Light blue
Common Color Patterns
// Theme colors
const primary = RGBA.fromHex("#7aa2f7") // Tokyo Night blue
const background = RGBA.fromHex("#1a1a2e")
const foreground = RGBA.fromHex("#c0caf5")
const error = RGBA.fromHex("#f7768e")
// Overlays and shadows
const modalOverlay = RGBA.fromValues(0.0, 0.0, 0.0, 0.5) // 50% black
const shadow = RGBA.fromInts(0, 0, 0, 77) // 30% black
// Borders
const activeBorder = RGBA.fromHex("#7aa2f7")
const inactiveBorder = RGBA.fromInts(65, 72, 104, 255)
parseColor Utility
// Accepts multiple formats
parseColor("#FF0000") // Hex string
parseColor("red") // CSS color name
parseColor("transparent") // Special values
parseColor(RGBA.fromHex("#F00")) // Pass-through RGBA objects
When to Use Each Method
| Method | Use When |
|---|---|
fromHex() |
Working with design specs, CSS colors, config files |
fromInts() |
You have 8-bit values (0-255), common in graphics |
fromValues() |
Doing color interpolation, animations, math |
parseColor() |
Accepting user input or config that could be any format |
Using RGBA in React/Solid
// Import from @opentui/core, use in any framework
import { RGBA } from "@opentui/core"
// React or Solid component
function ThemedBox() {
const bg = RGBA.fromHex("#1a1a2e")
const border = RGBA.fromInts(122, 162, 247, 255)
return (
<box backgroundColor={bg} borderColor={border} border>
<text fg={RGBA.fromHex("#c0caf5")}>Works everywhere!</text>
</box>
)
}
Color props in React/Solid accept both string formats ("#FF0000", "red") and RGBA objects.
Keyboard Input
import { type KeyEvent } from "@opentui/core"
renderer.keyInput.on("keypress", (key: KeyEvent) => {
console.log(key.name) // "a", "escape", "f1", etc.
console.log(key.sequence) // Raw escape sequence
console.log(key.ctrl) // Ctrl held
console.log(key.shift) // Shift held
console.log(key.meta) // Alt held
console.log(key.option) // Option held (macOS)
console.log(key.eventType) // "press" | "release" | "repeat"
})
renderer.keyInput.on("paste", (event: PasteEvent) => {
const text = decodePasteBytes(event.bytes)
console.log("Pasted:", text)
})
Animation Timeline
import { Timeline, engine } from "@opentui/core"
const timeline = new Timeline({
duration: 2000,
loop: false,
autoplay: true,
})
timeline.add(
{ width: 0 },
{
width: 50,
duration: 1000,
ease: "outQuad",
onUpdate: (anim) => {
box.width = anim.targets[0].width
},
},
)
engine.attach(renderer)
engine.register(timeline)
Type Exports
import type {
CliRenderer,
CliRendererConfig,
RenderContext,
KeyEvent,
Renderable,
WidthMethod, // "wcwidth" | "unicode" | "unicode-wide"
SelectionOccupancy, // "cell" | "boundary"
SelectionBehavior, // "cell" | "word" | "line"
// ... and more
} from "@opentui/core"