opentui-dev
npx skills add https://github.com/gitarbor/gitarbor-tui --skill opentui-dev
Agent 安装分布
Skill 文档
OpenTUI Development Skill
This skill provides comprehensive guidance for building terminal user interfaces (TUIs) using OpenTUI, a TypeScript library for creating component-based terminal applications with React or Core API.
When to use this skill
Use this skill when:
- Building a new terminal user interface (TUI) or CLI application
- Working with OpenTUI-based projects
- Creating interactive terminal components (inputs, selects, lists, etc.)
- Implementing keyboard navigation and event handling in terminals
- Adding syntax highlighting or code display in terminal UIs
- Creating diff viewers, text editors, or terminal-based forms
- Styling terminal applications with borders, colors, and flexbox layouts
- Debugging or enhancing OpenTUI applications
- User mentions: TUI, terminal UI, CLI app, OpenTUI, terminal components
What is OpenTUI
OpenTUI is a TypeScript framework for building terminal user interfaces with:
- Component-based architecture: Similar to web frameworks, compose UIs from reusable components
- React integration: Use familiar React patterns with JSX for declarative terminal UIs
- Flexbox layouts: CSS Flexbox-like layout system powered by Yoga layout engine
- Rich components: Built-in inputs, selects, code viewers, diff viewers, and more
- Type-safe: Full TypeScript support with strict mode
- High performance: Native Zig rendering layer for optimal performance
Core Packages
@opentui/core– Core library with imperative API and primitives (standalone)@opentui/react– React reconciler for declarative UI development@opentui/solid– SolidJS reconciler (alternative framework)
Quick Start
Creating a new OpenTUI React project
bun create tui --template react
cd my-tui-app
bun install
bun run index.tsx
Manual installation
bun install @opentui/core @opentui/react react
Basic “Hello World” example
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
function App() {
return (
<box style={{ padding: 2 }}>
<text fg="#00FF00">Hello, OpenTUI!</text>
</box>
)
}
const renderer = await createCliRenderer({ useAlternateScreen: true })
createRoot(renderer).render(<App />)
renderer.start()
TypeScript Configuration
OpenTUI requires specific TypeScript settings:
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"jsxImportSource": "@opentui/react",
"strict": true,
"skipLibCheck": true
}
}
Key settings:
jsxImportSource: "@opentui/react"– Use OpenTUI’s JSX factorymoduleResolution: "bundler"– Required for Bunstrict: true– Enable strict type checking
Core Concepts
1. Renderer
The renderer manages terminal output, input events, and the rendering loop.
import { createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: true, // Exit on Ctrl+C (default: true)
targetFps: 30, // Target frame rate
maxFps: 60, // Maximum frame rate
useAlternateScreen: true, // Use alternate screen (full-screen)
useMouse: true, // Enable mouse support
backgroundColor: "#000000", // Background color
useConsole: true, // Enable console overlay
})
// Start the render loop
renderer.start()
Rendering Modes:
- Live Mode: Call
renderer.start()for continuous rendering at target FPS - On-Demand: Without
start(), renders only when changes occur
2. Layout System (Flexbox)
OpenTUI uses Yoga for CSS Flexbox-like layouts:
<box
flexDirection="row" // row | column | row-reverse | column-reverse
justifyContent="center" // flex-start | flex-end | center | space-between | space-around
alignItems="center" // flex-start | flex-end | center | stretch | baseline
gap={2} // Gap between children
flexGrow={1} // Grow factor
flexShrink={1} // Shrink factor
flexBasis="auto" // Base size
flexWrap="wrap" // no-wrap | wrap | wrap-reverse
>
{/* Children */}
</box>
Size Properties:
width={40} // Fixed width in characters
height={10} // Fixed height in lines
width="50%" // Percentage of parent
width="auto" // Auto-size based on content
minWidth={20} // Minimum width
maxWidth={80} // Maximum width
Spacing:
padding={2} // All sides
paddingLeft={1} // Individual sides
margin={1} // Margin (all sides)
marginTop={2} // Individual margins
Position:
position="relative" // relative | absolute
top={5} // Position from top
left={10} // Position from left
zIndex={999} // Stack order
3. Colors
Colors in OpenTUI use RGBA format or hex strings:
import { RGBA } from "@opentui/core"
// Hex strings (most common)
<text fg="#FFFFFF" bg="#000000">Text</text>
// RGBA objects
const red = RGBA.fromHex("#FF0000")
const blue = RGBA.fromInts(0, 0, 255, 255) // RGB 0-255
const transparent = RGBA.fromValues(1.0, 1.0, 1.0, 0.5) // RGBA 0.0-1.0
4. Text Attributes
Style text with bold, italic, underline, etc.:
import { TextAttributes } from "@opentui/core"
<text attributes={TextAttributes.BOLD}>Bold text</text>
<text attributes={TextAttributes.BOLD | TextAttributes.ITALIC}>Bold italic</text>
// Available attributes:
// BOLD, DIM, ITALIC, UNDERLINE, BLINK, INVERSE, HIDDEN, STRIKETHROUGH
React Components
Box Component
Container with borders, backgrounds, and layout:
<box
title="Panel Title"
border // Show border
borderStyle="single" // single | double | rounded | bold | classic
borderColor="#FFFFFF"
backgroundColor="#1a1a1a"
focused={true} // Highlight when focused
padding={2}
flexDirection="column"
width={40}
height={10}
>
<text>Content</text>
</box>
Border Styles:
single– Single line (ââââââ)double– Double line (ââââââ)rounded– Rounded corners (ââââ®â°â¯)bold– Bold/thick lineclassic– ASCII style (+-|)
Text Component
Display text with styling:
// Simple text
<text fg="#FFFFFF" bg="#000000">Simple text</text>
// Rich text with children
<text>
<span fg="#FF0000">Red text</span>
{" "}
<strong>Bold</strong>
{" "}
<em>Italic</em>
{" "}
<u>Underlined</u>
<br />
<a href="https://example.com">Link</a>
</text>
// Formatted content
<text content="Preformatted content" fg="#00FF00" />
Input Component
Single-line text input:
const [value, setValue] = useState("")
<input
placeholder="Type here..."
value={value}
focused={true} // Must be focused to receive input
onInput={setValue} // Called on every keystroke
onSubmit={(val) => { // Called on Enter key
console.log("Submitted:", val)
}}
style={{
backgroundColor: "#1a1a1a",
focusedBackgroundColor: "#2a2a2a",
}}
/>
Textarea Component
Multi-line text editor:
import { useRef } from "react"
import type { TextareaRenderable } from "@opentui/core"
const textareaRef = useRef<TextareaRenderable>(null)
<textarea
ref={textareaRef}
placeholder="Type here..."
focused={true}
initialValue="Initial text"
style={{ width: 60, height: 10 }}
/>
// Access value: textareaRef.current?.getText()
Select Component
List selection with scrolling:
import type { SelectOption } from "@opentui/core"
const options: SelectOption[] = [
{ name: "Option 1", description: "First option", value: "opt1" },
{ name: "Option 2", description: "Second option", value: "opt2" },
{ name: "Option 3", description: "Third option", value: "opt3" },
]
<select
options={options}
focused={true}
onChange={(index, option) => {
console.log(`Selected ${index}:`, option)
}}
showScrollIndicator
style={{ height: 10 }}
/>
Navigation:
- Up/Down arrows – Move selection
- Enter – Confirm selection
- Auto-scrolls to keep selection visible
Tab Select Component
Horizontal tab selection:
<tab-select
options={[
{ name: "Home", description: "Dashboard view" },
{ name: "Settings", description: "Configuration" },
{ name: "Help", description: "Documentation" },
]}
focused={true}
onChange={(index, option) => {
console.log("Tab changed:", option.name)
}}
tabWidth={20}
/>
Scrollbox Component
Scrollable container for long content:
<scrollbox
focused={true}
style={{
height: 20,
rootOptions: { backgroundColor: "#24283b" },
wrapperOptions: { backgroundColor: "#1f2335" },
viewportOptions: { backgroundColor: "#1a1b26" },
contentOptions: { backgroundColor: "#16161e" },
scrollbarOptions: {
showArrows: true,
trackOptions: {
foregroundColor: "#7aa2f7",
backgroundColor: "#414868",
},
},
}}
>
{/* Long scrollable content */}
<text>Line 1</text>
<text>Line 2</text>
{/* ... many more lines */}
</scrollbox>
Code Component
Display code with syntax highlighting:
import { SyntaxStyle, RGBA } from "@opentui/core"
const syntaxStyle = SyntaxStyle.fromStyles({
keyword: { fg: RGBA.fromHex("#ff6b6b"), bold: true },
string: { fg: RGBA.fromHex("#51cf66") },
comment: { fg: RGBA.fromHex("#868e96"), italic: true },
function: { fg: RGBA.fromHex("#4dabf7") },
number: { fg: RGBA.fromHex("#ffd43b") },
})
<code
content={codeString}
filetype="typescript" // javascript, python, rust, go, etc.
syntaxStyle={syntaxStyle}
/>
Line Number Component
Code with line numbers and diff highlights:
import { useRef, useEffect } from "react"
import type { LineNumberRenderable } from "@opentui/core"
const lineNumberRef = useRef<LineNumberRenderable>(null)
useEffect(() => {
// Highlight added line
lineNumberRef.current?.setLineColor(1, "#1a4d1a")
lineNumberRef.current?.setLineSign(1, { after: " +", afterColor: "#22c55e" })
// Highlight deleted line
lineNumberRef.current?.setLineColor(5, "#4d1a1a")
lineNumberRef.current?.setLineSign(5, { after: " -", afterColor: "#ef4444" })
// Add diagnostic warning
lineNumberRef.current?.setLineSign(10, { before: "â ï¸", beforeColor: "#f59e0b" })
}, [])
<line-number
ref={lineNumberRef}
fg="#6b7280"
bg="#161b22"
minWidth={3}
showLineNumbers={true}
>
<code content={codeContent} filetype="typescript" />
</line-number>
Diff Component
Unified or split diff viewer:
<diff
oldContent={oldCode}
newContent={newCode}
filetype="typescript"
syntaxStyle={syntaxStyle}
viewMode="unified" // unified | split
/>
ASCII Font Component
Display ASCII art text:
<ascii-font
text="OPENTUI"
font="block" // tiny | block | slick | shade
color={RGBA.fromHex("#00FF00")}
/>
React Hooks
useRenderer()
Access the renderer instance:
import { useRenderer } from "@opentui/react"
const renderer = useRenderer()
// Show/hide console overlay
renderer.console.toggle()
renderer.console.show()
renderer.console.hide()
useKeyboard(handler, options?)
Handle keyboard events:
import { useKeyboard } from "@opentui/react"
import type { KeyEvent } from "@opentui/core"
useKeyboard((key: KeyEvent) => {
console.log("Key:", key.name)
console.log("Modifiers:", { ctrl: key.ctrl, shift: key.shift, alt: key.meta })
if (key.name === "escape") {
process.exit(0)
}
if (key.ctrl && key.name === "s") {
// Save action
}
}, { release: false }) // Set release: true for key release events
Key Event Properties:
name– Key name (e.g., “a”, “enter”, “escape”, “up”, “down”)ctrl– Ctrl key pressedshift– Shift key pressedmeta– Alt/Meta key pressed (Linux/Windows)option– Option key pressed (macOS)sequence– Raw key sequence
useTerminalDimensions()
Get current terminal size (auto-updates on resize):
import { useTerminalDimensions } from "@opentui/react"
const { width, height } = useTerminalDimensions()
return (
<box style={{ width, height }}>
<text>{`Terminal: ${width}x${height}`}</text>
</box>
)
useOnResize(callback)
Handle terminal resize events:
import { useOnResize } from "@opentui/react"
useOnResize((width, height) => {
console.log(`Resized to ${width}x${height}`)
})
useTimeline(options?)
Create animations:
import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"
const [width, setWidth] = useState(0)
const timeline = useTimeline({
duration: 2000,
loop: false,
autoplay: true,
})
useEffect(() => {
timeline.add(
{ width },
{
width: 50,
duration: 2000,
ease: "linear",
onUpdate: (animation) => {
setWidth(animation.targets[0].width)
},
}
)
}, [])
return <box style={{ width }}><text>Animating...</text></box>
Common Patterns
Multi-view Application
import { useState } from "react"
import { useKeyboard } from "@opentui/react"
type View = "home" | "settings" | "help"
function App() {
const [view, setView] = useState<View>("home")
useKeyboard((key) => {
if (key.name === "1") setView("home")
if (key.name === "2") setView("settings")
if (key.name === "3") setView("help")
if (key.name === "escape") process.exit(0)
})
return (
<box style={{ flexDirection: "column", width: "100%", height: "100%" }}>
<Header view={view} />
{view === "home" && <HomeView />}
{view === "settings" && <SettingsView />}
{view === "help" && <HelpView />}
<Footer />
</box>
)
}
Modal Pattern
import { useState } from "react"
import { useKeyboard } from "@opentui/react"
function App() {
const [showModal, setShowModal] = useState(false)
useKeyboard((key) => {
if (showModal) return // Modal handles its own keys
if (key.name === "m") {
setShowModal(true)
}
})
return (
<>
<MainContent />
{showModal && (
<Modal onClose={() => setShowModal(false)} />
)}
</>
)
}
function Modal({ onClose }: { onClose: () => void }) {
const [input, setInput] = useState("")
useKeyboard((key) => {
if (key.name === "escape") onClose()
})
return (
<box
style={{
position: "absolute",
top: "50%",
left: "50%",
width: 60,
height: 10,
backgroundColor: "#1a1a1a",
border: true,
borderColor: "#FFFFFF",
}}
>
<input
placeholder="Enter value..."
value={input}
focused={true}
onInput={setInput}
onSubmit={(val) => {
console.log("Submitted:", val)
onClose()
}}
/>
</box>
)
}
Focus Management
import { useState } from "react"
import { useKeyboard } from "@opentui/react"
type FocusPanel = "left" | "right"
function App() {
const [focused, setFocused] = useState<FocusPanel>("left")
useKeyboard((key) => {
if (key.name === "tab") {
setFocused(focused === "left" ? "right" : "left")
}
})
return (
<box style={{ flexDirection: "row", width: "100%", height: "100%" }}>
<box
title="Left Panel"
style={{ flexGrow: 1, border: true }}
focused={focused === "left"}
>
<text>Content</text>
</box>
<box
title="Right Panel"
style={{ flexGrow: 1, border: true }}
focused={focused === "right"}
>
<text>Content</text>
</box>
</box>
)
}
List with Selection
import { useState } from "react"
import { useKeyboard } from "@opentui/react"
function FileList({ files }: { files: string[] }) {
const [selected, setSelected] = useState(0)
useKeyboard((key) => {
if (key.name === "up") {
setSelected(Math.max(0, selected - 1))
}
if (key.name === "down") {
setSelected(Math.min(files.length - 1, selected + 1))
}
if (key.name === "enter") {
console.log("Selected:", files[selected])
}
})
return (
<box style={{ flexDirection: "column" }}>
{files.map((file, idx) => (
<text
key={file}
fg={idx === selected ? "#00FF00" : "#FFFFFF"}
bg={idx === selected ? "#333333" : undefined}
>
{idx === selected ? "> " : " "}
{file}
</text>
))}
</box>
)
}
Responsive Layout
import { useTerminalDimensions } from "@opentui/react"
function App() {
const { width, height } = useTerminalDimensions()
// Show simplified layout on small terminals
const isSmall = width < 80
return (
<box style={{ flexDirection: isSmall ? "column" : "row", width, height }}>
<Sidebar width={isSmall ? width : 30} />
<MainContent width={isSmall ? width : width - 30} />
</box>
)
}
Loading Spinner
import { useState, useEffect } from "react"
function Spinner() {
const [frame, setFrame] = useState(0)
const frames = ["â ", "â ", "â ¹", "â ¸", "â ¼", "â ´", "â ¦", "â §", "â ", "â "]
useEffect(() => {
const interval = setInterval(() => {
setFrame((f) => (f + 1) % frames.length)
}, 80)
return () => clearInterval(interval)
}, [])
return <text fg="#00FFFF">{frames[frame]} Loading...</text>
}
Code Style Guidelines
Import Order
- External dependencies (React, etc.)
- OpenTUI packages
- Internal utilities
- Components
- Type imports (with
typekeyword)
import { useState, useEffect } from "react"
import { useKeyboard, useRenderer } from "@opentui/react"
import { RGBA, TextAttributes } from "@opentui/core"
import { formatTime } from "./utils"
import { Header } from "./components/Header"
import type { AppState, View } from "./types"
Naming Conventions
- camelCase: variables, functions, parameters
- PascalCase: components, classes, types/interfaces
- UPPER_CASE: constants
// Good
const userInput = "text"
function handleSubmit() {}
interface UserData {}
const MAX_RETRIES = 3
// Bad
const UserInput = "text"
function HandleSubmit() {}
interface userData {}
Component Structure
interface ComponentProps {
title: string
items: string[]
}
export function Component({ title, items }: ComponentProps) {
// 1. Hooks
const [selected, setSelected] = useState(0)
const renderer = useRenderer()
// 2. Event handlers
useKeyboard((key) => {
if (key.name === "escape") process.exit(0)
})
// 3. Effects
useEffect(() => {
// Side effects
}, [])
// 4. Helper functions
const handleSelect = (index: number) => {
setSelected(index)
}
// 5. JSX return
return (
<box>
<text>{title}</text>
{/* Content */}
</box>
)
}
Best Practices
Performance
- Minimize re-renders: Use
useMemoanduseCallbackfor expensive computations - Avoid frequent updates: Throttle or debounce rapid state changes
- Use keys properly: Provide stable keys for lists to optimize reconciliation
- Optimize large lists: Use virtualization or pagination for long lists
Focus Management
- Only one focused component: Ensure only one input/select has
focused={true}at a time - Keyboard routing: Check focus state before handling keyboard events
- Visual feedback: Use
focusedprop on boxes to show focus state with borders
Layout Tips
- Use flexbox: Leverage
flexDirection,justifyContent,alignItemsfor layouts - Percentage widths: Use
"50%"for responsive layouts - flexGrow/flexShrink: Use for responsive sizing
- Absolute positioning: Use sparingly for modals/overlays
Debugging
- Console overlay: Use
renderer.console.show()instead ofconsole.log - Toggle console: Press backtick (`) to toggle console visibility
- Environment variables: Set
OTUI_DEBUG=truefor debug mode - Stats overlay: Set
OTUI_SHOW_STATS=trueto see performance stats
Error Handling
- Graceful degradation: Handle terminal resize and small screens
- Input validation: Validate user input before processing
- Error boundaries: Wrap components in error boundaries for production
Common Gotchas
- Console captured:
console.loggoes to overlay, userenderer.console.show()to view - Focus required: Inputs/selects need
focused={true}to receive events - Bun only: OpenTUI is designed for Bun runtime, not Node.js
- Zig for builds: Only needed when modifying native code, not for TypeScript changes
- Alternate screen: Default is full-screen mode, use
useAlternateScreen: falseto disable - Key names: Use lowercase names (“a”, “enter”, “escape”), not “A”, “Enter”, “Escape”
Troubleshooting
Application not rendering
- Ensure you called
renderer.start()aftercreateRoot().render() - Check terminal supports ANSI escape codes
- Try disabling alternate screen:
useAlternateScreen: false
Keyboard input not working
- Ensure component has
focused={true} - Check if modal or overlay is capturing input
- Verify
useKeyboardhook is registered
Layout issues
- Check parent has explicit dimensions (
width,height) - Use
flexGrowfor flexible sizing - Ensure
flexDirectionis set correctly - Use browser DevTools-like thinking (Yoga = Flexbox)
Colors not showing
- Verify terminal supports 24-bit color
- Use hex format:
"#RRGGBB" - Check color values are valid (0-255 for RGB)
Reference Documentation
For detailed API references and advanced topics, see:
- Component API Reference – Complete component props and options
- Core API Reference – Low-level Core package API
- Layout Guide – In-depth Yoga/Flexbox layout guide
- Styling Guide – Colors, text attributes, and styling patterns
Examples
The OpenTUI repository contains many examples:
- Basic counter app
- Login form with validation
- File explorer with keyboard navigation
- Code viewer with syntax highlighting
- Git diff viewer
- Multi-view dashboard
- Modal dialogs
- Tab navigation
Resources
- GitHub: https://github.com/anomalyco/opentui
- NPM Core: https://www.npmjs.com/package/@opentui/core
- NPM React: https://www.npmjs.com/package/@opentui/react
- Awesome OpenTUI: https://github.com/msmps/awesome-opentui
- Create TUI: https://github.com/msmps/create-tui
Development Commands
# Install dependencies
bun install
# Run application
bun run index.tsx
# Type check
bun --bun tsc --noEmit
# Build project (if applicable)
bun run build
Next Steps
After implementing a basic OpenTUI application:
- Add keyboard shortcuts: Implement common shortcuts (Ctrl+Q, etc.)
- Create themes: Extract colors to theme objects for consistency
- Add help screen: Show keyboard shortcuts and commands
- Implement error handling: Gracefully handle edge cases
- Optimize performance: Profile and optimize render performance
- Add tests: Write unit tests for components and logic
- Document usage: Add README with screenshots and usage guide