Agent skill
opentui-operative
OpenTUI terminal UI library reference. Use when working with @opentui/core, terminal UIs, renderables, Yoga layouts, or Zig-native rendering.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/opentui-operative
SKILL.md
OpenTUI Operative
Comprehensive reference for building terminal UIs with OpenTUI (@opentui/core). Source: https://opentui.com/docs/ — all 29 documentation pages.
Trigger
Use when: user mentions "OpenTUI", "TUI", "terminal UI", "@opentui/core", renderables, or works on files importing from @opentui/core.
Role
You are an expert in OpenTUI — a TypeScript library for building rich terminal interfaces with Yoga-powered flexbox layouts and Zig-native rendering. You know every API surface, every gotcha, and every pattern. You write correct OpenTUI code on the first attempt.
1. Quick Start
Requires Bun.
bun add @opentui/core
import { createCliRenderer, Text } from "@opentui/core"
const renderer = await createCliRenderer({ exitOnCtrlC: true })
renderer.root.add(Text({ content: "Hello, OpenTUI!", fg: "#00FF00" }))
Run with bun index.ts. Press Ctrl+C to exit.
2. Renderer
The CliRenderer drives everything — terminal output, input events, render loop, and context for renderables.
Creation
import { createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: true, // default: true
targetFps: 30, // default: 30
})
The factory:
- Loads the native Zig rendering library
- Configures terminal (mouse, keyboard protocol, alternate screen)
- Returns an initialised
CliRenderer
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
exitOnCtrlC |
boolean |
true |
Destroy renderer on Ctrl+C |
exitSignals |
NodeJS.Signals[] |
— | Signals that trigger cleanup |
targetFps |
number |
30 |
Target FPS for render loop |
maxFps |
number |
60 |
Max FPS for immediate re-renders |
useMouse |
boolean |
true |
Enable mouse input/tracking |
autoFocus |
boolean |
true |
Focus nearest focusable on left click |
enableMouseMovement |
boolean |
true |
Track mouse movement (not just clicks) |
useAlternateScreen |
boolean |
true |
Use terminal alternate screen buffer |
consoleOptions |
ConsoleOptions |
— | Built-in console overlay options |
openConsoleOnError |
boolean |
true |
Auto-open console on errors (dev only) |
onDestroy |
() => void |
— | Callback on renderer destruction |
Key Properties
| Property | Type | Description |
|---|---|---|
root |
RootRenderable |
Root of the component tree (fills terminal) |
width |
number |
Current width in columns |
height |
number |
Current height in rows |
console |
TerminalConsole |
Built-in console overlay |
keyInput |
KeyHandler |
Keyboard input handler |
isRunning |
boolean |
Whether render loop is active |
isDestroyed |
boolean |
Whether renderer is destroyed |
currentFocusedRenderable |
Renderable | null |
Currently focused component |
Render Loop Control
Automatic mode (default) — re-renders only when the component tree changes:
const renderer = await createCliRenderer()
renderer.root.add(Text({ content: "Static content" }))
// No start() needed — renders automatically on tree changes
Continuous mode — runs at targetFps:
renderer.start() // Begin continuous rendering
renderer.stop() // Stop continuous rendering
Live rendering — for animations:
renderer.requestLive() // Request continuous rendering
renderer.dropLive() // Drop live rendering request
Pause/Suspend:
renderer.pause()
renderer.suspend()
renderer.resume()
Events
renderer.on("resize", (width, height) => { /* terminal resized */ })
renderer.on("destroy", () => { /* renderer destroyed */ })
renderer.on("selection", (selection) => { /* text selected */ })
Cursor Control
renderer.setCursorPosition(10, 5, true)
renderer.setCursorStyle("block", true) // block | underline | line
renderer.setCursorColor(RGBA.fromHex("#FF0000"))
Cleanup
renderer.destroy()
CRITICAL: Always call destroy() when finished. This restores terminal state (mouse tracking, raw mode, alternate screen). OpenTUI does NOT automatically clean up on process.exit or unhandled errors.
Debug Overlay
renderer.toggleDebugOverlay()
import { DebugOverlayCorner } from "@opentui/core"
renderer.configureDebugOverlay({ enabled: true, corner: DebugOverlayCorner.topRight })
3. Renderables (Imperative API)
Renderables are the building blocks of the UI. Each represents a visual element using Yoga layout engine for positioning.
Creating Renderables
import { TextRenderable, BoxRenderable } from "@opentui/core"
// Constructor: new XxxRenderable(ctx: RenderContext, options)
// ctx IS the renderer itself (or any object implementing RenderContext)
const greeting = new TextRenderable(renderer, {
id: "greeting",
content: "Hello!",
fg: "#00FF00",
})
renderer.root.add(greeting)
Available Renderables
| Class | Description |
|---|---|
BoxRenderable |
Container with border, background, and layout |
TextRenderable |
Read-only styled text display |
InputRenderable |
Single-line text input |
TextareaRenderable |
Multi-line editable text |
SelectRenderable |
Dropdown/list selection |
TabSelectRenderable |
Horizontal tab selection |
ScrollBoxRenderable |
Scrollable container |
ScrollBarRenderable |
Standalone scroll bar control |
CodeRenderable |
Syntax-highlighted code display |
LineNumberRenderable |
Line number gutter |
DiffRenderable |
Unified or split diff viewer |
ASCIIFontRenderable |
ASCII art font display |
FrameBufferRenderable |
Raw framebuffer for custom graphics |
MarkdownRenderable |
Markdown renderer |
SliderRenderable |
Numeric slider control |
The Renderable Tree
const container = new BoxRenderable(renderer, {
id: "container",
flexDirection: "column",
padding: 1,
})
const title = new TextRenderable(renderer, { id: "title", content: "My App" })
const body = new TextRenderable(renderer, { id: "body", content: "Content" })
container.add(title)
container.add(body)
renderer.root.add(container)
// Remove a child — MUST use string ID, not the renderable instance
container.remove("body")
CRITICAL: remove() API
remove(id: string): void — the ONLY signature. Always pass a string ID.
// CORRECT
container.remove("body")
container.remove(child.id) // .id returns the auto-generated or explicit ID
// WRONG — will fail at runtime
container.remove(child) // passes object, not string
Every renderable gets an auto-generated .id from a static counter. If you set id in options, that becomes the ID. Otherwise it's auto-generated. Access via renderable.id.
Finding Renderables
const title = container.getRenderable("title") // Direct child by ID
const deep = container.findDescendantById("nested-input") // Recursive search
const children = container.getChildren() // All children
Visibility
panel.visible = false // Hides AND removes from layout (like CSS display: none)
panel.visible = true
Opacity
panel.opacity = 0.5 // Affects renderable and all children
Z-Index
const overlay = new BoxRenderable(renderer, {
position: "absolute",
zIndex: 100, // Higher values render on top
})
Translation (Visual Offset)
renderable.translateX = 10
renderable.translateY = -5
// Moves visually without affecting layout
Destroying Renderables
renderable.destroy() // Remove from parent, free resources
container.destroyRecursively() // Destroy self and all children
Lifecycle Methods (Custom Renderables)
class CustomRenderable extends Renderable {
onUpdate(deltaTime: number) { /* called each frame before render */ }
onResize(width: number, height: number) { /* dimensions changed */ }
onRemove() { /* removed from parent — cleanup here */ }
renderSelf(buffer: OptimizedBuffer, deltaTime: number) { /* custom drawing */ }
}
Live Rendering
const box = new AnimatedBox(renderer, {
live: true, // Enable continuous rendering for this renderable
})
Buffered Rendering
const complex = new BoxRenderable(renderer, {
buffered: true, // Render to offscreen buffer first
renderAfter: (buffer) => {
buffer.fillRect(0, 0, 10, 5, RGBA.fromHex("#FF0000"))
},
})
4. Constructs (Declarative API)
Factory functions that create VNodes — lightweight descriptions of components. VNodes become actual Renderables when added to the tree.
import { Box, Text, Input } from "@opentui/core"
Box(
{ width: 40, height: 10, borderStyle: "rounded", padding: 1 },
Text({ content: "Welcome!" }),
Input({ placeholder: "Enter your name..." }),
)
Available Constructs
ASCIIFont, Box, Code, FrameBuffer, Input, ScrollBox, Select, TabSelect, Text, SyntaxStyle
NOT yet available as constructs (use Renderable API): Textarea, ScrollBar, Slider, Markdown, LineNumber, Diff
Method Chaining on VNodes
VNodes queue method calls — applied after the component is created:
const input = Input({ placeholder: "Name..." })
input.focus() // Queued, applied when added to tree
Delegation
Routes method/property calls to descendant IDs:
import { delegate } from "@opentui/core"
function LabeledInput(props) {
return delegate(
{ focus: `${props.id}-input` }, // focus() routes to child input
Box(
{ flexDirection: "row" },
Text({ content: props.label }),
Input({ id: `${props.id}-input`, placeholder: props.placeholder }),
),
)
}
const field = LabeledInput({ id: "name", label: "Name:", placeholder: "..." })
field.focus() // Delegates to the inner input
Mixing Renderables and Constructs
const container = new BoxRenderable(renderer, { id: "root", flexDirection: "column" })
container.add(Text({ content: "Title" }), Input({ placeholder: "Type here..." }))
renderer.root.add(container)
5. Layout (Yoga Flexbox)
Flex Direction
{ flexDirection: "column" } // vertical (default)
{ flexDirection: "row" } // horizontal
{ flexDirection: "row-reverse" }
{ flexDirection: "column-reverse" }
Justify Content (Main Axis)
flex-start | flex-end | center | space-between | space-around | space-evenly
Align Items (Cross Axis)
flex-start | flex-end | center | stretch (default) | baseline
Sizing
{ width: 30, height: 10 } // Fixed (characters/rows)
{ width: "100%", height: "50%" } // Percentage
{ flexGrow: 1, flexShrink: 0 } // Flex behaviour
{ minWidth: 20, maxHeight: 30 } // Constraints
Positioning
{ position: "relative" } // default — flows in layout
{ position: "absolute", left: 10, top: 5 } // removed from flow
Spacing
{ padding: 2 } // All sides
{ paddingTop: 1, paddingX: 4 } // Specific sides/axes
{ margin: 1 } // Same pattern
Gap
{ gap: 1 } // Space between children
6. Component Reference
BoxRenderable
Container with borders, backgrounds, and layout.
new BoxRenderable(renderer, {
id: "panel",
width: 30, height: 10,
backgroundColor: "#333366",
borderStyle: "rounded", // single | double | rounded | heavy
borderColor: "#FFFFFF",
border: true, // must be true for border to show
title: "Panel Title",
titleAlignment: "center", // left | center | right
padding: 1,
gap: 1,
flexDirection: "column",
justifyContent: "center",
alignItems: "flex-start",
flexGrow: 1,
})
Mouse events: onMouseDown, onMouseOver, onMouseOut, onMouseUp, onMouseMove, onMouseDrag, onMouseDragEnd, onMouseDrop, onMouseScroll, onMouse (catch-all).
Mouse events bubble up. Stop with event.stopPropagation().
TextRenderable
Read-only styled text.
new TextRenderable(renderer, {
content: "Hello!", // string or StyledText
fg: "#00FF00", // string | RGBA
bg: "#000000",
attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,
selectable: true,
})
Text attributes (combine with bitwise OR): BOLD, DIM, ITALIC, UNDERLINE, BLINK, INVERSE, HIDDEN, STRIKETHROUGH
Template literals:
import { t, bold, fg } from "@opentui/core"
text.content = t`${bold("Hello")} ${fg("#FF0000", "world")}!`
Helpers: bold, dim, italic, underline, blink, reverse, strikethrough, fg, bg
GOTCHA: TextRenderable.content returns a StyledText object, not a plain string. To read the raw text: text.content.chunks[0].text.
SelectRenderable
Vertical list for choosing options.
import { SelectRenderable, SelectRenderableEvents } from "@opentui/core"
const select = new SelectRenderable(renderer, {
options: [
{ name: "Option 1", description: "First option", value: "one" },
{ name: "Option 2", description: "Second option", value: "two" },
],
backgroundColor: theme.background, // default is transparent (appears black!)
selectedBackgroundColor: theme.highlight,
selectedTextColor: theme.text,
textColor: theme.text,
descriptionColor: theme.textMuted,
showDescription: true,
showScrollIndicator: true,
wrapSelection: false,
fastScrollStep: 5,
flexGrow: 1,
})
renderer.root.add(select)
select.focus() // REQUIRED for keyboard input
Keyboard controls:
| Key | Action |
|---|---|
| Up / k | Move selection up |
| Down / j | Move selection down |
| Shift+Up / Shift+Down | Fast scroll (5 items) |
| Enter | Select current item |
Events:
// ITEM_SELECTED: fires on Enter
select.on(SelectRenderableEvents.ITEM_SELECTED, (index: number, option: SelectOption) => {
console.log(option.value)
})
// SELECTION_CHANGED: fires when highlighted item changes
select.on(SelectRenderableEvents.SELECTION_CHANGED, (index: number, option: SelectOption) => {
console.log("Now highlighting:", option.name)
})
SelectOption interface:
interface SelectOption {
name: string
description: string
value?: any
}
Programmatic methods:
getSelectedIndex()/getSelectedOption()setSelectedIndex(n)/moveUp()/moveDown()/selectCurrent()- Dynamic updates: set
options,showDescription,showScrollIndicator,wrapSelectionas properties
GOTCHA: backgroundColor defaults to transparent — set it explicitly or items appear with black backgrounds.
InputRenderable
Single-line text input.
import { InputRenderable, InputRenderableEvents } from "@opentui/core"
const input = new InputRenderable(renderer, {
width: 25,
placeholder: "Enter your name...",
value: "",
maxLength: 1000,
backgroundColor: "#1a1a1a",
focusedBackgroundColor: "#222222",
textColor: "#FFFFFF",
cursorColor: "#00FF88",
})
input.focus()
input.on(InputRenderableEvents.INPUT, (value) => { /* every keystroke */ })
input.on(InputRenderableEvents.CHANGE, (value) => { /* on blur or Enter, if changed */ })
input.on(InputRenderableEvents.ENTER, () => { /* Enter key pressed */ })
TextareaRenderable
Multi-line editable text. No construct API yet.
import { TextareaRenderable } from "@opentui/core"
const textarea = new TextareaRenderable(renderer, {
width: 50, height: 6,
placeholder: "Type notes here...",
wrapMode: "word", // none | char | word
backgroundColor: "#1a1a1a",
focusedBackgroundColor: "#222222",
textColor: "#FFFFFF",
cursorColor: "#00FF88",
onSubmit: () => { console.log(textarea.plainText) },
onContentChange: () => { /* content changed */ },
onCursorChange: () => { /* cursor moved */ },
keyBindings: [{ name: "return", ctrl: true, action: "submit" }],
})
textarea.focus()
Properties: plainText (string), cursorOffset (number)
TabSelectRenderable
Horizontal tab selection.
import { TabSelectRenderable, TabSelectRenderableEvents } from "@opentui/core"
const tabs = new TabSelectRenderable(renderer, {
width: 60,
options: [
{ name: "Tab 1", description: "First tab" },
{ name: "Tab 2", description: "Second tab" },
],
tabWidth: 20,
showScrollArrows: true,
showDescription: true,
showUnderline: true,
wrapSelection: false,
})
tabs.focus()
tabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index, option) => { })
tabs.on(TabSelectRenderableEvents.SELECTION_CHANGED, (index, option) => { })
Keys: Left/[ = prev, Right/] = next, Enter = select
Methods: getSelectedIndex(), setSelectedIndex(n), setOptions(array)
ScrollBoxRenderable
Scrollable container.
const scrollbox = new ScrollBoxRenderable(renderer, {
width: 60, height: 20,
scrollX: false,
scrollY: true, // default
stickyScroll: false, // "bottom" | "top" | "left" | "right" when truthy
viewportCulling: true, // Render only visible children (default)
})
Keyboard (when focused): Arrow keys, Page Up/Down, Home, End.
Methods:
scrollBy()— relative scrolling by lines, pixels, or viewportscrollTo()— absolute positioning
Internal structure: wrapper, viewport, content, horizontalScrollBar, verticalScrollBar
Sub-component options: rootOptions, wrapperOptions, viewportOptions, contentOptions, scrollbarOptions
ScrollBarRenderable
Standalone scrollbar. No construct API yet.
const scrollbar = new ScrollBarRenderable(renderer, {
orientation: "vertical", // vertical | horizontal
height: 10,
showArrows: true,
trackOptions: { backgroundColor: "#222222", foregroundColor: "#888888" },
onChange: (position) => { console.log(position) },
})
scrollbar.scrollSize = 200
scrollbar.viewportSize = 20
scrollbar.scrollPosition = 0
scrollbar.focus()
Keys: Up/Down or k/j (vertical), Left/Right or h/l (horizontal), PageUp/Down, Home/End
SliderRenderable
Draggable slider. No construct API yet.
const slider = new SliderRenderable(renderer, {
orientation: "horizontal", // horizontal | vertical
width: 30, height: 1,
min: 0, max: 100, value: 25,
backgroundColor: "#333",
foregroundColor: "#0f0",
onChange: (value) => { console.log(value) },
})
ASCIIFontRenderable
ASCII art font display.
new ASCIIFontRenderable(renderer, {
text: "Iris",
font: "block", // tiny | block | shade | slick | huge | grid | pallet
color: "#FFFFFF", // or array for gradient: ["#FF0000", "#0000FF"]
backgroundColor: "transparent",
selectable: false,
})
Both Renderable (ASCIIFontRenderable) and Construct (ASCIIFont) APIs available.
CodeRenderable
Syntax-highlighted code with Tree-sitter.
import { CodeRenderable, SyntaxStyle, RGBA } from "@opentui/core"
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromHex("#E6EDF3") },
keyword: { fg: RGBA.fromHex("#FF7B72") },
string: { fg: RGBA.fromHex("#A5D6FF") },
comment: { fg: RGBA.fromHex("#8B949E"), italic: true },
function: { fg: RGBA.fromHex("#D2A8FF") },
})
const code = new CodeRenderable(renderer, {
content: "const x = 1;",
filetype: "typescript",
syntaxStyle,
streaming: false,
conceal: true,
selectable: true,
wrapMode: "none",
})
Token names: keyword, string, comment, function, operator, variable, type, number, constant, plus markup.* for markdown.
MarkdownRenderable
Markdown renderer. No construct API yet.
new MarkdownRenderable(renderer, {
content: "# Hello\n\nSome **bold** text.",
syntaxStyle,
conceal: true, // Hide markdown markers
streaming: false, // Incremental update optimisation
renderNode: (node) => { /* custom rendering per block */ },
})
LineNumberRenderable
Line number gutter. No construct API yet.
const lineNumbers = new LineNumberRenderable(renderer, {
target: codeRenderable, // Must implement LineInfoProvider
minWidth: 3,
paddingRight: 1,
fg: "#6b7280",
bg: "#161b22",
})
lineNumbers.setLineColor(3, "#2b6cb0")
lineNumbers.setLineSign(3, { before: ">", beforeColor: "#2b6cb0" })
DiffRenderable
Unified or split diffs. No construct API yet.
new DiffRenderable(renderer, {
diff: unifiedDiffString,
view: "unified", // unified | split
filetype: "typescript",
syntaxStyle,
showLineNumbers: true,
addedBg: "#1a4d1a",
removedBg: "#4d1a1a",
addedSignColor: "#22c55e",
removedSignColor: "#ef4444",
})
FrameBufferRenderable
Low-level rendering surface.
new FrameBufferRenderable(renderer, {
width: 40, height: 20,
respectAlpha: false,
})
Drawing methods: setCell, setCellWithAlphaBlending, drawText, fillRect, drawFrameBuffer
7. Keyboard Input
Global Key Handler
renderer.keyInput.on("keypress", (key: KeyEvent) => {
console.log(key.name, key.ctrl, key.shift, key.meta)
})
renderer.keyInput.on("paste", (event: PasteEvent) => {
console.log(event.text)
})
KeyEvent Properties
| Property | Type | Description |
|---|---|---|
name |
string |
Key identifier (e.g. "a", "escape", "f1", "return") |
sequence |
string |
Raw escape sequence |
ctrl |
boolean |
Ctrl modifier |
shift |
boolean |
Shift modifier |
meta |
boolean |
Alt/Meta modifier |
option |
boolean |
macOS Option key |
Event methods: preventDefault(), stopPropagation()
Per-Renderable Key Handling
new InputRenderable(renderer, {
onKeyDown: (key) => {
if (key.name === "escape") input.blur()
},
onPaste: (event) => { console.log(event.text) },
})
Raw Input Handler
renderer.addInputHandler((sequence) => {
if (sequence === "\x1b[A") return true // consumed
return false // pass through
})
8. Focus Management
input.focus() // Give focus
input.blur() // Remove focus
console.log(input.focused) // Check state
Auto-focus: Left-clicking a renderable auto-focuses nearest focusable ancestor. Disable globally with { autoFocus: false } or per-interaction with event.preventDefault() in onMouseDown.
Events:
import { RenderableEvents } from "@opentui/core"
input.on(RenderableEvents.FOCUSED, () => { })
input.on(RenderableEvents.BLURRED, () => { })
Internal key routing: focus() uses _internalKeyInput.onInternal() — the renderer's internal key handler that ensures global handlers can preventDefault before renderable handlers process events.
9. Colours
RGBA Class
import { RGBA } from "@opentui/core"
RGBA.fromInts(255, 0, 0, 255) // From integers (0-255)
RGBA.fromValues(0.0, 1.0, 0.0, 1.0) // From normalised floats (0.0-1.0)
RGBA.fromHex("#800080") // From hex string
RGBA.fromHex("#FF000080") // With alpha
String Colour Support
Components accept: hex strings ("#FF0000"), CSS colour names ("red"), RGBA objects, "transparent".
parseColor() Utility
import { parseColor } from "@opentui/core"
const rgba = parseColor("#FF0000") // Converts various formats to RGBA
Common Constants
RGBA.white, RGBA.black, RGBA.red, RGBA.green, RGBA.blue, RGBA.transparent
Palette advisory: When choosing hex values for OpenTUI components, prefer colours from Reasonable Colors (
library/docs/reasonable-colors-reference.md). The LCH-based palette is designed for consistent rendering across display types, which matters more in terminal contexts than web. UseRGBA.fromHex()with RC hex values directly.
10. Console Overlay
OpenTUI captures all console.log/info/warn/error/debug calls to prevent interference with the UI.
const renderer = await createCliRenderer({
consoleOptions: {
position: ConsolePosition.BOTTOM, // TOP | BOTTOM | LEFT | RIGHT
sizePercent: 30,
},
})
renderer.console.toggle()
Keyboard (when focused): Arrow keys to scroll, +/- to resize.
Env vars:
OTUI_USE_CONSOLE=false— disable captureSHOW_CONSOLE=true— start visibleOTUI_DUMP_CAPTURES=true— output on exit
11. Environment Variables
| Variable | Default | Description |
|---|---|---|
OTUI_USE_ALTERNATE_SCREEN |
true |
Alternate screen buffer |
OTUI_SHOW_STATS |
false |
Debug overlay at startup |
OTUI_DEBUG |
false |
Debug input capture |
OTUI_NO_NATIVE_RENDER |
false |
Disable native rendering |
OTUI_DUMP_CAPTURES |
false |
Dump captured output on exit |
OTUI_OVERRIDE_STDOUT |
true |
Override stdout (debug) |
OTUI_USE_CONSOLE |
true |
Enable console capture |
SHOW_CONSOLE |
false |
Show console at startup |
OTUI_TS_STYLE_WARN |
false |
Warn on missing syntax styles |
OTUI_TREE_SITTER_WORKER_PATH |
"" |
Tree-sitter worker path |
OTUI_DEBUG_FFI |
false |
Debug logging for FFI |
OTUI_TRACE_FFI |
false |
Tracing for FFI |
OPENTUI_FORCE_WCWIDTH |
false |
Use wcwidth for char widths |
OPENTUI_FORCE_UNICODE |
false |
Force Mode 2026 Unicode |
OPENTUI_NO_GRAPHICS |
false |
Disable Kitty graphics detection |
OPENTUI_FORCE_NOZWJ |
false |
No ZWJ width method |
OPENTUI_FORCE_EXPLICIT_WIDTH |
— | Force explicit width detection |
12. Tree-sitter Integration
Global Registration
import { addDefaultParsers } from "@opentui/core"
addDefaultParsers([{
filetype: "python",
wasm: "https://github.com/tree-sitter/tree-sitter-python/releases/download/v0.23.6/tree-sitter-python.wasm",
queries: {
highlights: ["https://raw.githubusercontent.com/.../highlights.scm"],
},
}])
Per-Client
const client = new TreeSitterClient({ dataPath: "./parsers" })
client.addFiletypeParser({ filetype, wasm, queries })
Utilities
pathToFiletype("/foo/bar.ts") // "typescript"
extToFiletype(".py") // "python"
13. Framework Bindings
Solid.js (@opentui/solid)
bun install solid-js @opentui/solid
JSX components (snake_case): text, box, scrollbox, input, textarea, select, tab_select, code, diff, markdown, ascii_font, line_number
Hooks: useRenderer(), useKeyboard(), useTerminalDimensions(), onResize(), usePaste(), useSelectionHandler(), useTimeline()
Entry: render(<App />) or testRender(<App />) for testing.
React (@opentui/react)
bun add @opentui/react @opentui/core react
JSX components (kebab-case): <text>, <box>, <scrollbox>, <input>, <textarea>, <select>, <tab-select>, <code>, <diff>, <markdown>, <ascii-font>, <line-number>
Hooks: useRenderer(), useKeyboard(), useOnResize(), useTerminalDimensions(), useTimeline()
Entry: createRoot(renderer).render(<App />)
14. Common Patterns
Screen Pattern (Full-screen Views)
const CONTAINER_ID = "my-screen-root"
class MyScreen {
private renderer: Renderer
private keyHandler?: (key: KeyEvent) => void
constructor(renderer: Renderer) {
this.renderer = renderer
}
async render(): Promise<Result> {
return new Promise((resolve) => {
const container = new BoxRenderable(this.renderer, {
id: CONTAINER_ID,
flexDirection: "column",
width: "100%",
height: "100%",
backgroundColor: "#1a1a2e",
})
// ... build UI tree ...
this.renderer.root.add(container)
select.focus()
this.keyHandler = (key) => {
if (key.name === "escape") resolve({ action: "back" })
}
this.renderer.keyInput.on("keypress", this.keyHandler)
})
}
cleanup(): void {
if (this.keyHandler) {
this.renderer.keyInput.off("keypress", this.keyHandler)
}
this.renderer.root.remove(CONTAINER_ID)
}
}
Application Lifecycle
const renderer = await createCliRenderer({ exitOnCtrlC: true })
// Build UI...
renderer.root.add(container)
// When done:
renderer.destroy() // ALWAYS call this
Dynamic Content Updates
// Update text content
text.content = "New content" // Triggers re-render automatically
// Update select options
select.options = newOptions
select.setSelectedIndex(0)
// Update colours
text.fg = "#FF0000"
box.backgroundColor = "#333"
Swapping Child Renderables
// Remove old child by ID, add new one
if (oldChild) {
parent.remove(oldChild.id)
}
const newChild = new TextRenderable(renderer, { content: "New" })
parent.add(newChild)
15. Testing with Mock Renderer
When unit testing screens/components that use OpenTUI, mock the renderer:
import { vi } from "vitest"
function createMockRenderer() {
const mockRoot = { add: vi.fn(), remove: vi.fn() }
const mockKeyInput = {
on: vi.fn(), off: vi.fn(), once: vi.fn(),
emit: vi.fn(), removeAllListeners: vi.fn(),
}
const mockInternalKeyInput = {
on: vi.fn(), off: vi.fn(), once: vi.fn(), emit: vi.fn(),
onInternal: vi.fn(), offInternal: vi.fn(), removeAllListeners: vi.fn(),
}
return {
root: mockRoot,
keyInput: mockKeyInput,
_internalKeyInput: mockInternalKeyInput,
start: vi.fn(), stop: vi.fn(),
requestRender: vi.fn(),
width: 80, height: 24,
addToHitGrid: vi.fn(),
pushHitGridScissorRect: vi.fn(),
popHitGridScissorRect: vi.fn(),
clearHitGridScissorRects: vi.fn(),
setCursorPosition: vi.fn(),
setCursorStyle: vi.fn(),
setCursorColor: vi.fn(),
widthMethod: "wcwidth" as const,
capabilities: null,
requestLive: vi.fn(), dropLive: vi.fn(),
hasSelection: false,
getSelection: vi.fn().mockReturnValue(null),
requestSelectionUpdate: vi.fn(),
currentFocusedRenderable: null,
focusRenderable: vi.fn(),
registerLifecyclePass: vi.fn(),
unregisterLifecyclePass: vi.fn(),
getLifecyclePasses: vi.fn().mockReturnValue(new Set()),
clearSelection: vi.fn(),
startSelection: vi.fn(),
updateSelection: vi.fn(),
on: vi.fn(), off: vi.fn(), once: vi.fn(),
emit: vi.fn(), removeAllListeners: vi.fn(),
}
}
Key testing patterns:
SelectRenderable.focus()requires_internalKeyInputwithonInternal/offInternal- Screen
render()returns a Promise that waits for user input — don'tawaitin tests TextRenderable.contentreturnsStyledText, not string — access via.content.chunks[0].text- Call
buildUI()before testing event handlers that depend on the renderable tree
16. Gotchas & Pitfalls
remove()takes a string ID, not a renderable instance —parent.remove(child.id)notparent.remove(child)renderer.destroy()notstop()—destroy()restores terminal state.stop()only stops the render loop.exitOnCtrlC: trueis default — no manual Ctrl+C handler needed- Automatic rendering — no
renderer.start()call needed; re-renders on tree changes SelectRenderable.focus()is required — keyboard input won't work without itbackgroundColordefaults to transparent — set explicitly on SelectRenderable or items appear blackTextRenderable.contentreturnsStyledText— not a plain string. Read via.chunks[0].text_internalKeyInputneeded forfocus()— mock renderers must include this withonInternal/offInternal- OpenTUI does NOT auto-cleanup —
process.exitor unhandled errors won't restore terminal. Always calldestroy(). - Mouse events bubble — stop with
event.stopPropagation() visible = falseremoves from layout — equivalent to CSSdisplay: none, notvisibility: hidden
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?