一键导入
opentui-operative
OpenTUI terminal UI library reference. Use when working with @opentui/core, terminal UIs, renderables, Yoga layouts, or Zig-native rendering.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
OpenTUI terminal UI library reference. Use when working with @opentui/core, terminal UIs, renderables, Yoga layouts, or Zig-native rendering.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
{{ 𝚫𝚫𝚫 }} Rebuild roadmap-system.zip, the distributable snapshot of the roadmap tooling (scripts, HTML template, conventions reference, and every roadmap-touching skill, including this one).
{{ 𝛀𝛀𝛀 }} Create a project roadmap in the rich phase-array format — roadmaps.json as source of truth plus a PHASE task list and prose overview
{{ 𝛀𝛀𝛀 }} Recompute and synchronise roadmap task statuses across roadmaps.json and its projections, with optional codebase reconciliation
{{ 𝛀𝛀𝛀 }} Add a task to a rich-format project roadmap with correct ID, dependency wiring, and graph integrity — ID assignment, status computation, dependency edges in both directions, and no unconnected islands.
Git workflow: branch management, commit conventions, PR patterns, conflict resolution.
{{ 𝛀𝛀𝛀 }} Review a pull request and post it as a GitHub review
| name | opentui-operative |
| description | OpenTUI terminal UI library reference. Use when working with @opentui/core, terminal UIs, renderables, Yoga layouts, or Zig-native rendering. |
| when_to_use | When building or debugging a terminal UI with @opentui/core — renderable composition, Yoga layout issues, or anything touching the Zig-native rendering layer. |
| user-invocable | false |
Comprehensive reference for building terminal UIs with OpenTUI (@opentui/core). Source: https://opentui.com/docs/ — all 29 documentation pages.
Use when: user mentions "OpenTUI", "TUI", "terminal UI", "@opentui/core", renderables, or works on files importing from @opentui/core. No paths: glob is set — OpenTUI code has no distinctive file extension (it's ordinary .ts/.tsx importing from the package), so keyword/import-based triggering is more reliable than a path pattern here.
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.
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.
The CliRenderer drives everything — terminal output, input events, render loop, and context for renderables.
import { createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: true, // default: true
targetFps: 30, // default: 30
})
The factory:
CliRenderer| 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 |
| 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 |
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()
renderer.on("resize", (width, height) => { /* terminal resized */ })
renderer.on("destroy", () => { /* renderer destroyed */ })
renderer.on("selection", (selection) => { /* text selected */ })
renderer.setCursorPosition(10, 5, true)
renderer.setCursorStyle("block", true) // block | underline | line
renderer.setCursorColor(RGBA.fromHex("#FF0000"))
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.
renderer.toggleDebugOverlay()
import { DebugOverlayCorner } from "@opentui/core"
renderer.configureDebugOverlay({ enabled: true, corner: DebugOverlayCorner.topRight })
Renderables are the building blocks of the UI. Each represents a visual element using Yoga layout engine for positioning.
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)
| 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 |
Full per-component API (options, events, gotchas) is in component-reference.md — load it when working with a specific component beyond basic construction.
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")
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.
const title = container.getRenderable("title") // Direct child by ID
const deep = container.findDescendantById("nested-input") // Recursive search
const children = container.getChildren() // All children
panel.visible = false // Hides AND removes from layout (like CSS display: none)
panel.visible = true
panel.opacity = 0.5 // Affects renderable and all children
const overlay = new BoxRenderable(renderer, {
position: "absolute",
zIndex: 100, // Higher values render on top
})
renderable.translateX = 10
renderable.translateY = -5
// Moves visually without affecting layout
renderable.destroy() // Remove from parent, free resources
container.destroyRecursively() // Destroy self and all children
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 */ }
}
const box = new AnimatedBox(renderer, {
live: true, // Enable continuous rendering for this renderable
})
const complex = new BoxRenderable(renderer, {
buffered: true, // Render to offscreen buffer first
renderAfter: (buffer) => {
buffer.fillRect(0, 0, 10, 5, RGBA.fromHex("#FF0000"))
},
})
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..." }),
)
ASCIIFont, Box, Code, FrameBuffer, Input, ScrollBox, Select, TabSelect, Text, SyntaxStyle
NOT yet available as constructs (use Renderable API): Textarea, ScrollBar, Slider, Markdown, LineNumber, Diff
VNodes queue method calls — applied after the component is created:
const input = Input({ placeholder: "Name..." })
input.focus() // Queued, applied when added to tree
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
const container = new BoxRenderable(renderer, { id: "root", flexDirection: "column" })
container.add(Text({ content: "Title" }), Input({ placeholder: "Type here..." }))
renderer.root.add(container)
{ flexDirection: "column" } // vertical (default)
{ flexDirection: "row" } // horizontal
{ flexDirection: "row-reverse" }
{ flexDirection: "column-reverse" }
flex-start | flex-end | center | space-between | space-around | space-evenly
flex-start | flex-end | center | stretch (default) | baseline
{ width: 30, height: 10 } // Fixed (characters/rows)
{ width: "100%", height: "50%" } // Percentage
{ flexGrow: 1, flexShrink: 0 } // Flex behaviour
{ minWidth: 20, maxHeight: 30 } // Constraints
{ position: "relative" } // default — flows in layout
{ position: "absolute", left: 10, top: 5 } // removed from flow
{ padding: 2 } // All sides
{ paddingTop: 1, paddingX: 4 } // Specific sides/axes
{ margin: 1 } // Same pattern
{ gap: 1 } // Space between children
Deep-dive detail lives in supporting files, loaded only when needed: