| name | opentui |
| description | Comprehensive skill for building terminal user interfaces with OpenTUI framework. This skill should be used when building TUIs, creating terminal applications, or working with the OpenTUI React/Solid bindings. |
OpenTUI Skill
OpenTUI is a native terminal UI framework written in Zig with TypeScript bindings. It provides a component-based architecture for building interactive terminal applications with first-class React and SolidJS support.
1. Introduction and Overview
OpenTUI (Open Terminal User Interface) is a library for building terminal user interfaces using a native Zig core. Key features include:
- Native Zig Core: High-performance rendering with a C ABI for any language
- Component-Based Architecture: Flexbox-like layout system using Yoga
- React & SolidJS Bindings: Use familiar React/Solid patterns for TUIs
- Rich Components: Text, Box, Input, Select, ScrollBox, Code, Markdown, Diff, and more
- Keyboard & Mouse Input: Full input handling with focus management
- Theming: Support for light/dark mode detection
- Debug Overlay: Built-in FPS, memory, and performance metrics
Use Cases
- CLI tools with rich interfaces
- Terminal-based games
- Developer tools (terminal editors, debuggers)
- System monitoring applications
- Interactive command-line applications
2. Installation and Setup
Prerequisites
- Bun 1.3.0+ (recommended) or Node.js 18+
- Zig 0.15.2 (required only for building from source)
Quick Start with create-tui
bun create tui my-app
cd my-app
bun run src/index.ts
Select a template (React, Solid, or vanilla TypeScript) during creation.
Manual Installation
bun add @opentui/core
bun add @opentui/react @opentui/core react
bun add @opentui-ui/solid @opentui/core solid-js
TypeScript Configuration
For React projects using OpenTUI:
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"jsxImportSource": "@opentui/react",
"strict": true,
"skipLibCheck": true
}
}
3. Core Concepts and Architecture
Renderer
The CliRenderer is the core of OpenTUI. It manages terminal output, handles input events, runs the rendering loop, and provides context for creating renderables.
import { createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: true,
targetFps: 30,
})
Screen Modes
- alternate-screen (default): Full-screen TUI application
- main-screen: Render without switching buffers
- split-footer: Pin renderer to footer region
Renderables vs Constructs
OpenTUI provides two APIs:
-
Renderable API: Object-oriented, imperative style
const box = new BoxRenderable(renderer, { width: 30, height: 10 })
box.add(text)
renderer.root.add(box)
-
Construct API: Declarative, functional style
renderer.root.add(
Box({ width: 30, height: 10 }, Text({ content: "Hello" }))
)
Layout System
OpenTUI uses the Yoga layout engine with CSS Flexbox-like properties:
Box({
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
height: 10,
padding: 2,
gap: 1,
})
Keyboard Input
renderer.keyInput.on("keypress", (key) => {
if (key.name === "escape") {
renderer.destroy()
process.exit(0)
}
})
Colors
import { RGBA } from "@opentui/core"
const red = RGBA.fromHex("#FF0000")
const blue = RGBA.fromInts(0, 0, 255, 255)
Text({ content: "Hello", fg: "#00FFFF" })
Box({ backgroundColor: "blue" })
Lifecycle
Always call renderer.destroy() on exit to restore terminal state:
process.on("uncaughtException", () => {
renderer.destroy()
process.exit(1)
})
4. API Reference
Core Functions
| Function | Description |
|---|
createCliRenderer(config?) | Create a new CLI renderer |
createRoot(renderer) | Create a React root (React binding) |
Core Components
| Component | Description |
|---|
Text | Text display with styling |
Box | Container with borders and layout |
Input | Single-line text input |
Textarea | Multi-line text input |
Select | Vertical selection list |
TabSelect | Tab-based selection |
ScrollBox | Scrollable container |
ScrollBar | Scrollbar component |
Slider | Slider control |
Code | Syntax-highlighted code |
Markdown | Markdown rendering |
LineNumber | Line numbers with diff support |
Diff | Unified or split diff viewer |
FrameBuffer | Low-level frame buffer |
ASCIIFont | ASCII art text rendering |
React Hooks
| Hook | Description |
|---|
useRenderer() | Access the OpenTUI renderer |
useKeyboard(handler, options?) | Handle keyboard events |
useOnResize(callback) | Handle terminal resize |
useTerminalDimensions() | Get reactive dimensions |
useTimeline(options?) | Create animations |
Events
Common events emitted by components:
change: Value changed
input: Input event
submit: Form submit
itemSelected: Item selected in list
focus: Component focused
blur: Component lost focus
5. Practical Examples
Hello World
import { createCliRenderer, Text } from "@opentui/core"
const renderer = await createCliRenderer()
renderer.root.add(Text({ content: "Hello, OpenTUI!", fg: "#00FFFF" }))
Interactive Counter
import { createCliRenderer, Box, Text } from "@opentui/core"
const renderer = await createCliRenderer()
let count = 0
const display = Text({ content: "Count: 0", fg: "#FFFF00" })
renderer.keyInput.on("keypress", (key) => {
if (key.name === "up") {
count++
display.setContent(`Count: ${count}`)
renderer.requestRender()
} else if (key.name === "down") {
count--
display.setContent(`Count: ${count}`)
renderer.requestRender()
}
})
renderer.root.add(display)
Login Form
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
function LoginForm() {
return (
<box style={{ border: true, padding: 2, flexDirection: "column", gap: 1 }}>
<text fg="#FFFF00">Login</text>
<box title="Username" style={{ border: true, width: 30, height: 3 }}>
<input placeholder="Username..." />
</box>
<box title="Password" style={{ border: true, width: 30, height: 3 }}>
<input placeholder="Password..." />
</box>
</box>
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(<LoginForm />)
File Menu
import { createCliRenderer, Box, Select } from "@opentui/core"
const renderer = await createCliRenderer()
const menu = Select({
width: 25,
height: 10,
options: [
{ name: "New File", description: "Create new (Ctrl+N)" },
{ name: "Open...", description: "Open file (Ctrl+O)" },
{ name: "Save", description: "Save (Ctrl+S)" },
{ name: "Exit", description: "Quit (Ctrl+Q)" },
],
})
menu.on("itemSelected", (index, option) => {
console.log("Selected:", option.name)
})
menu.focus()
renderer.root.add(Box({ border: true, borderStyle: "rounded" }, menu))
Flexbox Layout
renderer.root.add(
Box({
flexDirection: "row",
width: "100%",
height: "100%",
padding: 1,
},
Box({
flexGrow: 1,
border: true,
borderStyle: "rounded",
}, Text({ content: "Left Panel" })),
Box({
width: 30,
border: true,
borderStyle: "rounded",
}, Text({ content: "Right" })),
)
)
6. Configuration and Customization
Renderer Options
const renderer = await createCliRenderer({
screenMode: "alternate-screen",
exitOnCtrlC: true,
targetFps: 30,
maxFps: 60,
useMouse: true,
autoFocus: true,
backgroundColor: "#000000",
})
Styling Components
Box({
width: 40,
height: 15,
backgroundColor: "#1a1a1a",
border: true,
borderStyle: "rounded",
borderColor: "#666666",
padding: 2,
margin: 1,
})
Border Styles
single: Single line (┌─┐│└─┘)
double: Double line (╔═╗║╚═╝)
rounded: Rounded corners (╭─╮│╰─╯)
heavy: Heavy lines (┏━┓┃┗━┛)
Theme Mode Detection
const mode = renderer.themeMode
renderer.on("theme_mode", (newMode) => {
console.log("Theme changed to:", newMode)
})
7. Troubleshooting
Common Issues
Terminal stays broken after crash:
- Run
reset command in terminal
- Add error handlers calling
renderer.destroy()
Mouse not working:
- Ensure
useMouse: true in config
- Check terminal supports mouse protocol
Input not receiving keys:
- Call
input.focus() on the component
- Check no other component has focus
Environment Variables
| Variable | Description |
|---|
OTUI_USE_CONSOLE | Enable console capture (default: true) |
SHOW_CONSOLE | Show console overlay at startup |
OTUI_NO_NATIVE_RENDER | Skip native renderer |
OTUI_DEBUG | Capture raw stdin for debugging |
OTUI_SHOW_STATS | Show debug overlay at startup |
Debug Overlay
renderer.toggleDebugOverlay()
renderer.configureDebugOverlay({
enabled: true,
corner: "topRight",
})
8. Best Practices
Performance
- Use
requestRender() instead of continuous rendering when possible
- Batch DOM updates in React by using state
- Use
renderable.requestRender() for targeted updates
Accessibility
- Always provide focus indicators
- Use clear, high-contrast colors
- Support keyboard navigation for all interactive elements
Clean Shutdown
const renderer = await createCliRenderer()
process.on("uncaughtException", () => {
renderer.destroy()
process.exit(1)
})
process.on("unhandledRejection", () => {
renderer.destroy()
process.exit(1)
})
Component Structure
function Panel({ title, children }) {
return (
<box
style={{
border: true,
borderStyle: "rounded",
padding: 1,
margin: 1,
}}
>
<text bold fg="#00FFFF">{title}</text>
{children}
</box>
)
}
9. Integration
React Integration
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
function App() {
return <box><text>Hello React!</text></box>
}
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
SolidJS Integration
import { createCliRenderer } from "@opentui/core"
import { createRoot as createSolidRoot } from "@opentui-ui/solid"
function App() {
return <box><text>Hello Solid!</text></box>
}
const renderer = await createCliRenderer()
createSolidRoot(renderer).render(<App />)
Tree-sitter for Syntax Highlighting
OpenTUI supports tree-sitter for code syntax highlighting. Set the worker path:
OTUI_TREE_SITTER_WORKER_PATH=/path/to/worker.js
10. Advanced Features
Custom Renderables
import { BoxRenderable, type RenderContext, type BoxOptions } from "@opentui/core"
class CustomButton extends BoxRenderable {
constructor(ctx: RenderContext, options: BoxOptions & { label?: string }) {
super(ctx, options)
this.borderStyle = "rounded"
this.padding = 2
}
}
Animations with useTimeline
import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"
function AnimatedBox() {
const [width, setWidth] = useState(0)
const timeline = useTimeline({ duration: 2000, loop: true })
useEffect(() => {
timeline.add(
{ width },
{
width: 50,
duration: 2000,
ease: "linear",
onUpdate: (anim) => setWidth(anim.targets[0].width),
}
)
}, [])
return <box style={{ width, backgroundColor: "#6a5acd" }} />
}
Cursor Control
renderer.setCursorPosition(10, 5, true)
renderer.setCursorStyle({ style: "block", blinking: true })
Memory Management
renderer.on("memory:snapshot", (snapshot) => {
console.log("Memory:", snapshot)
})
References
For detailed API documentation, see:
references/api-quickref.md - Quick API reference
references/examples.md - Extended code examples