
Opentui
- 2.3k installs
- 12.9k repo stars
- Updated August 4, 2026
- anomalyco/opentui
opentui is an agent skill for Build terminal UIs with OpenTUI. Covers the core API, native audio, keymaps, React and Solid bindings, components, layou
About
The opentui skill Build terminal UIs with OpenTUI. Covers the core API, native audio, keymaps, React and Solid bindings, components, layout, keyboard input, plugins, and testing. It covers /docs/<slug maps to docs/<slug .mdx relative to this skill root. Key workflows include in the repo, that same slug maps to packages/web/src/content/docs/<slug .mdx. Developers invoke opentui when the task matches the triggers and reference files in SKILL.md for grounded, stepwise execution. Reference files and progressive disclosure keep context focused while preserving concrete commands, configuration fields, and validation checks copied from the upstream documentation. Reference files and progressive disclosure keep context focused while preserving concrete commands, configuration fields, and validation checks copied from the upstream documentation. Reference files and progressive disclosure keep context focused while preserving concrete commands, configuration fields, and validation checks copied from the upstream documentation.
- /docs/<slug maps to docs/<slug .mdx relative to this skill root
- in the repo, that same slug maps to packages/web/src/content/docs/<slug .mdx
- Getting started: /docs/getting-started
- Core: /docs/core-concepts/renderer
- Testing: /docs/core-concepts/testing
Opentui by the numbers
- 2,335 all-time installs (skills.sh)
- +180 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #350 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
opentui capabilities & compatibility
- Capabilities
- /docs/<slug maps to docs/<slug .mdx relative to · in the repo, that same slug maps to packages/web · getting started: /docs/getting started · core: /docs/core concepts/renderer · testing: /docs/core concepts/testing
- Use cases
- documentation
What opentui says it does
description: Build terminal UIs with OpenTUI. Covers the core API, native audio, keymaps, React and Solid bindings, components, layout, keyboard input, plugins, and testing.
npx skills add https://github.com/anomalyco/opentui --skill opentuiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.3k |
|---|---|
| repo stars | ★ 12.9k |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | anomalyco/opentui ↗ |
What problem does opentui solve for developers using the documented workflows?
Build terminal UIs with OpenTUI. Covers the core API, native audio, keymaps, React and Solid bindings, components, layout, keyboard input, plugins, and testing.
Who is it for?
Developers working with opentui patterns described in the skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill documented scope.
When should I use this skill?
Use when Build terminal UIs with OpenTUI. Covers the core API, native audio, keymaps, React and Solid bindings, components, layout, keyboard input, plugins, and testing.
What you get
Actionable opentui guidance grounded in SKILL.md workflows and reference files.
- TUI component code
- Keyboard handler setup
- Interactive terminal layout
Files
OpenTUI Skill
Canonical reference docs are authored once in sibling docs/**/*.mdx files.
Inside the OpenTUI repo, this skill root lives at packages/web/src/content/, so the same files are also visible at packages/web/src/content/docs/**/*.mdx.
Path invariant
/docs/<slug>maps todocs/<slug>.mdxrelative to this skill root- in the repo, that same slug maps to
packages/web/src/content/docs/<slug>.mdx
Reading order by area
- Getting started:
/docs/getting-started - Core:
/docs/core-concepts/renderer - Testing:
/docs/core-concepts/testing - Audio:
/docs/core-concepts/audio - Keymap:
/docs/keymap/overview - React:
/docs/bindings/react - Solid:
/docs/bindings/solid - Components:
/docs/components/text,/docs/components/input - Layout:
/docs/core-concepts/layout - Keyboard:
/docs/core-concepts/keyboard - Plugins:
/docs/plugins/slots - Reference:
/docs/reference/env-vars
Quick routing by intent
| Intent(s) | Start here |
|---|---|
getting-started, installation, quickstart, intro | docs/getting-started.mdx |
core, renderer, terminal, scrollback, lifecycle | docs/core-concepts/renderer.mdx |
audio, native-audio, sound, playback, pcm, fft | docs/core-concepts/audio.mdx |
keymap, keybindings, shortcuts, commands, leader | docs/keymap/overview.mdx |
layout, flexbox, yoga, positioning | docs/core-concepts/layout.mdx |
keyboard, input, keybindings, paste, focus | docs/core-concepts/keyboard.mdx |
testing, test-renderer, snapshots, frames | docs/core-concepts/testing.mdx |
react, jsx, hooks, animation, testing | docs/bindings/react.mdx |
solid, signals, jsx, hooks, animation, testing | docs/bindings/solid.mdx |
plugins, plugin, slots, registry, extensions | docs/plugins/slots.mdx |
text, styling, content, selection | docs/components/text.mdx |
input, form, editing, focus | docs/components/input.mdx |
env, environment, configuration, flags | docs/reference/env-vars.mdx |
For concrete component requests, jump straight to docs/components/<name>.mdx after the relevant entry page. For plugin implementation details, narrow from docs/plugins/slots.mdx into docs/plugins/core.mdx, docs/plugins/react.mdx, or docs/plugins/solid.mdx.
Current skill entry pages
docs/getting-started.mdxdocs/core-concepts/renderer.mdxdocs/core-concepts/audio.mdxdocs/keymap/overview.mdxdocs/core-concepts/layout.mdxdocs/core-concepts/keyboard.mdxdocs/bindings/react.mdxdocs/bindings/solid.mdxdocs/plugins/slots.mdxdocs/components/text.mdxdocs/components/input.mdxdocs/reference/env-vars.mdx
Working rules
- Prefer the current entry pages first, then read narrower docs in the same section.
- Read the sibling
docs/**/*.mdxfiles directly instead of copying prose into this file. - Use stable
/docs/...URLs when cross-referencing docs.
---
title: React
description: Build terminal UIs with React and OpenTUI
order: 2
skill:
entry: true
intents: [react, jsx, hooks, keyboard, paste, focus, blur, selection, animation, testing]
---
# React bindings
Build terminal user interfaces using React with familiar patterns and components.
## Installation
Quick start with [bun](https://bun.sh) and [create-tui](https://github.com/msmps/create-tui):
```bash
bun create tui --template react
```
Manual installation:
```bash
bun install @opentui/react @opentui/core react
```
## Quick start
```tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
function App() {
return <text>Hello, world!</text>
}
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
```
## TypeScript configuration
Configure your `tsconfig.json`:
```json
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"jsxImportSource": "@opentui/react",
"strict": true,
"skipLibCheck": true
}
}
```
## Runtime-loaded plugin support (if needed)
If your app loads external TS/TSX modules at runtime (for example a file-based plugin system), import this once in your app entry before dynamic imports:
```ts
import "@opentui/react/runtime-plugin-support"
```
Use this for both normal Bun runs and standalone compiled executables.
## Components
OpenTUI React provides JSX intrinsic elements that map to core renderables:
### Layout & display
- `<text>` - Text display with styling
- `<box>` - Container with borders and layout
- `<scrollbox>` - Scrollable container
- `<ascii-font>` - ASCII art text
QR code support is available from `@opentui/qrcode/react` and must be registered explicitly with `registerQRCode()`.
### Input
- `<input>` - Single-line text input
- `<textarea>` - Multi-line text input
- `<select>` - Selection list
- `<tab-select>` - Tab-based selection
### Code & diff
- `<code>` - Syntax-highlighted code
- `<line-number>` - Line numbers with diff/diagnostic support
- `<diff>` - Unified or split diff viewer
- `<markdown>` - Markdown rendering
### Text modifiers
Use inside `<text>` components:
- `<span>` - Inline styled text
- `<strong>`, `<b>` - Bold text
- `<em>`, `<i>` - Italic text
- `<u>` - Underlined text
- `<br>` - Line break
- `<a>` - Link text
## API reference
### `createRoot(renderer)`
Creates a React root for rendering into the terminal.
```tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
```
For plugin slots, see [Plugin Slots overview](/docs/plugins/slots) and [React slots](/docs/plugins/react).
## Hooks
### `useRenderer()`
Access the OpenTUI renderer instance.
```tsx
import { useRenderer } from "@opentui/react"
import { useEffect } from "react"
function App() {
const renderer = useRenderer()
useEffect(() => {
renderer.console.show()
console.log("Hello from console!")
}, [])
return <box />
}
```
### `useKeyboard(handler, options?)`
Handle keyboard events.
```tsx
import { useKeyboard, useRenderer } from "@opentui/react"
function App() {
const renderer = useRenderer()
useKeyboard((key) => {
if (key.name === "escape") {
renderer.destroy()
}
})
return <text>Press ESC to close</text>
}
```
To handle release events:
```tsx
useKeyboard(
(event) => {
if (event.eventType === "release") {
console.log("Key released:", event.name)
} else {
console.log("Key pressed:", event.name)
}
},
{ release: true },
)
```
### `useOnResize(callback)`
Handle terminal resize events.
```tsx
import { useOnResize } from "@opentui/react"
function App() {
useOnResize((width, height) => {
console.log(`Resized to ${width}x${height}`)
})
return <text>Resize-aware component</text>
}
```
### `useTerminalDimensions()`
Get reactive terminal dimensions.
```tsx
import { useTerminalDimensions } from "@opentui/react"
function App() {
const { width, height } = useTerminalDimensions()
return (
<text>
Terminal: {width}x{height}
</text>
)
}
```
### `usePaste(handler)`
Handle terminal paste events (bracketed paste).
```tsx
import { decodePasteBytes } from "@opentui/core"
import { usePaste } from "@opentui/react"
function App() {
usePaste((event) => {
const text = decodePasteBytes(event.bytes)
console.log("Pasted text:", text)
})
return <text>Paste something into the terminal</text>
}
```
### `useFocus(handler)`
Subscribe to terminal window focus events.
```tsx
import { useFocus } from "@opentui/react"
function App() {
useFocus(() => {
console.log("Terminal gained focus")
})
return <text>Focus-aware component</text>
}
```
### `useBlur(handler)`
Subscribe to terminal window blur events.
```tsx
import { useBlur } from "@opentui/react"
function App() {
useBlur(() => {
console.log("Terminal lost focus")
})
return <text>Blur-aware component</text>
}
```
### `useSelectionHandler(handler)`
Handle text selection events (e.g., mouse drag selection).
```tsx
import { useSelectionHandler } from "@opentui/react"
function App() {
useSelectionHandler((selection) => {
const text = selection.getSelectedText()
console.log("Selected:", text)
})
return <text selectable>Select this text with your mouse</text>
}
```
### `useTimeline(options?)`
Create and manage animations.
```tsx
import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"
function App() {
const [width, setWidth] = useState(0)
const timeline = useTimeline({
duration: 2000,
loop: false,
})
useEffect(() => {
timeline.add(
{ width },
{
width: 50,
duration: 2000,
ease: "linear",
onUpdate: (animation) => {
setWidth(animation.targets[0].width)
},
},
)
}, [])
return <box style={{ width, backgroundColor: "#6a5acd" }} />
}
```
**Options:**
- `duration` - Animation duration in ms (default: 1000)
- `loop` - Whether to loop (default: false)
- `autoplay` - Auto-start (default: true)
- `onComplete` - Completion callback
- `onPause` - Pause callback
## Styling
Style components with props or the `style` prop:
```tsx
// Direct props
<box backgroundColor="blue" padding={2}>
<text>Hello</text>
</box>
// Style prop
<box style={{ backgroundColor: "blue", padding: 2 }}>
<text>Hello</text>
</box>
```
## Example: Login form
```tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot, useKeyboard } from "@opentui/react"
import { useCallback, useState } from "react"
function App() {
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [focused, setFocused] = useState<"username" | "password">("username")
const [status, setStatus] = useState("idle")
useKeyboard((key) => {
if (key.name === "tab") {
setFocused((prev) => (prev === "username" ? "password" : "username"))
}
})
const handleSubmit = useCallback(() => {
if (username === "admin" && password === "secret") {
setStatus("success")
} else {
setStatus("error")
}
}, [username, password])
return (
<box style={{ border: true, padding: 2, flexDirection: "column", gap: 1 }}>
<text fg="#FFFF00">Login Form</text>
<box title="Username" style={{ border: true, width: 40, height: 3 }}>
<input
placeholder="Enter username..."
onInput={setUsername}
onSubmit={handleSubmit}
focused={focused === "username"}
/>
</box>
<box title="Password" style={{ border: true, width: 40, height: 3 }}>
<input
placeholder="Enter password..."
onInput={setPassword}
onSubmit={handleSubmit}
focused={focused === "password"}
/>
</box>
<text fg={status === "success" ? "green" : status === "error" ? "red" : "#999"}>{status.toUpperCase()}</text>
</box>
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
```
## Component extension
Register custom renderables as JSX elements:
```tsx
import { BoxRenderable, createCliRenderer, type BoxOptions, type RenderContext } from "@opentui/core"
import { createRoot, extend } from "@opentui/react"
class ConsoleButtonRenderable extends BoxRenderable {
private _label: string = "Button"
constructor(ctx: RenderContext, options: BoxOptions & { label?: string }) {
super(ctx, options)
if (options.label) this._label = options.label
this.borderStyle = "single"
this.padding = 2
}
get label(): string {
return this._label
}
set label(value: string) {
this._label = value
this.requestRender()
}
}
// Add TypeScript support
declare module "@opentui/react" {
interface OpenTUIComponents {
consoleButton: typeof ConsoleButtonRenderable
}
}
// Register the component
extend({ consoleButton: ConsoleButtonRenderable })
// Use in JSX
function App() {
return <consoleButton label="Click me!" style={{ border: true, backgroundColor: "green" }} />
}
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
```
## React DevTools
OpenTUI React supports React DevTools for debugging:
1. Install:
```bash
bun add --dev react-devtools-core@7
```
2. Start DevTools:
```bash
npx react-devtools@7
```
3. Run with DEV flag:
```bash
DEV=true bun run your-app.ts
```
---
title: Solid.js
description: Build terminal UIs with Solid.js and OpenTUI
order: 1
skill:
entry: true
intents: [solid, jsx, signals, hooks, keyboard, animation, testing]
---
# Solid.js bindings
Build terminal user interfaces with Solid.js's fine-grained reactivity and OpenTUI.
## Installation
```bash
bun install solid-js @opentui/solid
```
## Runtime support
`@opentui/solid` publishes one npm package with runtime-specific entrypoints behind the package `exports` map. The root package and JSX runtime subpaths work in both Bun and Node. Bun-specific helpers stay Bun-only and throw a clear runtime error if imported from Node.
| Entrypoint | Bun | Node | Notes |
| ------------------------------------------------- | --- | ---- | ----------------------------------------------- |
| `@opentui/solid` | yes | yes | Main renderer and test helpers |
| `@opentui/solid/jsx-runtime` | yes | yes | Real runtime module for compiled JSX |
| `@opentui/solid/jsx-dev-runtime` | yes | yes | Real dev runtime module for compiled JSX |
| `@opentui/solid/preload` | yes | no | Bun preload hook |
| `@opentui/solid/bun-plugin` | yes | no | Bun bundler/plugin entry |
| `@opentui/solid/runtime-plugin-support` | yes | no | Bun runtime-loader support |
| `@opentui/solid/runtime-plugin-support/configure` | yes | no | Bun runtime-loader support without side effects |
## Setup
### 1. Configure TypeScript
Add JSX config to `tsconfig.json`:
```json
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@opentui/solid"
}
}
```
### 2. Configure Bun
Add preload script to `bunfig.toml`:
```toml
preload = ["@opentui/solid/preload"]
```
### 3. Configure Node compilation
Node support currently targets compiled Solid TSX. Use Solid's universal transform semantics and point the generated runtime imports at the published `solid-js` JS files:
```js
import ts from "@babel/preset-typescript"
import moduleResolver from "babel-plugin-module-resolver"
import solid from "babel-preset-solid"
export default {
plugins: [
[
moduleResolver,
{
resolvePath(specifier) {
if (specifier === "solid-js") return "solid-js/dist/solid.js"
if (specifier === "solid-js/store") return "solid-js/store/dist/store.js"
return specifier
},
},
],
],
presets: [[solid, { moduleName: "@opentui/solid", generate: "universal" }], [ts]],
}
```
The current Node runtime path is still lower-level than Bun. OpenTUI's native renderer currently relies on Node's experimental FFI support, so select Node 26.3.0 before running it and pass `--experimental-ffi`, `--permission`, and the filesystem or FFI permissions your app needs. OpenTUI does not install Node automatically.
### 4. Enable runtime-loaded plugin support (if needed)
If your app loads external TS/TSX modules at runtime (for example a file-based plugin system), import this once in your entry file before dynamic imports:
```ts
import "@opentui/solid/runtime-plugin-support"
```
This entrypoint is Bun-only.
### 5. Create your app
```tsx
import { render } from "@opentui/solid"
const App = () => <text>Hello, World!</text>
render(App)
```
Run with `bun index.tsx`.
## Components
OpenTUI Solid provides JSX intrinsic elements that map to core renderables.
**Note:** Solid uses snake_case for multi-word component names (e.g., `ascii_font`, `tab_select`).
### Layout & display
- `<text>` - Styled text container
- `<box>` - Layout container with borders
- `<scrollbox>` - Scrollable container
- `<ascii_font>` - ASCII art text
- `<markdown>` - Render Markdown content
QR code support is available from `@opentui/qrcode/solid` and must be registered explicitly with `registerQRCode()`.
### Input
- `<input>` - Single-line text input
- `<textarea>` - Multi-line text input
- `<select>` - List selection
- `<tab_select>` - Tab-based selection
### Code & diff
- `<code>` - Syntax-highlighted code
- `<line_number>` - Line numbers with diff/diagnostic support
- `<diff>` - Unified or split diff viewer
### Text modifiers
Use inside `<text>` components:
- `<span>` - Inline styled text
- `<strong>`, `<b>` - Bold text
- `<em>`, `<i>` - Italic text
- `<u>` - Underlined text
- `<br>` - Line break
- `<a>` - Link text with href
## API reference
### `render(node, rendererOrConfig?)`
Render a Solid component tree into a CLI renderer.
```tsx
import { render } from "@opentui/solid"
// Simple usage
render(() => <App />)
// With renderer config
render(() => <App />, {
targetFps: 30,
exitOnCtrlC: false,
})
```
**Parameters:**
- `node` - Function returning a JSX element
- `rendererOrConfig` - Optional `CliRenderer` instance or `CliRendererConfig`
### `testRender(node, options?)`
Create a test renderer for snapshots and interaction tests.
```tsx
import { testRender } from "@opentui/solid"
const testSetup = await testRender(() => <App />, { width: 40, height: 10 })
```
### `extend(components)`
Register custom renderables as JSX intrinsic elements.
```tsx
import { extend } from "@opentui/solid"
extend({ custom_box: CustomBoxRenderable })
```
### `getComponentCatalogue()`
Returns the current component catalogue that powers JSX tag lookup.
For plugin slots, see [Plugin Slots overview](/docs/plugins/slots) and [Solid slots](/docs/plugins/solid).
## Scrollback writers
In [`split-footer`](/docs/core-concepts/renderer#split-footer) mode with `externalOutputMode: "capture-stdout"`, the Solid binding provides helpers that append JSX-rendered output above the footer. They wrap [`renderer.writeToScrollback`](/docs/core-concepts/renderer#writing-to-scrollback) so you can write scrollback content with signals and components.
### `writeSolidToScrollback(renderer, node, options?)`
Render a JSX node once and append it as a scrollback commit.
```tsx
import { writeSolidToScrollback } from "@opentui/solid"
writeSolidToScrollback(renderer, () => <text fg="#8BD5CA">api responded in 12ms</text>)
```
### `createScrollbackWriter(node, options?)`
If you need to pass the same JSX rendering to multiple `writeToScrollback` calls (or hold onto the writer inside your own code), use the lower-level factory:
```tsx
import { createScrollbackWriter } from "@opentui/solid"
const writer = createScrollbackWriter(() => <text>logged at {new Date().toISOString()}</text>, { startOnNewLine: true })
renderer.writeToScrollback(writer)
```
### Options
| Option | Type | Default | Description |
| ----------------- | --------- | -------------- | ------------------------------------------------------------------------ |
| `width` | `number` | renderer width | Override the snapshot width in columns |
| `height` | `number` | measured auto | Override the snapshot height in rows (otherwise measured from layout) |
| `rowColumns` | `number` | snapshot width | Explicit last-row column count for tail tracking |
| `startOnNewLine` | `boolean` | `true` | Insert a newline before this commit if the previous commit ended mid-row |
| `trailingNewline` | `boolean` | - | Append a newline after the final row |
The writer returns a `ScrollbackSnapshot` whose teardown disposes the inner Solid subtree after the snapshot renders. Use the core [`ScrollbackSurface`](/docs/core-concepts/renderer#writing-to-scrollback) APIs directly if you need streaming commits that re-render the same tree over time.
## Hooks
### `useRenderer()`
Access the OpenTUI renderer instance.
```tsx
import { useRenderer } from "@opentui/solid"
import { onMount } from "solid-js"
const App = () => {
const renderer = useRenderer()
onMount(() => {
renderer.console.show()
console.log("Hello from console!")
})
return <box />
}
```
### `useKeyboard(handler, options?)`
Subscribe to keyboard events.
```tsx
import { useKeyboard, useRenderer } from "@opentui/solid"
const App = () => {
const renderer = useRenderer()
useKeyboard((key) => {
if (key.name === "escape") {
renderer.destroy()
}
})
return <text>Press ESC to close</text>
}
```
With release events:
```tsx
import { createSignal } from "solid-js"
const App = () => {
const [pressedKeys, setPressedKeys] = createSignal(new Set<string>())
useKeyboard(
(event) => {
setPressedKeys((keys) => {
const newKeys = new Set(keys)
if (event.eventType === "release") {
newKeys.delete(event.name)
} else {
newKeys.add(event.name)
}
return newKeys
})
},
{ release: true },
)
return <text>Pressed: {Array.from(pressedKeys()).join(", ") || "none"}</text>
}
```
### `onResize(callback)`
Handle terminal resize events.
```tsx
import { onResize } from "@opentui/solid"
const App = () => {
onResize((width, height) => {
console.log(`Resized to ${width}x${height}`)
})
return <text>Resize-aware component</text>
}
```
### `onFocus(callback)`
Run side effects when the terminal window gains focus.
```tsx
import { onFocus } from "@opentui/solid"
const App = () => {
onFocus(() => {
console.log("Terminal focused")
})
return <text>Switch away and back to trigger focus events</text>
}
```
### `onBlur(callback)`
Run side effects when the terminal window loses focus.
```tsx
import { onBlur } from "@opentui/solid"
const App = () => {
onBlur(() => {
console.log("Terminal blurred")
})
return <text>Switch away and back to trigger blur events</text>
}
```
These hooks listen for terminal focus-in/focus-out events when the terminal emulator supports them.
### `useTerminalDimensions()`
Get reactive terminal dimensions (returns a Solid signal).
```tsx
import { useTerminalDimensions } from "@opentui/solid"
const App = () => {
const dimensions = useTerminalDimensions()
return (
<text>
Terminal: {dimensions().width}x{dimensions().height}
</text>
)
}
```
### `usePaste(handler)`
Subscribe to paste events.
```tsx
import { usePaste } from "@opentui/solid"
const textDecoder = new TextDecoder()
const App = () => {
usePaste((event) => {
console.log("Pasted:", textDecoder.decode(event.bytes))
})
return <text>Paste something!</text>
}
```
### `useSelectionHandler(callback)`
Handle text selection events.
```tsx
import { useSelectionHandler } from "@opentui/solid"
const App = () => {
useSelectionHandler((selection) => {
console.log("Selected:", selection)
})
return <text selectable>Select me!</text>
}
```
### `useTimeline(options?)`
Create and manage animations.
```tsx
import { useTimeline } from "@opentui/solid"
import { createSignal, onMount } from "solid-js"
const App = () => {
const [width, setWidth] = createSignal(0)
const timeline = useTimeline({
duration: 2000,
loop: false,
})
onMount(() => {
timeline.add(
{ width: width() },
{
width: 50,
duration: 2000,
ease: "linear",
onUpdate: (animation) => {
setWidth(animation.targets[0].width)
},
},
)
})
return <box style={{ width: width(), backgroundColor: "#6a5acd" }} />
}
```
## Special components
### `Portal`
Render children into a different mount point (useful for modals and overlays).
```tsx
import { Portal, useRenderer } from "@opentui/solid"
const App = () => {
const renderer = useRenderer()
return (
<box>
<text>Main content</text>
<Portal mount={renderer.root}>
<box border>Overlay</box>
</Portal>
</box>
)
}
```
### `Dynamic`
Render arbitrary intrinsic elements or components dynamically.
```tsx
import { Dynamic } from "@opentui/solid"
import { createSignal } from "solid-js"
const App = () => {
const [isMultiline, setIsMultiline] = createSignal(false)
return <Dynamic component={isMultiline() ? "textarea" : "input"} />
}
```
## Building for production
Use [Bun.build](https://bun.sh/docs/bundler) with the Solid plugin:
```ts
import solidPlugin from "@opentui/solid/bun-plugin"
await Bun.build({
entrypoints: ["./index.tsx"],
target: "bun",
outdir: "./build",
plugins: [solidPlugin],
})
```
To compile to a standalone executable:
```ts
await Bun.build({
entrypoints: ["./index.tsx"],
plugins: [solidPlugin],
compile: {
target: "bun-darwin-arm64",
outfile: "./app-macos",
},
})
```
If that executable loads external plugins/modules at runtime, keep `import "@opentui/solid/runtime-plugin-support"` in your app entry.
## Example: counter
```tsx
import { render, useKeyboard, useRenderer } from "@opentui/solid"
import { createSignal } from "solid-js"
const App = () => {
const [count, setCount] = createSignal(0)
const renderer = useRenderer()
useKeyboard((key) => {
if (key.name === "up") setCount((c) => c + 1)
if (key.name === "down") setCount((c) => c - 1)
if (key.name === "escape") renderer.destroy()
})
return (
<box border padding={2}>
<text>Count: {count()}</text>
<text fg="#888">Up/Down to change, ESC to close</text>
</box>
)
}
render(App)
```
## Differences from React bindings
| Aspect | Solid | React |
| ------------------ | ------------------------------------------------------ | -------------------------------------- |
| Render function | `render(() => <App />)` | `createRoot(renderer).render(<App />)` |
| Component naming | snake_case (`ascii_font`) | kebab-case (`ascii-font`) |
| State | `createSignal` | `useState` |
| Effects | `onMount`, `onCleanup` | `useEffect` |
| Resize hook | `onResize(callback)` | `useOnResize(callback)` |
| Dimensions | Returns signal: `dimensions().width` | Returns object: `dimensions.width` |
| Extra hooks | `onFocus`, `onBlur`, `usePaste`, `useSelectionHandler` | - |
| Special components | `Portal`, `Dynamic` | - |
---
title: ASCIIFont
description: Display text using ASCII art fonts
order: 14
---
# ASCIIFont
Display text using ASCII art fonts with multiple font styles available. Great for titles, headers, and decorative text.
## Basic Usage
### Renderable API
```typescript
import { ASCIIFontRenderable, RGBA, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const title = new ASCIIFontRenderable(renderer, {
id: "title",
text: "OPENTUI",
font: "tiny",
color: RGBA.fromInts(255, 255, 255, 255),
})
renderer.root.add(title)
```
### Construct API
```typescript
import { ASCIIFont, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
renderer.root.add(
ASCIIFont({
text: "HELLO",
font: "block",
color: "#00FF00",
}),
)
```
## Available Fonts
OpenTUI includes several ASCII art font styles:
```typescript
// Small, compact font
{
font: "tiny"
}
// Block style font
{
font: "block"
}
// Shaded style font
{
font: "shade"
}
// Slick style font
{
font: "slick"
}
// Large font
{
font: "huge"
}
// Grid style font
{
font: "grid"
}
// Pallet style font
{
font: "pallet"
}
```
## Positioning
Position the ASCII text anywhere on screen:
```typescript
const title = new ASCIIFontRenderable(renderer, {
id: "title",
text: "TITLE",
font: "block",
color: RGBA.fromHex("#FFFF00"),
x: 10,
y: 2,
})
```
## Properties
| Property | Type | Default | Description |
| ----------------- | ---------------------------- | --------------- | -------------------------- |
| `text` | `string` | `""` | Text to display |
| `font` | `ASCIIFontName` | `"tiny"` | Font style to use |
| `color` | `ColorInput \| ColorInput[]` | `"#FFFFFF"` | Text color(s) |
| `backgroundColor` | `ColorInput` | `"transparent"` | Background color |
| `selectable` | `boolean` | `true` | Whether text is selectable |
| `selectionBg` | `ColorInput` | - | Selection background color |
| `selectionFg` | `ColorInput` | - | Selection foreground color |
| `x` | `number` | - | X position offset |
| `y` | `number` | - | Y position offset |
## Example: Welcome Screen
```typescript
import { Box, ASCIIFont, Text, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const welcomeScreen = Box(
{
width: "100%",
height: "100%",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
},
ASCIIFont({
text: "OPENTUI",
font: "huge",
color: "#00FFFF",
}),
Text({
content: "Terminal UI Framework",
fg: "#888888",
}),
Text({
content: "Press any key to continue...",
fg: "#444444",
}),
)
renderer.root.add(welcomeScreen)
```
## Dynamic Text
Update the text content dynamically:
```typescript
const counter = new ASCIIFontRenderable(renderer, {
id: "counter",
text: "0",
font: "block",
color: RGBA.fromHex("#FF0000"),
})
let count = 0
setInterval(() => {
count++
counter.text = count.toString()
}, 1000)
```
## Color Effects
Create gradient-like effects by positioning multiple ASCII fonts:
```typescript
import { Box, ASCIIFont } from "@opentui/core"
const gradientTitle = Box(
{},
ASCIIFont({
text: "HELLO",
font: "block",
color: "#FF0000",
}),
// Overlay with offset for shadow effect
ASCIIFont({
text: "HELLO",
font: "block",
color: "#880000",
left: 1,
top: 1,
}),
)
```
---
title: Box
description: Container component with borders and layout capabilities
order: 2
---
# Box
A container component with borders, background colors, and layout capabilities. Use it to create panels, frames, and organized sections.
## Basic usage
### Renderable API
```typescript
import { BoxRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const panel = new BoxRenderable(renderer, {
id: "panel",
width: 30,
height: 10,
backgroundColor: "#333366",
borderStyle: "double",
borderColor: "#FFFFFF",
})
renderer.root.add(panel)
```
### Construct API
```typescript
import { Box, Text, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
renderer.root.add(
Box(
{
width: 30,
height: 10,
backgroundColor: "#333366",
borderStyle: "rounded",
},
Text({ content: "Inside the box!" }),
),
)
```
## Border styles
```typescript
// No border
{
border: false
}
// Simple border (default style)
{
border: true
}
// Specific border styles
{
borderStyle: "single"
} // Single line: ┌─┐│└─┘
{
borderStyle: "double"
} // Double line: ╔═╗║╚═╝
{
borderStyle: "rounded"
} // Rounded corners: ╭─╮│╰─╯
{
borderStyle: "heavy"
} // Heavy lines: ┏━┓┃┗━┛
```
## Titles
Add a title and bottom title to the box border:
```typescript
const panel = new BoxRenderable(renderer, {
id: "settings",
width: 40,
height: 15,
borderStyle: "rounded",
title: "Settings",
titleColor: "yellow",
titleAlignment: "center",
bottomTitle: "Footer",
bottomTitleAlignment: "center",
})
```
### Title alignment (both top and bottom titles)
```typescript
{
titleAlignment: "left"
} // ┌─ Title ────────┐
{
titleAlignment: "center"
} // ┌──── Title ─────┐
{
titleAlignment: "right"
} // ┌────────── Title ┐
{
bottomTitleAlignment: "left"
} // └─ Title ────────┘
{
bottomTitleAlignment: "center"
} // └──── Title ─────┘
{
bottomTitleAlignment: "right"
} // └────────── Title ┘
```
## Layout container
Box works as a flex container for child elements:
```typescript
const container = Box(
{
flexDirection: "column",
justifyContent: "space-between",
alignItems: "stretch",
width: 50,
height: 20,
padding: 1,
gap: 1,
},
Text({ content: "Header" }),
Box({ flexGrow: 1, backgroundColor: "#222" }, Text({ content: "Content area" })),
Text({ content: "Footer" }),
)
```
## Mouse events
Handle mouse interactions on the box:
```typescript
const button = new BoxRenderable(renderer, {
id: "button",
width: 12,
height: 3,
border: true,
backgroundColor: "#444",
onMouseDown: () => {
console.log("Button clicked!")
},
onMouseOver: () => {
button.backgroundColor = "#666"
},
onMouseOut: () => {
button.backgroundColor = "#444"
},
})
```
## Properties
| Property | Type | Default | Description |
| ---------------------- | ------------------ | -------------- | --------------------------------- |
| `width` | `number \| string` | - | Width in characters or percentage |
| `height` | `number \| string` | - | Height in rows or percentage |
| `backgroundColor` | `string \| RGBA` | - | Background fill color |
| `border` | `boolean` | `false` | Show border |
| `borderStyle` | `string` | `"single"` | Border style |
| `borderColor` | `string \| RGBA` | - | Border color |
| `title` | `string` | - | Title text in border |
| `titleColor` | `string \| RGBA` | - | Color of the title text |
| `titleAlignment` | `string` | `"left"` | Title position |
| `bottomTitle` | `string` | - | Bottom title text in border |
| `bottomTitleAlignment` | `string` | `"left"` | Bottom title position |
| `padding` | `number` | `0` | Internal padding |
| `gap` | `number \| string` | - | Gap between children |
| `flexDirection` | `string` | `"column"` | Child layout direction |
| `justifyContent` | `string` | `"flex-start"` | Main axis alignment |
| `alignItems` | `string` | `"stretch"` | Cross axis alignment |
## Example: Card component
```typescript
import { Box, Text, t, bold, fg } from "@opentui/core"
function Card(props: { title: string; description: string }) {
return Box(
{
width: 40,
borderStyle: "rounded",
borderColor: "#666",
padding: 1,
margin: 1,
},
Text({
content: t`${bold(fg("#00FFFF")(props.title))}`,
}),
Text({
content: props.description,
fg: "#AAAAAA",
}),
)
}
renderer.root.add(
Box(
{ flexDirection: "row", flexWrap: "wrap" },
Card({ title: "Feature 1", description: "Description of feature 1" }),
Card({ title: "Feature 2", description: "Description of feature 2" }),
Card({ title: "Feature 3", description: "Description of feature 3" }),
),
)
```
---
title: Code
description: Syntax-highlighted code display component with Tree-sitter support
order: 10
---
# Code
Displays syntax-highlighted code using Tree-sitter. Supports many languages with accurate, fast highlighting.
## Basic usage
### Renderable API
```typescript
import { CodeRenderable, createCliRenderer, SyntaxStyle, RGBA } from "@opentui/core"
const renderer = await createCliRenderer()
const syntaxStyle = SyntaxStyle.fromStyles({
keyword: { fg: RGBA.fromHex("#FF7B72"), bold: true },
string: { fg: RGBA.fromHex("#A5D6FF") },
comment: { fg: RGBA.fromHex("#8B949E"), italic: true },
number: { fg: RGBA.fromHex("#79C0FF") },
function: { fg: RGBA.fromHex("#D2A8FF") },
default: { fg: RGBA.fromHex("#E6EDF3") },
})
const code = new CodeRenderable(renderer, {
id: "code",
content: `function hello() {
// This is a comment
const message = "Hello, world!"
return message
}`,
filetype: "javascript",
syntaxStyle,
width: 50,
height: 10,
})
renderer.root.add(code)
```
### Construct API
```typescript
import { Code, Box, createCliRenderer, SyntaxStyle, RGBA } from "@opentui/core"
const renderer = await createCliRenderer()
const syntaxStyle = SyntaxStyle.fromStyles({
keyword: { fg: RGBA.fromHex("#FF7B72"), bold: true },
string: { fg: RGBA.fromHex("#A5D6FF") },
default: { fg: RGBA.fromHex("#E6EDF3") },
})
renderer.root.add(
Box(
{ border: true, width: 50, height: 10 },
Code({
content: 'const x = "hello"',
filetype: "javascript",
syntaxStyle,
}),
),
)
```
## Creating syntax styles
Use `SyntaxStyle.fromStyles()` to define colors and attributes for syntax tokens:
```typescript
import { SyntaxStyle, RGBA, parseColor } from "@opentui/core"
const syntaxStyle = SyntaxStyle.fromStyles({
// Basic tokens
keyword: { fg: RGBA.fromHex("#FF7B72"), bold: true },
"keyword.import": { fg: RGBA.fromHex("#FF7B72"), bold: true },
"keyword.operator": { fg: RGBA.fromHex("#FF7B72") },
string: { fg: RGBA.fromHex("#A5D6FF") },
comment: { fg: RGBA.fromHex("#8B949E"), italic: true },
number: { fg: RGBA.fromHex("#79C0FF") },
boolean: { fg: RGBA.fromHex("#79C0FF") },
constant: { fg: RGBA.fromHex("#79C0FF") },
// Functions and types
function: { fg: RGBA.fromHex("#D2A8FF") },
"function.call": { fg: RGBA.fromHex("#D2A8FF") },
"function.method.call": { fg: RGBA.fromHex("#D2A8FF") },
type: { fg: RGBA.fromHex("#FFA657") },
constructor: { fg: RGBA.fromHex("#FFA657") },
// Variables and properties
variable: { fg: RGBA.fromHex("#E6EDF3") },
"variable.member": { fg: RGBA.fromHex("#79C0FF") },
property: { fg: RGBA.fromHex("#79C0FF") },
// Operators and punctuation
operator: { fg: RGBA.fromHex("#FF7B72") },
punctuation: { fg: RGBA.fromHex("#F0F6FC") },
"punctuation.bracket": { fg: RGBA.fromHex("#F0F6FC") },
"punctuation.delimiter": { fg: RGBA.fromHex("#C9D1D9") },
// Default fallback
default: { fg: RGBA.fromHex("#E6EDF3") },
})
```
### Style properties
Each style definition can include:
| Property | Type | Description |
| ----------- | --------- | ----------------------- |
| `fg` | `RGBA` | Foreground (text) color |
| `bg` | `RGBA` | Background color |
| `bold` | `boolean` | Bold text |
| `italic` | `boolean` | Italic text |
| `underline` | `boolean` | Underlined text |
| `dim` | `boolean` | Dimmed text |
## Supported languages
Code uses Tree-sitter for parsing. Tree-sitter supports these languages:
- TypeScript / JavaScript
- Markdown
- Zig
- And any language with a Tree-sitter grammar
## Streaming mode
Enable streaming mode when content arrives incrementally, like LLM output:
```typescript
const code = new CodeRenderable(renderer, {
id: "streaming-code",
content: "",
filetype: "typescript",
syntaxStyle,
streaming: true, // Enable streaming mode
})
// Later, append content
code.content += "const x = 1\n"
code.content += "const y = 2\n"
```
Streaming mode optimizes highlighting for incremental updates.
## Text selection
Enable text selection for copy operations:
```typescript
const code = new CodeRenderable(renderer, {
id: "code",
content: sourceCode,
filetype: "typescript",
syntaxStyle,
selectable: true,
selectionBg: "#264F78",
selectionFg: "#FFFFFF",
})
```
## Concealment
The `conceal` option controls whether certain syntax elements (like markdown formatting characters) are hidden:
```typescript
const code = new CodeRenderable(renderer, {
id: "markdown",
content: "# Heading\n**bold** text",
filetype: "markdown",
syntaxStyle,
conceal: true, // Hide formatting characters
})
```
## With line numbers
Use `LineNumberRenderable` to add line numbers:
```typescript
import { CodeRenderable, LineNumberRenderable, RGBA, ScrollBoxRenderable, SyntaxStyle } from "@opentui/core"
const sourceCode = "const answer = 42\nconsole.log(answer)\n"
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromHex("#E6EDF3") },
})
const code = new CodeRenderable(renderer, {
id: "code",
content: sourceCode,
filetype: "typescript",
syntaxStyle,
width: "100%",
})
const lineNumbers = new LineNumberRenderable(renderer, {
id: "code-with-lines",
target: code,
minWidth: 3,
paddingRight: 1,
fg: "#6b7280",
bg: "#161b22",
width: "100%",
})
// Wrap in ScrollBox for scrolling
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "scrollbox",
width: 60,
height: 20,
})
scrollbox.add(lineNumbers)
renderer.root.add(scrollbox)
```
## Properties
| Property | Type | Default | Description |
| ------------------ | ------------------ | -------- | ---------------------------------------- |
| `content` | `string` | `""` | Source code to display |
| `filetype` | `string` | - | Language for syntax highlighting |
| `syntaxStyle` | `SyntaxStyle` | required | Syntax highlighting theme |
| `streaming` | `boolean` | `false` | Optimize for incremental content updates |
| `conceal` | `boolean` | `true` | Hide concealed syntax elements |
| `drawUnstyledText` | `boolean` | `true` | Show text before highlighting completes |
| `treeSitterClient` | `TreeSitterClient` | - | Custom Tree-sitter client instance |
### Inherited from TextBufferRenderable
| Property | Type | Default | Description |
| -------------- | ------------------ | -------- | ------------------------------------------- |
| `fg` | `string \| RGBA` | - | Default foreground color |
| `bg` | `string \| RGBA` | - | Background color |
| `selectable` | `boolean` | `false` | Enable text selection |
| `selectionBg` | `string \| RGBA` | - | Selection background color |
| `selectionFg` | `string \| RGBA` | - | Selection foreground color |
| `wrapMode` | `string` | `"word"` | Text wrapping: `"none"`, `"char"`, `"word"` |
| `tabIndicator` | `string \| number` | - | Tab display character or width |
## Additional properties
| Property | Type | Description |
| ---------------- | --------- | -------------------------------------------- |
| `lineCount` | `number` | Number of lines in content |
| `scrollY` | `number` | Current vertical scroll position (get/set) |
| `scrollX` | `number` | Current horizontal scroll position (get/set) |
| `scrollWidth` | `number` | Total scrollable width (read-only) |
| `scrollHeight` | `number` | Total scrollable height (read-only) |
| `isHighlighting` | `boolean` | Whether highlighting is in progress |
| `plainText` | `string` | Raw text content |
## Markdown styles
For markdown highlighting, use markup-prefixed style names:
```typescript
const markdownStyle = SyntaxStyle.fromStyles({
"markup.heading": { fg: RGBA.fromHex("#58A6FF"), bold: true },
"markup.heading.1": { fg: RGBA.fromHex("#00FF88"), bold: true, underline: true },
"markup.heading.2": { fg: RGBA.fromHex("#00D7FF"), bold: true },
"markup.bold": { fg: RGBA.fromHex("#F0F6FC"), bold: true },
"markup.strong": { fg: RGBA.fromHex("#F0F6FC"), bold: true },
"markup.italic": { fg: RGBA.fromHex("#F0F6FC"), italic: true },
"markup.list": { fg: RGBA.fromHex("#FF7B72") },
"markup.quote": { fg: RGBA.fromHex("#8B949E"), italic: true },
"markup.raw": { fg: RGBA.fromHex("#A5D6FF") },
"markup.raw.block": { fg: RGBA.fromHex("#A5D6FF") },
"markup.link": { fg: RGBA.fromHex("#58A6FF"), underline: true },
"markup.link.url": { fg: RGBA.fromHex("#58A6FF"), underline: true },
default: { fg: RGBA.fromHex("#E6EDF3") },
})
```
---
title: Diff
description: Unified or split diff viewer
order: 15
---
# Diff
Render unified or split diffs with syntax highlighting and optional line numbers.
## Basic usage
### Renderable API
```typescript
import { DiffRenderable, SyntaxStyle, RGBA, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromHex("#E6EDF3") },
string: { fg: RGBA.fromHex("#A5D6FF") },
keyword: { fg: RGBA.fromHex("#FF7B72"), bold: true },
})
const diff = new DiffRenderable(renderer, {
id: "diff",
width: "100%",
height: 16,
diff: `diff --git a/app.ts b/app.ts\nindex 1111111..2222222 100644\n--- a/app.ts\n+++ b/app.ts\n@@ -1,3 +1,3 @@\n-const a = 1\n+const a = 2\n`,
view: "split",
filetype: "typescript",
syntaxStyle,
showLineNumbers: true,
})
renderer.root.add(diff)
```
## Construct API
> Not available yet. Use `DiffRenderable` for now.
## Split view scroll sync
When `view: "split"` is active, you can link the vertical scroll of both panes. Scrolling one pane then moves the other:
```typescript
const diff = new DiffRenderable(renderer, {
id: "diff",
view: "split",
syncScroll: true,
diff: patch,
syntaxStyle,
})
// Toggle at runtime
diff.syncScroll = false
```
Scroll sync is a no-op in the unified view.
## Properties
| Property | Type | Default | Description |
| --------------------- | ------------------------------- | ----------- | ------------------------------------- |
| `diff` | `string` | `""` | Unified diff string |
| `view` | `"unified"` or `"split"` | `"unified"` | Layout style |
| `syncScroll` | `boolean` | `false` | Link vertical scroll in split view |
| `filetype` | `string` | - | Syntax highlighting language |
| `fg` | `string` or `RGBA` | - | Base foreground for inner code panes |
| `syntaxStyle` | `SyntaxStyle` | - | Syntax style for code |
| `wrapMode` | `"word"`, `"char"`, or `"none"` | - | Code wrapping mode |
| `conceal` | `boolean` | `false` | Conceal markup when highlighting |
| `selectionBg` | `string` or `RGBA` | - | Selection background in code panes |
| `selectionFg` | `string` or `RGBA` | - | Selection foreground in code panes |
| `treeSitterClient` | `TreeSitterClient` | - | Custom Tree-sitter client |
| `showLineNumbers` | `boolean` | `true` | Show line numbers |
| `lineNumberFg` | `string` or `RGBA` | `#888888` | Line number text color |
| `lineNumberBg` | `string` or `RGBA` | transparent | Line number background |
| `addedLineNumberBg` | `string` or `RGBA` | transparent | Line number background for added |
| `removedLineNumberBg` | `string` or `RGBA` | transparent | Line number background for removed |
| `addedBg` | `string` or `RGBA` | `#1a4d1a` | Background for added lines |
| `removedBg` | `string` or `RGBA` | `#4d1a1a` | Background for removed lines |
| `contextBg` | `string` or `RGBA` | transparent | Background for context lines |
| `addedContentBg` | `string` or `RGBA` | - | Optional content background (added) |
| `removedContentBg` | `string` or `RGBA` | - | Optional content background (removed) |
| `contextContentBg` | `string` or `RGBA` | - | Optional content background (context) |
| `addedSignColor` | `string` or `RGBA` | `#22c55e` | Sign color for added lines |
| `removedSignColor` | `string` or `RGBA` | `#ef4444` | Sign color for removed lines |
---
title: FrameBuffer
description: Low-level rendering surface for custom graphics
order: 13
---
# FrameBuffer
A low-level rendering surface for custom graphics and complex visual effects. FrameBuffer provides a 2D array of cells with methods optimized for performance and memory.
## Basic usage
### Renderable API
```typescript
import { FrameBufferRenderable, RGBA, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const canvas = new FrameBufferRenderable(renderer, {
id: "canvas",
width: 50,
height: 20,
})
// Draw on the frame buffer
canvas.frameBuffer.fillRect(5, 2, 20, 10, RGBA.fromHex("#FF0000"))
canvas.frameBuffer.drawText("Hello!", 8, 6, RGBA.fromHex("#FFFFFF"))
renderer.root.add(canvas)
```
### Construct API
```typescript
import { FrameBuffer, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
renderer.root.add(
FrameBuffer({
width: 50,
height: 20,
}),
)
```
## Drawing methods
### setCell
Set a single cell's content and colors:
```typescript
canvas.frameBuffer.setCell(
x, // X position
y, // Y position
char, // Character to display
fg, // Foreground color (RGBA)
bg, // Background color (RGBA)
attributes, // Text attributes (optional, default: 0)
)
// Example
canvas.frameBuffer.setCell(10, 5, "@", RGBA.fromHex("#FFFF00"), RGBA.fromHex("#000000"))
```
### setCellWithAlphaBlending
Set a cell with alpha blending for transparency effects:
```typescript
const semiTransparent = RGBA.fromValues(1.0, 0.0, 0.0, 0.5)
const transparent = RGBA.fromValues(0, 0, 0, 0)
canvas.frameBuffer.setCellWithAlphaBlending(10, 5, " ", transparent, semiTransparent)
```
### drawText
Draw a string of text at a position:
```typescript
canvas.frameBuffer.drawText(
text, // String to draw
x, // Starting X position
y, // Y position
fg, // Text color (RGBA)
bg, // Background color (RGBA, optional)
attributes, // Text attributes (optional, default: 0)
)
// Example
canvas.frameBuffer.drawText("Score: 100", 2, 1, RGBA.fromHex("#00FF00"))
```
### fillRect
Fill a rectangular area with a color:
```typescript
canvas.frameBuffer.fillRect(
x, // X position
y, // Y position
width, // Rectangle width
height, // Rectangle height
color, // Fill color (RGBA)
)
// Example: Draw a red rectangle
canvas.frameBuffer.fillRect(10, 5, 20, 8, RGBA.fromHex("#FF0000"))
```
### drawFrameBuffer
Copy another frame buffer onto this one:
```typescript
canvas.frameBuffer.drawFrameBuffer(
destX, // Destination X
destY, // Destination Y
sourceBuffer, // Source FrameBuffer (OptimizedBuffer)
sourceX, // Source X offset (optional)
sourceY, // Source Y offset (optional)
sourceWidth, // Width to copy (optional)
sourceHeight, // Height to copy (optional)
)
```
### colorMatrix / colorMatrixUniform
Apply native 4x4 RGBA matrix transforms for post-processing effects. Use `colorMatrixUniform` for full-buffer
transforms, and `colorMatrix` when you want to target specific cells.
```typescript
import { INVERT_MATRIX, TargetChannel } from "@opentui/core"
// Full-buffer transform
canvas.frameBuffer.colorMatrixUniform(INVERT_MATRIX, 1.0, TargetChannel.Both)
// Per-cell transform with explicit mask
const cellMask = new Float32Array([10, 5, 1.0, 11, 5, 0.5])
canvas.frameBuffer.colorMatrix(INVERT_MATRIX, cellMask, 1.0, TargetChannel.FG)
```
See [Color matrix reference](/docs/reference/color-matrix) for matrix layout, mask format, and behavior details.
## Properties
| Property | Type | Default | Description |
| -------------------------------- | --------- | ------------ | ------------------------------------- |
| `width` | `number` | - | Buffer width in characters (required) |
| `height` | `number` | - | Buffer height in rows (required) |
| `respectAlpha` | `boolean` | `false` | Enable alpha blending when drawing |
| `position` | `string` | `"relative"` | Positioning mode |
| `left`, `top`, `right`, `bottom` | `number` | - | Position offsets |
## Example: Simple game canvas
```typescript
import { FrameBufferRenderable, RGBA, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const gameCanvas = new FrameBufferRenderable(renderer, {
id: "game",
width: 40,
height: 20,
position: "absolute",
left: 5,
top: 2,
})
// Game state
let playerX = 20
let playerY = 10
function render() {
const fb = gameCanvas.frameBuffer
const BG = RGBA.fromHex("#111111")
// Clear the canvas
fb.fillRect(0, 0, 40, 20, BG)
// Draw border
for (let x = 0; x < 40; x++) {
fb.setCell(x, 0, "-", RGBA.fromHex("#444444"), BG)
fb.setCell(x, 19, "-", RGBA.fromHex("#444444"), BG)
}
for (let y = 0; y < 20; y++) {
fb.setCell(0, y, "|", RGBA.fromHex("#444444"), BG)
fb.setCell(39, y, "|", RGBA.fromHex("#444444"), BG)
}
// Draw player
fb.setCell(playerX, playerY, "@", RGBA.fromHex("#00FF00"), BG)
// Draw score
fb.drawText("Score: 0", 2, 0, RGBA.fromHex("#FFFF00"))
}
// Handle input
renderer.keyInput.on("keypress", (key) => {
switch (key.name) {
case "up":
playerY = Math.max(1, playerY - 1)
break
case "down":
playerY = Math.min(18, playerY + 1)
break
case "left":
playerX = Math.max(1, playerX - 1)
break
case "right":
playerX = Math.min(38, playerX + 1)
break
}
render()
})
render()
renderer.root.add(gameCanvas)
```
## Example: Progress bar
```typescript
const EMPTY_BG = RGBA.fromHex("#222222")
function drawProgressBar(fb, x, y, width, progress, color) {
const filled = Math.floor(width * progress)
// Draw filled portion
for (let i = 0; i < filled; i++) {
fb.setCell(x + i, y, "█", color, EMPTY_BG)
}
// Draw empty portion
for (let i = filled; i < width; i++) {
fb.setCell(x + i, y, "░", RGBA.fromHex("#333333"), EMPTY_BG)
}
}
// Usage
drawProgressBar(canvas.frameBuffer, 5, 10, 30, 0.75, RGBA.fromHex("#00FF00"))
```
## Performance tips
1. **Batch updates**: Make multiple changes to the frame buffer before the next render cycle
2. **Minimize fillRect calls**: Use individual setCell calls for complex shapes
3. **Reuse RGBA objects**: Create color constants instead of calling `fromHex` repeatedly
```typescript
// Good: Create once, reuse
const RED = RGBA.fromHex("#FF0000")
const GREEN = RGBA.fromHex("#00FF00")
const BG = RGBA.fromHex("#000000")
for (let i = 0; i < 100; i++) {
fb.setCell(i, 5, "*", RED, BG)
}
// Avoid: Creating new RGBA objects in loops
for (let i = 0; i < 100; i++) {
fb.setCell(i, 5, "*", RGBA.fromHex("#FF0000"), RGBA.fromHex("#000000")) // Creates 200 objects
}
```
---
title: Input
description: Text input field with cursor and focus states
order: 3
skill:
entry: true
intents: [input, form, editing, focus]
---
# Input
Text input field with cursor, placeholder text, and focus states. Focus the input to receive keyboard input.
## Basic usage
### Renderable api
```typescript
import { InputRenderable, InputRenderableEvents, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const input = new InputRenderable(renderer, {
id: "name-input",
width: 25,
placeholder: "Enter your name...",
})
input.on(InputRenderableEvents.CHANGE, (value) => {
console.log("Input value:", value)
})
input.focus()
renderer.root.add(input)
```
### Construct api
```typescript
import { Input, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const input = Input({
placeholder: "Enter your name...",
width: 25,
})
input.focus()
renderer.root.add(input)
```
## Focus states
The input changes appearance when focused:
```typescript
const input = new InputRenderable(renderer, {
id: "styled-input",
width: 30,
placeholder: "Type here...",
backgroundColor: "#1a1a1a",
focusedBackgroundColor: "#2a2a2a",
textColor: "#FFFFFF",
cursorColor: "#00FF00",
})
```
## Events
### Input event
Fires on every keystroke as the value changes:
```typescript
import { InputRenderableEvents } from "@opentui/core"
input.on(InputRenderableEvents.INPUT, (value: string) => {
console.log("Current value:", value)
})
```
### Change event
Fires when the input loses focus (blur) or when you press Enter, but only if the value changed since focus:
```typescript
input.on(InputRenderableEvents.CHANGE, (value: string) => {
console.log("Value committed:", value)
})
```
### Enter event
Fires when the user presses Enter/Return:
```typescript
input.on(InputRenderableEvents.ENTER, (value: string) => {
console.log("Submitted value:", value)
})
```
### Getting the current value
```typescript
const currentValue = input.value
```
### Setting the value
```typescript
input.value = "New value"
```
## Properties
| Property | Type | Default | Description |
| ------------------------ | -------------------------------------- | ------------ | ---------------------------- |
| `width` | `number` | - | Input field width |
| `value` | `string` | `""` | Initial text value |
| `placeholder` | `string` | `""` | Placeholder text when empty |
| `maxLength` | `number` | `1000` | Maximum number of characters |
| `backgroundColor` | `string \| RGBA` | - | Background when unfocused |
| `focusedBackgroundColor` | `string \| RGBA` | - | Background when focused |
| `textColor` | `string \| RGBA` | - | Text color |
| `cursorColor` | `string \| RGBA` | - | Cursor color |
| `position` | `"static" \| "relative" \| "absolute"` | `"relative"` | Positioning mode |
## Example: Login form
```typescript
import { Box, Text, Input, delegate, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
function LabeledInput(props: { id: string; label: string; placeholder: string }) {
return delegate(
{ focus: `${props.id}-input` },
Box(
{ flexDirection: "row", marginBottom: 1 },
Text({ content: props.label.padEnd(12), fg: "#888888" }),
Input({
id: `${props.id}-input`,
placeholder: props.placeholder,
width: 20,
backgroundColor: "#222",
focusedBackgroundColor: "#333",
textColor: "#FFF",
cursorColor: "#0F0",
}),
),
)
}
const usernameInput = LabeledInput({
id: "username",
label: "Username:",
placeholder: "Enter username",
})
const passwordInput = LabeledInput({
id: "password",
label: "Password:",
placeholder: "Enter password",
})
const form = Box(
{
width: 40,
borderStyle: "rounded",
title: "Login",
padding: 1,
},
usernameInput,
passwordInput,
)
// Focus the username input
usernameInput.focus()
renderer.root.add(form)
```
## Tab navigation
Add tab navigation between inputs:
```typescript
const inputs = [usernameInput, passwordInput]
let focusIndex = 0
renderer.keyInput.on("keypress", (key) => {
if (key.name === "tab") {
focusIndex = (focusIndex + 1) % inputs.length
inputs[focusIndex].focus()
}
})
```
---
title: Line numbers
description: Line number gutter for code and text renderables
order: 12
---
# Line numbers
Add a line number gutter to renderables that provide line info, such as `CodeRenderable` and text editor components.
## Basic usage
### Renderable API
```typescript
import {
CodeRenderable,
LineNumberRenderable,
ScrollBoxRenderable,
SyntaxStyle,
RGBA,
createCliRenderer,
} from "@opentui/core"
const renderer = await createCliRenderer()
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromHex("#E6EDF3") },
})
const code = new CodeRenderable(renderer, {
id: "code",
content: "const x = 1\nconst y = 2\n",
filetype: "typescript",
syntaxStyle,
width: "100%",
})
const lineNumbers = new LineNumberRenderable(renderer, {
id: "code-lines",
target: code,
minWidth: 3,
paddingRight: 1,
fg: "#6b7280",
bg: "#161b22",
})
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "scrollbox",
width: 70,
height: 18,
})
scrollbox.add(lineNumbers)
renderer.root.add(scrollbox)
```
## Line signs and colors
Set a single background for a line, or split gutter and content colors separately with a `LineColorConfig` object:
```typescript
// Shorthand: applies to both gutter and content
lineNumbers.setLineColor(3, "#2b6cb0")
// Split config: different gutter vs content background
lineNumbers.setLineColor(3, {
gutter: "#2b6cb0",
content: "#1f2937",
})
lineNumbers.setLineSign(3, { before: ">", beforeColor: "#2b6cb0" })
// Clear
lineNumbers.clearLineColor(3)
lineNumbers.clearLineSign(3)
```
## Theming the gutter
Gutter text and background colors accept assignments after construction:
```typescript
lineNumbers.fg = "#6b7280"
lineNumbers.bg = "#161b22"
```
Assign `undefined` to reset to the defaults (`#888888` foreground, transparent background).
## Construct API
> Not available yet. Use `LineNumberRenderable` for now.
## Properties
| Property | Type | Default | Description |
| ------------------ | ------------------------------------------------ | ----------- | ------------------------------ |
| `target` | `Renderable & LineInfoProvider` | - | Target renderable to number |
| `fg` | `string` or `RGBA` | `#888888` | Gutter text color |
| `bg` | `string` or `RGBA` | transparent | Gutter background color |
| `minWidth` | `number` | `3` | Minimum gutter width |
| `paddingRight` | `number` | `1` | Right padding for gutter |
| `lineColors` | `Map<number, string \| RGBA \| LineColorConfig>` | - | Per-line background colors |
| `lineSigns` | `Map<number, LineSign>` | - | Per-line signs (before/after) |
| `lineNumberOffset` | `number` | `0` | Offset for line numbering |
| `hideLineNumbers` | `Set<number>` | - | Lines to hide numbers for |
| `lineNumbers` | `Map<number, number>` | - | Override line numbers per line |
| `showLineNumbers` | `boolean` | `true` | Toggle gutter visibility |
## Methods
| Method | Description |
| ---------------------- | ------------------------------------- |
| `setLineColor()` | Set a background color for a line |
| `clearLineColor()` | Clear a line background color |
| `setLineSign()` | Set a sign before/after a line number |
| `clearLineSign()` | Clear a line sign |
| `setLineNumbers()` | Override multiple line numbers |
| `setHideLineNumbers()` | Hide line numbers for specific lines |
---
title: Markdown
description: Render markdown with syntax-aware styling
order: 11
---
# Markdown
Render markdown content with syntax-aware styling and optional Tree-sitter highlighting for code blocks.
## Basic usage
### Renderable API
````typescript
import { MarkdownRenderable, SyntaxStyle, RGBA, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const syntaxStyle = SyntaxStyle.fromStyles({
"markup.heading.1": { fg: RGBA.fromHex("#58A6FF"), bold: true },
"markup.list": { fg: RGBA.fromHex("#FF7B72") },
"markup.raw": { fg: RGBA.fromHex("#A5D6FF") },
default: { fg: RGBA.fromHex("#E6EDF3") },
})
const markdown = new MarkdownRenderable(renderer, {
id: "readme",
width: 60,
content: "# Hello\n\n- One\n- Two\n\n```ts\nconst x = 1\n```",
syntaxStyle,
})
renderer.root.add(markdown)
````
## Fenced language normalization
Code fence info strings are normalized before Tree-sitter highlighting.
- `tsx` -> `typescriptreact`
- `.jsx` -> `javascriptreact`
- `TSX title=Button.tsx` -> `typescriptreact`
- `Dockerfile` -> `dockerfile`
Normalization uses `infoStringToFiletype()`. You can extend or override mappings at runtime:
```typescript
import { extensionToFiletype, basenameToFiletype } from "@opentui/core"
extensionToFiletype.set("templ", "html")
basenameToFiletype.set("mytoolrc", "yaml")
```
## Concealment
Hide markdown markers (backticks, emphasis markers, etc.) when `conceal` is true:
```typescript
const markdown = new MarkdownRenderable(renderer, {
content: "**bold** and `code`",
syntaxStyle,
conceal: true,
})
```
Use `concealCode` to control concealment inside fenced code blocks independently (`false` by default).
## Streaming updates
Enable streaming mode for incremental updates. Keep it `true` while appending chunks, then set `markdown.streaming = false` when complete to finalize trailing block parsing. Tables include trailing partial rows, with missing cells rendered empty.
```typescript
const markdown = new MarkdownRenderable(renderer, {
content: "",
syntaxStyle,
streaming: true,
})
markdown.content += "# Live log\n"
markdown.content += "- line 1\n"
markdown.streaming = false
```
### Stable block prefix
`parseMarkdownIncremental` reports how many parsed tokens at the head of the stream are stable — that is, unlikely to change if you append more content. The renderable surfaces this as a count of **top-level render blocks** you can safely commit elsewhere while the unstable tail keeps updating.
To use it, render with `internalBlockMode: "top-level"`. The renderable keeps each top-level markdown block (heading, paragraph, list, table, fenced code) as its own child renderable, and exposes `markdown._stableBlockCount` — the number of blocks at the head of the tree that are stable right now. This matches the shape [`ScrollbackSurface`](/docs/core-concepts/renderer#writing-to-scrollback) expects when you commit streamed output row-by-row.
```typescript
import { MarkdownRenderable, RGBA, SyntaxStyle } from "@opentui/core"
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromHex("#E6EDF3") },
})
const md = new MarkdownRenderable(renderer, {
content: "",
syntaxStyle,
streaming: true,
internalBlockMode: "top-level",
})
md.content = "# Title\n\nPara 1"
// md._stableBlockCount might be 1 here — "# Title" is settled, "Para 1" could still grow
md.content = "# Title\n\nPara 1\n\nPara 2"
// md._stableBlockCount rises as earlier blocks are sealed by a blank line
```
`internalBlockMode` is an experimental, opt-in flag that powers the built-in scrollback streaming demo. For non-streaming rendering, leave it at the default (`"coalesced"`), which folds sibling blocks together and matches the historical layout.
## Markdown tables
Markdown tables support `tableOptions` for style, sizing, wrapping, borders, and selection.
```typescript
const markdown = new MarkdownRenderable(renderer, {
content: "| Service | Status |\n| --- | --- |\n| api | ok |",
syntaxStyle,
tableOptions: {
style: "grid",
widthMode: "full",
columnFitter: "balanced",
wrapMode: "word",
cellPadding: 1,
borders: true,
outerBorder: true,
borderStyle: "rounded",
borderColor: "#6b7280",
selectable: true,
},
})
```
### `tableOptions`
| Option | Type | Default | Description |
| -------------- | ------------------------------ | ----------------------- | ------------------------------------------------- |
| `style` | `"grid" \| "columns"` | depends on block mode | Visual preset (see [Table styles](#table-styles)) |
| `widthMode` | `"content" \| "full"` | depends on style | `"full"` expands columns to fill available width |
| `columnFitter` | `"proportional" \| "balanced"` | `"proportional"` | How columns shrink when space is constrained |
| `wrapMode` | `"none" \| "char" \| "word"` | `"word"` | Wrapping mode inside each table cell |
| `cellPadding` | `number` | `0` | Padding on all sides of each cell |
| `borders` | `boolean` | depends on style | Enable inner and outer borders |
| `outerBorder` | `boolean` | `borders` | Override outer border visibility |
| `borderStyle` | `BorderStyle` | `"single"` | Table border character set |
| `borderColor` | `ColorInput` | conceal fg or `#888888` | Border color for markdown tables |
| `selectable` | `boolean` | `true` | Enable table cell text selection |
### Table styles
`tableOptions.style` picks a preset that tunes the defaults for `borders`, `outerBorder`, and `widthMode` together:
- **`"grid"`**: boxed table with visible borders. Defaults to `borders: true`, `widthMode: "full"`. This is the normal markdown-in-a-box rendering.
- **`"columns"`**: borderless columns with a 2-column gap, defaults to `widthMode: "content"`. Useful for append-only output where a full-width grid feels heavy.
If you do not pass `style`, it defaults to `"columns"` when `internalBlockMode` is `"top-level"`, and `"grid"` otherwise. You can still override individual fields (for example, `borders: true`) to pull toward a different look.
## Custom node rendering
Override rendering for a token and fall back to default rendering:
```typescript
const markdown = new MarkdownRenderable(renderer, {
content: "# Title\n\nHello",
syntaxStyle,
renderNode: (token, context) => {
if (token.type === "heading") {
return context.defaultRender()
}
return undefined
},
})
```
### Custom fenced-code languages
Use `createMarkdownCodeBlockRenderer` when you only want to replace specific fenced-code languages. The language key is matched against the normalized fence info string, so a `tsx` fence maps to `typescriptreact`, and custom DSL names like `taskflow` can be matched directly.
````typescript
import { BoxRenderable, MarkdownRenderable, TextRenderable, createMarkdownCodeBlockRenderer } from "@opentui/core"
const renderTaskFlow = (renderer) => (token) => {
const steps = token.text
.split("\n")
.filter((line) => line.startsWith("step "))
.map((line) => line.slice("step ".length))
const card = new BoxRenderable(renderer, {
border: true,
borderStyle: "rounded",
borderColor: "#38BDF8",
paddingX: 1,
flexDirection: "column",
width: "100%",
})
for (const step of steps) {
card.add(new TextRenderable(renderer, { content: `- ${step}`, width: "100%" }))
}
return card
}
const markdown = new MarkdownRenderable(renderer, {
content: "```taskflow\nstep Parse markdown done\nstep Render widget active\n```",
syntaxStyle,
renderNode: createMarkdownCodeBlockRenderer({
taskflow: renderTaskFlow(renderer),
}),
})
````
## Construct API
> Not available yet. Use `MarkdownRenderable` for now.
## Properties
| Property | Type | Default | Description |
| ------------------- | ------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------- |
| `content` | `string` | `""` | Markdown source |
| `syntaxStyle` | `SyntaxStyle` | required | Style definitions for tokens |
| `fg` | `ColorInput` | - | Base foreground color (flows into inner code blocks) |
| `bg` | `ColorInput` | - | Base background color (flows into inner code blocks) |
| `conceal` | `boolean` | `true` | Hide markdown markers in markdown text |
| `concealCode` | `boolean` | `false` | Hide markers inside fenced code blocks |
| `streaming` | `boolean` | `false` | Incremental mode; set `false` to finalize |
| `tableOptions` | `MarkdownTableOptions` | - | Options for markdown table rendering |
| `internalBlockMode` | `"coalesced" \| "top-level"` | `"coalesced"` | Experimental: expose top-level blocks as separate renderables |
| `treeSitterClient` | `TreeSitterClient` | - | Custom Tree-sitter client for code blocks |
| `renderNode` | `(token: Token, context: RenderNodeContext) => Renderable \| null \| undefined` | - | Custom render hook per markdown block |
---
title: QR Code
description: Display a scannable QR code from text content
order: 16
---
# QR Code
Display text or URLs as scannable standard QR Codes in the terminal. `QRCodeRenderable` renders QR Code Model 2 modules with half-block cells so the symbol stays square in terminal character geometry.
Install the QR code package separately:
```bash
bun install @opentui/qrcode
```
## Basic Usage
### Renderable API
```typescript
import { createCliRenderer } from "@opentui/core"
import { QRCodeRenderable } from "@opentui/qrcode"
const renderer = await createCliRenderer()
const qr = new QRCodeRenderable(renderer, {
id: "docs-link",
content: "https://opentui.com/docs/getting-started",
quietZone: 4,
scale: 2,
})
renderer.root.add(qr)
```
### React JSX
```tsx
import { createRoot } from "@opentui/react"
import { registerQRCode } from "@opentui/qrcode/react"
registerQRCode()
createRoot(renderer).render(
<qr-code
content="https://opentui.com/docs/getting-started"
quietZone={4}
scale={2}
foregroundColor="#111827"
backgroundColor="#ffffff"
/>,
)
```
### Solid JSX
```tsx
import { render } from "@opentui/solid"
import { registerQRCode } from "@opentui/qrcode/solid"
registerQRCode()
render(() => (
<qr_code
content="https://opentui.com/docs/getting-started"
quietZone={4}
scale={2}
foregroundColor="#111827"
backgroundColor="#ffffff"
/>
))
```
## Sizing
`QRCodeRenderable` measures itself from the encoded QR version, the quiet zone, and the requested scale. The default `fit: "contain"` lets the QR code shrink to fit constrained parents while preserving a valid square module grid.
```typescript
const qr = new QRCodeRenderable(renderer, {
content: "opentui.com",
quietZone: 4,
scale: 2,
fit: "contain",
})
```
Use `fit: "none"` when you want the configured scale to be the only rendered size.
`quietZone` must be at least 4 modules for standard QR Code. OpenTUI validates the matrix and quiet-zone geometry, but scanner reliability still depends on terminal font, cell aspect ratio, display contrast, and camera conditions.
## Fallback Content
When a container is too small to render even a scale-1 QR code, show fallback text instead of an empty area:
```typescript
const qr = new QRCodeRenderable(renderer, {
content: "https://opentui.com/docs/getting-started",
fallbackContent: "Resize for QR",
fallbackColor: "#94a3b8",
})
```
## Error Correction
Set `errorCorrectionLevel` with the QR package error correction enum:
```typescript
import { ErrorCorrectionLevel, QRCodeRenderable } from "@opentui/qrcode"
const qr = new QRCodeRenderable(renderer, {
content: "opentui.com",
errorCorrectionLevel: ErrorCorrectionLevel.H,
})
```
Higher error correction improves resilience but can increase the QR version and rendered size for longer content.
## Properties
| Property | Type | Default | Description |
| ---------------------- | ---------------------- | ----------- | ------------------------------------------------ |
| `content` | `string` | `""` | Text content to encode |
| `errorCorrectionLevel` | `ErrorCorrectionLevel` | `M` | QR error correction level |
| `quietZone` | `number` | `4` | Blank module border around the QR code |
| `scale` | `number` | `1` | Terminal columns per QR module before fitting |
| `fit` | `"contain" \| "none"` | `"contain"` | Whether the QR code may shrink to fit its parent |
| `foregroundColor` | `ColorInput` | `"#000000"` | Dark module color |
| `backgroundColor` | `ColorInput` | `"#ffffff"` | Light module and quiet-zone color |
| `fallbackContent` | `string` | `""` | Text rendered when the QR code cannot fit |
| `fallbackColor` | `ColorInput` | `"#ffffff"` | Fallback text color |
---
title: ScrollBar
description: Standalone scrollbar with arrows and keyboard control
order: 8
---
# ScrollBar
A standalone scrollbar component with optional arrows, keyboard navigation, and a draggable thumb.
## Basic usage
### Renderable API
```typescript
import { ScrollBarRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const scrollbar = new ScrollBarRenderable(renderer, {
id: "scrollbar",
orientation: "vertical",
height: 10,
showArrows: true,
trackOptions: {
backgroundColor: "#222222",
foregroundColor: "#888888",
},
onChange: (position) => {
console.log("Scroll position:", position)
},
})
// Connect the scrollbar to your content
scrollbar.scrollSize = 200
scrollbar.viewportSize = 20
scrollbar.scrollPosition = 0
renderer.root.add(scrollbar)
scrollbar.focus()
```
## Keyboard controls
When focused, the scrollbar responds to:
- `Up`/`Down` or `k`/`j` for vertical bars
- `Left`/`Right` or `h`/`l` for horizontal bars
- `PageUp`/`PageDown` for larger jumps
- `Home`/`End` to jump to start/end
## Construct API
> Not available yet. Use `ScrollBarRenderable` for now.
## Properties
| Property | Type | Default | Description |
| ---------------- | ------------------------------ | ------- | -------------------------------------- |
| `orientation` | `"vertical"` or `"horizontal"` | - | Scrollbar direction |
| `showArrows` | `boolean` | `false` | Show arrow buttons |
| `arrowOptions` | `ArrowOptions` | - | Styling for arrow buttons |
| `trackOptions` | `Partial<SliderOptions>` | - | Styling for the track and thumb |
| `scrollSize` | `number` | `0` | Total scrollable size |
| `viewportSize` | `number` | `0` | Visible size of the viewport |
| `scrollPosition` | `number` | `0` | Current scroll position |
| `scrollStep` | `number` | - | Step size when `scrollBy(..., "step")` |
| `onChange` | `(position: number) => void` | - | Fired when scroll position changes |
---
title: ScrollBox
description: A scrollable container component with customizable scrollbars
order: 7
---
# ScrollBox
A scrollable container that supports horizontal and vertical scrolling, sticky scroll behavior, viewport culling, and customizable scrollbars.
## Basic usage
### Renderable api
```typescript
import { ScrollBoxRenderable, TextRenderable, BoxRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "scrollbox",
width: 40,
height: 20,
})
// Add content to the scrollbox
for (let i = 0; i < 100; i++) {
scrollbox.add(
new BoxRenderable(renderer, {
id: `item-${i}`,
width: "100%",
height: 2,
backgroundColor: i % 2 === 0 ? "#292e42" : "#2f3449",
}),
)
}
renderer.root.add(scrollbox)
```
### Construct API
```typescript
import { ScrollBox, Box, Text, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
renderer.root.add(
ScrollBox(
{
width: 40,
height: 20,
},
...Array.from({ length: 100 }, (_, i) =>
Box(
{ width: "100%", padding: 1, backgroundColor: i % 2 === 0 ? "#292e42" : "#2f3449" },
Text({ content: `Item ${i}` }),
),
),
),
)
```
## Sticky scroll
Enable sticky scroll to keep content pinned to an edge as new content arrives. Useful for log viewers or chat interfaces.
```typescript
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "logs",
width: 60,
height: 20,
stickyScroll: true,
stickyStart: "bottom", // New content will keep the view scrolled to bottom
})
```
### Sticky positions
- `"bottom"` - Stay scrolled to bottom (default for chat/logs)
- `"top"` - Stay scrolled to top
- `"left"` - Stay scrolled to left
- `"right"` - Stay scrolled to right
When you scroll away from the sticky position, sticky behavior pauses until you scroll back to the sticky edge.
## Bidirectional scrolling
Enable scrolling in both directions:
```typescript
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "canvas",
width: 60,
height: 30,
scrollX: true,
scrollY: true,
})
```
By default, `scrollY` is `true` and `scrollX` is `false`.
## Viewport culling
Enable viewport culling for better performance with large content. When enabled, ScrollBox renders only visible children:
```typescript
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "large-list",
width: 40,
height: 20,
viewportCulling: true, // Only render visible items
})
```
Viewport culling skips render calls for offscreen children, so their `renderBefore` and `renderAfter` hooks do not run. Do not make layout or state depend on render hooks; disable `viewportCulling` if every child must run a render hook.
## Customizing scrollbars
Style the scrollbars using nested options:
```typescript
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "styled-scroll",
width: 40,
height: 20,
scrollbarOptions: {
showArrows: true,
trackOptions: {
foregroundColor: "#7aa2f7",
backgroundColor: "#414868",
},
},
// Or customize vertical and horizontal separately
verticalScrollbarOptions: {
trackOptions: { backgroundColor: "#333" },
},
horizontalScrollbarOptions: {
trackOptions: { backgroundColor: "#333" },
},
})
```
## Customizing sub-components
ScrollBox contains several internal components that you can style individually:
```typescript
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "custom-scroll",
width: 40,
height: 20,
rootOptions: {
backgroundColor: "#24283b",
},
wrapperOptions: {
backgroundColor: "#1f2335",
},
viewportOptions: {
backgroundColor: "#1a1b26",
},
contentOptions: {
backgroundColor: "#16161e",
},
})
```
## Scroll methods
### scrollBy
Scroll by a relative amount:
```typescript
// Scroll down 5 lines
scrollbox.scrollBy(5)
// Scroll with both x and y
scrollbox.scrollBy({ x: 10, y: 5 })
// Scroll by viewport (page)
scrollbox.scrollBy(1, "viewport")
```
### scrollTo
Scroll to an absolute position:
```typescript
// Scroll to top
scrollbox.scrollTo(0)
// Scroll to specific position
scrollbox.scrollTo({ x: 0, y: 100 })
```
### scrollChildIntoView
Scroll the minimum distance needed to show a nested child in the viewport. The method uses DOM-style "nearest" behavior: if the child already fits, the call is a no-op; otherwise it scrolls to reveal the nearest edge.
```typescript
scrollbox.scrollChildIntoView("table-row-42")
```
Use it when you focus an element offscreen (search results, a newly inserted form field, etc.) and want to bring it into view without moving the scroll position more than needed.
## Keyboard navigation
When focused, ScrollBox responds to keyboard input:
- **Arrow keys** - Scroll by line
- **Page Up/Down** - Scroll by page
- **Home/End** - Scroll to start/end
## Properties
| Property | Type | Default | Description |
| ---------------------------- | ---------------------------------------- | ------- | -------------------------------------- |
| `scrollX` | `boolean` | `false` | Enable horizontal scrolling |
| `scrollY` | `boolean` | `true` | Enable vertical scrolling |
| `stickyScroll` | `boolean` | `false` | Keep scroll position pinned to an edge |
| `stickyStart` | `"top" \| "bottom" \| "left" \| "right"` | - | Which edge to stick to |
| `viewportCulling` | `boolean` | `true` | Only render visible children |
| `scrollAcceleration` | `ScrollAcceleration` | - | Custom scroll acceleration algorithm |
| `rootOptions` | `BoxOptions` | - | Style options for root container |
| `wrapperOptions` | `BoxOptions` | - | Style options for wrapper |
| `viewportOptions` | `BoxOptions` | - | Style options for viewport |
| `contentOptions` | `BoxOptions` | - | Style options for content container |
| `scrollbarOptions` | `ScrollBarOptions` | - | Options for both scrollbars |
| `verticalScrollbarOptions` | `ScrollBarOptions` | - | Options for vertical scrollbar only |
| `horizontalScrollbarOptions` | `ScrollBarOptions` | - | Options for horizontal scrollbar only |
## Additional properties
| Property | Type | Description |
| -------------- | -------- | -------------------------------------------- |
| `scrollTop` | `number` | Current vertical scroll position (get/set) |
| `scrollLeft` | `number` | Current horizontal scroll position (get/set) |
| `scrollWidth` | `number` | Total scrollable width (read-only) |
| `scrollHeight` | `number` | Total scrollable height (read-only) |
## Internal components
ScrollBox exposes its internal components for advanced use:
```typescript
scrollbox.wrapper // BoxRenderable - outer wrapper
scrollbox.viewport // BoxRenderable - visible area
scrollbox.content // ContentRenderable - holds children
scrollbox.horizontalScrollBar // ScrollBarRenderable
scrollbox.verticalScrollBar // ScrollBarRenderable
```
---
title: Select
description: List selection component for choosing from options
order: 5
---
# Select
A vertical list for choosing from multiple options. Focus the select to enable keyboard input.
## Basic usage
### Renderable API
```typescript
import { SelectRenderable, SelectRenderableEvents, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const menu = new SelectRenderable(renderer, {
id: "menu",
width: 30,
height: 8,
options: [
{ name: "New File", description: "Create a new file" },
{ name: "Open File", description: "Open an existing file" },
{ name: "Save", description: "Save current file" },
{ name: "Exit", description: "Exit the application" },
],
})
menu.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {
console.log("Selected:", option.name)
})
menu.focus()
renderer.root.add(menu)
```
### Construct API
```typescript
import { Select, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const menu = Select({
width: 30,
height: 8,
options: [
{ name: "Option 1", description: "First option" },
{ name: "Option 2", description: "Second option" },
{ name: "Option 3", description: "Third option" },
],
})
menu.focus()
renderer.root.add(menu)
```
## Keyboard navigation
When focused, the select responds to these keys:
| 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 when the user presses Enter on an option:
```typescript
import { SelectRenderableEvents } from "@opentui/core"
menu.on(SelectRenderableEvents.ITEM_SELECTED, (index: number, option: SelectOption) => {
console.log(`Selected index ${index}: ${option.name}`)
})
```
### Selection changed
Fires when the highlighted item changes:
```typescript
menu.on(SelectRenderableEvents.SELECTION_CHANGED, (index: number, option: SelectOption) => {
console.log(`Highlighted: ${option.name}`)
// Update a preview pane, for example
})
```
## Option structure
```typescript
interface SelectOption {
name: string // Display text
description: string // Displays below the name when showDescription is true
value?: any // Optional value
}
```
## Styling
```typescript
const styledMenu = new SelectRenderable(renderer, {
id: "styled-menu",
width: 40,
height: 10,
options: [...],
backgroundColor: "#1a1a1a",
selectedBackgroundColor: "#333366",
selectedTextColor: "#FFFFFF",
textColor: "#AAAAAA",
descriptionColor: "#666666",
})
```
## Properties
| Property | Type | Default | Description |
| -------------------------- | ---------------- | ------------- | --------------------------------- |
| `width` | `number` | - | Component width |
| `height` | `number` | - | Component height |
| `options` | `SelectOption[]` | `[]` | Available options |
| `selectedIndex` | `number` | `0` | Initially selected index |
| `backgroundColor` | `string \| RGBA` | `transparent` | Background color |
| `textColor` | `string \| RGBA` | `#FFFFFF` | Normal text color |
| `focusedBackgroundColor` | `string \| RGBA` | `#1a1a1a` | Background when focused |
| `focusedTextColor` | `string \| RGBA` | `#FFFFFF` | Text color when focused |
| `selectedBackgroundColor` | `string \| RGBA` | `#334455` | Selected item background |
| `selectedTextColor` | `string \| RGBA` | `#FFFF00` | Selected item text color |
| `descriptionColor` | `string \| RGBA` | `#888888` | Description text color |
| `selectedDescriptionColor` | `string \| RGBA` | `#CCCCCC` | Selected item description color |
| `showDescription` | `boolean` | `true` | Show option descriptions |
| `showScrollIndicator` | `boolean` | `false` | Show scroll position indicator |
| `wrapSelection` | `boolean` | `false` | Wrap selection at list boundaries |
| `itemSpacing` | `number` | `0` | Spacing between items |
| `fastScrollStep` | `number` | `5` | Items to skip with Shift+Up/Down |
## Example: file menu
```typescript
import { Box, Select, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const fileMenu = Select({
width: 25,
height: 12,
options: [
{ name: "New", description: "Create new file (Ctrl+N)" },
{ name: "Open...", description: "Open file (Ctrl+O)" },
{ name: "Save", description: "Save file (Ctrl+S)" },
{ name: "Save As...", description: "Save with new name" },
{ name: "---", description: "" }, // Separator (visual only)
{ name: "Exit", description: "Quit application (Ctrl+Q)" },
],
})
const menuPanel = Box(
{
borderStyle: "single",
borderColor: "#666",
},
fileMenu,
)
fileMenu.focus()
renderer.root.add(menuPanel)
```
## Programmatic control
```typescript
// Get current selection index
const currentIndex = menu.getSelectedIndex()
// Get currently selected option
const option = menu.getSelectedOption()
// Set selection programmatically
menu.setSelectedIndex(2)
// Navigate programmatically
menu.moveUp() // Move up one item
menu.moveDown() // Move down one item
menu.moveUp(3) // Move up multiple items
menu.selectCurrent() // Trigger selection of current item
// Update options dynamically
menu.options = [
{ name: "New Option 1", description: "First" },
{ name: "New Option 2", description: "Second" },
]
// Toggle display options
menu.showDescription = false
menu.showScrollIndicator = true
menu.wrapSelection = true
```
---
title: Slider
description: A draggable slider for continuous values
order: 9
---
# Slider
A draggable slider for continuous values. Supports vertical and horizontal orientations.
## Basic usage
### Renderable API
```typescript
import { SliderRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const slider = new SliderRenderable(renderer, {
id: "volume",
orientation: "horizontal",
width: 30,
height: 1,
min: 0,
max: 100,
value: 25,
onChange: (value) => {
console.log("Value:", value)
},
})
renderer.root.add(slider)
```
## Vertical slider
```typescript
const slider = new SliderRenderable(renderer, {
orientation: "vertical",
width: 2,
height: 10,
min: 0,
max: 1,
value: 0.5,
})
```
## Construct API
> Not available yet. Use `SliderRenderable` for now.
## Properties
| Property | Type | Default | Description |
| ----------------- | ------------------------------ | ------------ | ------------------------------ |
| `orientation` | `"vertical"` or `"horizontal"` | - | Slider direction |
| `value` | `number` | `min` | Current value |
| `min` | `number` | `0` | Minimum value |
| `max` | `number` | `100` | Maximum value |
| `viewPortSize` | `number` | range \* 0.1 | Thumb size relative to content |
| `backgroundColor` | `string` or `RGBA` | - | Track color |
| `foregroundColor` | `string` or `RGBA` | - | Thumb color |
| `onChange` | `(value: number) => void` | - | Fired when value changes |
---
title: TabSelect
description: Horizontal tab-based selection component
order: 6
---
# TabSelect
Horizontal tab-based selection component with descriptions and scroll support. The component must be focused to receive keyboard input.
## Basic Usage
### Renderable API
```typescript
import { TabSelectRenderable, TabSelectRenderableEvents, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const tabs = new TabSelectRenderable(renderer, {
id: "tabs",
width: 60,
options: [
{ name: "Home", description: "Dashboard and overview" },
{ name: "Files", description: "File management" },
{ name: "Settings", description: "Application settings" },
],
tabWidth: 20,
})
tabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index, option) => {
console.log("Tab selected:", option.name)
})
tabs.focus()
renderer.root.add(tabs)
```
### Construct API
```typescript
import { TabSelect, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const tabs = TabSelect({
width: 60,
tabWidth: 15,
options: [
{ name: "Tab 1", description: "First tab" },
{ name: "Tab 2", description: "Second tab" },
{ name: "Tab 3", description: "Third tab" },
],
})
tabs.focus()
renderer.root.add(tabs)
```
## Keyboard Navigation
When focused, the tab select responds to these keys:
| Key | Action |
| ------------- | -------------------- |
| `Left` / `[` | Move to previous tab |
| `Right` / `]` | Move to next tab |
| `Enter` | Select current tab |
## Events
### Item Selected
Emitted when the user presses Enter on a tab:
```typescript
import { TabSelectRenderableEvents, type TabSelectOption } from "@opentui/core"
tabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index: number, option: TabSelectOption) => {
console.log(`Selected tab ${index}: ${option.name}`)
// Switch to the corresponding panel
})
```
### Selection Changed
Emitted when the highlighted tab changes:
```typescript
import { TabSelectRenderableEvents, type TabSelectOption } from "@opentui/core"
tabs.on(TabSelectRenderableEvents.SELECTION_CHANGED, (index: number, option: TabSelectOption) => {
console.log(`Hovering: ${option.name}`)
})
```
## Properties
| Property | Type | Default | Description |
| -------------------------- | ------------------------ | ------------- | ------------------------------ |
| `width` | `number` | - | Total component width |
| `options` | `TabSelectOption[]` | `[]` | Available tabs |
| `tabWidth` | `number` | `20` | Width of each tab |
| `backgroundColor` | `string \| RGBA` | `transparent` | Background color |
| `textColor` | `string \| RGBA` | `#FFFFFF` | Normal tab text |
| `focusedBackgroundColor` | `string \| RGBA` | `#1a1a1a` | Background color when focused |
| `focusedTextColor` | `string \| RGBA` | `#FFFFFF` | Text color when focused |
| `selectedBackgroundColor` | `string \| RGBA` | `#334455` | Selected tab background |
| `selectedTextColor` | `string \| RGBA` | `#FFFF00` | Selected tab text |
| `selectedDescriptionColor` | `string \| RGBA` | `#CCCCCC` | Description text color |
| `showScrollArrows` | `boolean` | `true` | Show scroll indicators |
| `showDescription` | `boolean` | `true` | Show tab descriptions |
| `showUnderline` | `boolean` | `true` | Show underline on selected tab |
| `wrapSelection` | `boolean` | `false` | Wrap around when navigating |
| `keyBindings` | `TabSelectKeyBinding[]` | - | Custom key bindings |
| `keyAliasMap` | `Record<string, string>` | - | Key alias mappings |
## Example: Tabbed Interface
```typescript
import { Box, Text, TabSelect, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
// Create content panels
const panels = {
home: Box({ padding: 1 }, Text({ content: "Home content here" })),
files: Box({ padding: 1 }, Text({ content: "File browser here" })),
settings: Box({ padding: 1 }, Text({ content: "Settings form here" })),
}
// Create the tabbed container
const container = Box({
width: 60,
height: 20,
borderStyle: "rounded",
})
const tabs = TabSelect({
width: 60,
tabWidth: 20,
options: [
{ name: "Home", description: "Dashboard" },
{ name: "Files", description: "Browse files" },
{ name: "Settings", description: "Preferences" },
],
})
// Content area
let currentPanel = panels.home
const contentArea = Box({
flexGrow: 1,
padding: 1,
})
contentArea.add(currentPanel)
// Handle tab changes
tabs.on("itemSelected", (index, option) => {
// Remove current panel
if (currentPanel) {
contentArea.remove(currentPanel.id)
}
// Add new panel based on selection
switch (option.name) {
case "Home":
currentPanel = panels.home
break
case "Files":
currentPanel = panels.files
break
case "Settings":
currentPanel = panels.settings
break
}
contentArea.add(currentPanel)
})
container.add(tabs)
container.add(contentArea)
tabs.focus()
renderer.root.add(container)
```
## Programmatic Control
```typescript
// Get current tab index
const currentIndex = tabs.getSelectedIndex()
// Set tab programmatically
tabs.setSelectedIndex(1)
// Update tabs dynamically
tabs.setOptions([
{ name: "New Tab 1", description: "Updated" },
{ name: "New Tab 2", description: "Also updated" },
])
```
## Scroll Behavior
When there are more tabs than fit in the width, the component automatically handles horizontal scrolling as you navigate with the keyboard.
---
title: Text
description: Display styled text content
order: 1
skill:
entry: true
intents: [text, styling, content, selection]
---
# Text
Display styled text content with support for colors, attributes, and text selection.
## Basic Usage
### Renderable API
```typescript
import { TextRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const text = new TextRenderable(renderer, {
id: "greeting",
content: "Hello, OpenTUI!",
fg: "#00FF00",
})
renderer.root.add(text)
```
### Construct API
```typescript
import { Text, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
renderer.root.add(
Text({
content: "Hello, OpenTUI!",
fg: "#00FF00",
}),
)
```
## Text attributes
Combine multiple text attributes using bitwise OR:
```typescript
import { TextRenderable, TextAttributes } from "@opentui/core"
const styledText = new TextRenderable(renderer, {
id: "styled",
content: "Important Message",
fg: "#FFFF00",
attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,
})
```
### Available attributes
| Attribute | Description |
| ------------------------------ | ------------------ |
| `TextAttributes.BOLD` | Bold text |
| `TextAttributes.DIM` | Dimmed text |
| `TextAttributes.ITALIC` | Italic text |
| `TextAttributes.UNDERLINE` | Underlined text |
| `TextAttributes.BLINK` | Blinking text |
| `TextAttributes.INVERSE` | Inverted colors |
| `TextAttributes.HIDDEN` | Hidden text |
| `TextAttributes.STRIKETHROUGH` | Strikethrough text |
## Template literals for rich text
Use the `t` template literal for inline styling within a single text element:
```typescript
import { TextRenderable, t, bold, underline, fg, bg, italic } from "@opentui/core"
const richText = new TextRenderable(renderer, {
id: "rich",
content: t`${bold("Important:")} ${fg("#FF0000")(underline("Warning!"))} Normal text`,
})
```
### Available style functions
```typescript
import { t, bold, dim, italic, underline, blink, reverse, strikethrough, fg, bg } from "@opentui/core"
// Basic attributes
t`${bold("bold text")}`
t`${italic("italic text")}`
t`${underline("underlined")}`
t`${strikethrough("deleted")}`
// Colors
t`${fg("#FF0000")("red text")}`
t`${bg("#0000FF")("blue background")}`
// Combining styles
t`${bold(fg("#FFFF00")("bold yellow"))}`
```
## Positioning
```typescript
const text = new TextRenderable(renderer, {
id: "positioned",
content: "Absolute position",
position: "absolute",
left: 10,
top: 5,
})
```
## Text selection
Enable text selection for copying:
```typescript
const selectableText = new TextRenderable(renderer, {
id: "selectable",
content: "Select me!",
selectable: true, // Default is true
})
const nonSelectable = new TextRenderable(renderer, {
id: "label",
content: "Button Label",
selectable: false, // Disable selection
})
```
## Properties
| Property | Type | Default | Description |
| -------------------------------- | --------------------------------- | ------------ | ---------------------------- |
| `content` | `string \| StyledText` | `""` | The text content to display |
| `fg` | `string \| RGBA` | - | Foreground (text) color |
| `bg` | `string \| RGBA` | - | Background color |
| `attributes` | `TextAttributes` | `0` | Text styling attributes |
| `selectable` | `boolean` | `true` | Whether text can be selected |
| `position` | `"relative" \| "absolute"` | `"relative"` | Positioning mode |
| `left`, `top`, `right`, `bottom` | `number \| "auto" \| "{number}%"` | - | Position offsets |
## Example: status bar
```typescript
import { Text, Box, t, bold, fg } from "@opentui/core"
const statusBar = Box(
{
position: "absolute",
bottom: 0,
width: "100%",
height: 1,
backgroundColor: "#333333",
flexDirection: "row",
justifyContent: "space-between",
paddingLeft: 1,
paddingRight: 1,
},
Text({
content: t`${bold("myfile.ts")} - ${fg("#888888")("TypeScript")}`,
}),
Text({
content: t`Ln ${fg("#00FF00")("42")}, Col ${fg("#00FF00")("15")}`,
}),
)
renderer.root.add(statusBar)
```
---
title: Textarea
description: Multi-line text input with selection and keybindings
order: 4
---
# Textarea
Multi-line text input with cursor, selection, and rich keybindings.
## Basic usage
### Renderable API
```typescript
import { TextareaRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const textarea = new TextareaRenderable(renderer, {
id: "notes",
width: 50,
height: 6,
placeholder: "Type notes here...",
backgroundColor: "#1a1a1a",
focusedBackgroundColor: "#222222",
textColor: "#FFFFFF",
cursorColor: "#00FF88",
})
renderer.root.add(textarea)
textarea.focus()
```
## Submit handling
Bind a submit action and listen for `onSubmit`:
```typescript
import { TextareaRenderable } from "@opentui/core"
const textarea = new TextareaRenderable(renderer, {
width: 50,
height: 6,
onSubmit: () => {
console.log("Submitted:", textarea.plainText)
},
keyBindings: [{ name: "return", ctrl: true, action: "submit" }],
})
```
## Placeholder styling
```typescript
const textarea = new TextareaRenderable(renderer, {
width: 40,
height: 4,
placeholder: "Type here",
placeholderColor: "#666666",
})
```
## Construct API
> Not available yet. Use `TextareaRenderable` for now.
## Properties
| Property | Type | Default | Description |
| ------------------------ | ------------------------------------- | ----------- | --------------------------------- |
| `width` | `number` or `string` | - | Width in characters or percentage |
| `height` | `number` or `string` | - | Height in rows or percentage |
| `initialValue` | `string` | `""` | Initial text content |
| `placeholder` | `string`, `StyledText`, or `null` | `null` | Placeholder content |
| `placeholderColor` | `string` or `RGBA` | `#666666` | Placeholder color |
| `backgroundColor` | `string` or `RGBA` | transparent | Background when unfocused |
| `textColor` | `string` or `RGBA` | `#FFFFFF` | Text color when unfocused |
| `focusedBackgroundColor` | `string` or `RGBA` | transparent | Background when focused |
| `focusedTextColor` | `string` or `RGBA` | `#FFFFFF` | Text color when focused |
| `wrapMode` | `"none"`, `"char"`, or `"word"` | `"word"` | Line wrapping mode |
| `selectionBg` | `string` or `RGBA` | - | Selection background |
| `selectionFg` | `string` or `RGBA` | - | Selection foreground |
| `cursorColor` | `string` or `RGBA` | `#FFFFFF` | Cursor color |
| `cursorStyle` | `CursorStyleOptions` | - | Cursor style and blinking |
| `keyBindings` | `KeyBinding[]` | - | Custom keybindings |
| `keyAliasMap` | `Record<string, string>` | - | Key alias mapping |
| `onSubmit` | `(event: SubmitEvent) => void` | - | Submit handler |
| `onContentChange` | `(event: ContentChangeEvent) => void` | - | Fired on content changes |
| `onCursorChange` | `(event: CursorChangeEvent) => void` | - | Fired on cursor movement |
## Useful properties
| Property | Type | Description |
| ----------------------- | -------------------------- | --------------------------------------------------------------- |
| `plainText` | `string` | Current text content |
| `cursorOffset` | `number` | Cursor offset in the buffer |
| `cursorCharacterOffset` | `number \| undefined` | Offset of the character under the cursor (skips trailing blank) |
| `logicalCursor` | `{ row, col }` | Logical line/column of the cursor |
| `visualCursor` | `{ row, col, visualLine }` | Visual (post-wrap) cursor position |
| `traits` | `EditorTraits` | Editor traits published to hosting UI (see [Traits](#traits)) |
## Cursor and selection control
`TextareaRenderable` (and its base `EditBufferRenderable`) exposes a programmatic API. You can move the cursor, edit text, and drive selections from your own keybindings or commands. All selection-aware movement methods accept `{ select: true }` to extend the current selection instead of moving the cursor.
### Cursor movement
```typescript
textarea.setCursor(row, col)
textarea.moveCursorLeft()
textarea.moveCursorRight({ select: true })
textarea.moveCursorUp()
textarea.moveCursorDown()
textarea.moveWordForward({ select: true })
textarea.moveWordBackward()
textarea.gotoLine(0)
textarea.gotoLineStart()
textarea.gotoLineTextEnd()
textarea.gotoLineHome({ select: true }) // Emacs-style smart home
textarea.gotoLineEnd()
textarea.gotoVisualLineHome()
textarea.gotoVisualLineEnd()
textarea.gotoBufferHome()
textarea.gotoBufferEnd({ select: true })
```
### Selection
```typescript
textarea.setSelection(start, end) // half-open offsets
textarea.setSelectionInclusive(start, end) // inclusive end
textarea.selectAll()
textarea.clearSelection()
textarea.deleteSelection()
```
### Editing
```typescript
textarea.insertChar("a")
textarea.insertText("\ninserted")
textarea.deleteChar() // forward delete
textarea.deleteCharBackward() // backspace
textarea.deleteWordForward()
textarea.deleteWordBackward()
textarea.deleteToLineEnd()
textarea.deleteToLineStart()
textarea.deleteLine()
textarea.newLine()
textarea.undo()
textarea.redo()
```
These methods update the cursor, clear any active global selection, and call `requestRender()` for you.
## Traits
The `traits` property lets an editor advertise intent to a host UI — which built-in keys it wants to capture, whether it wants the chrome to visually suspend, and a short status label for a footer or status bar. Assigning a new `EditorTraits` object emits the `traits-changed` event when the value differs from the previous one.
```typescript
import { EditBufferRenderableEvents, type EditorTraits } from "@opentui/core"
textarea.traits = {
capture: ["escape", "submit"], // consume these before host binds
suspend: false,
status: "Composing reply",
} satisfies EditorTraits
textarea.on(EditBufferRenderableEvents.TRAITS_CHANGED, (traits) => {
updateFooter(traits.status ?? "")
})
```
| Field | Type | Description |
| --------- | ----------------- | ------------------------------------------------------------------------------- |
| `capture` | `EditorCapture[]` | Keys the editor wants to capture: `"escape"`, `"navigate"`, `"submit"`, `"tab"` |
| `suspend` | `boolean` | Hint to the host to suspend ambient UI (dim borders, hide hints, etc.) |
| `status` | `string` | Optional short label surfacing editor mode in a status bar |
Traits reset to an empty object when you destroy the renderable. Use `isEditBufferRenderable(renderable)` if you need to distinguish editor renderables from plain text renderables in a generic tree.
---
title: Native audio
description: Load, play, mix, and inspect audio with the native miniaudio backend
order: 11
skill:
entry: true
intents: [audio, native-audio, sound, playback, pcm, fft]
---
# Native audio
OpenTUI exposes a native miniaudio-backed `Audio` engine from `@opentui/core`. It can decode audio bytes or files, play sounds through named groups, choose playback devices, mix into PCM buffers, and expose recent mixed frames for visualization.
## Basic usage
```typescript
import { Audio } from "@opentui/core"
const audio = Audio.create({ autoStart: false })
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 })
}
audio.dispose()
```
Use `setupAudio(options?)` when you prefer a function wrapper around `Audio.create(options?)`.
## Engine options
| Option | Default | Description |
| ------------------ | -------- | ---------------------------------------------- |
| `autoStart` | `false` | Call `start()` during creation |
| `sampleRate` | `48000` | Engine sample rate |
| `playbackChannels` | `2` | Requested playback channel count |
| `startOptions` | defaults | Options passed to `start()` when auto-starting |
Create a new `Audio` instance to change `sampleRate` or `playbackChannels`.
## Sounds and voices
| Method | Description |
| ------------------------------- | ------------------------------------------------------------- |
| `loadSound(data)` | Decode `Uint8Array` or `ArrayBuffer`; returns `AudioSound` |
| `loadSoundFile(path)` | Read and decode a file; returns `Promise<AudioSound \| null>` |
| `unloadSound(sound)` | Free a loaded sound and stop voices using it |
| `play(sound, options?)` | Start a voice; returns `AudioVoice \| null` |
| `stopVoice(voice)` | Stop one active voice |
| `group(name)` | Create or reuse a named group; group `0` is the default group |
| `setVoiceGroup(voice, group)` | Move a voice to another group |
| `setGroupVolume(group, volume)` | Set group volume |
| `setMasterVolume(volume)` | Set master volume |
`play()` accepts `{ volume, pan, loop, groupId }`. Native playback clamps `volume` to `0..4` and `pan` to `-1..1`. The engine has 32 voice slots.
## Starting audio
`start()` starts native playback and returns `false` when no output device is available. Use `isStarted()` to check native playback state.
For headless mixing, tests, benchmarks, or manual `mixFrames()` output, use `startMixer()`. It starts the mixer without opening a playback device. Use `isMixerStarted()` to check either mode.
## Bun standalone executables
Bun embeds files in `bun build --compile` executables when they are imported with `with { type: "file" }`. Read the embedded file as bytes and pass it to `loadSound()` so audio decodes from memory instead of a file path.
```typescript
import { file } from "bun"
import { Audio } from "@opentui/core"
import clickPath from "./click.wav" with { type: "file" }
const audio = Audio.create({ autoStart: false })
const clickBytes = await file(clickPath).bytes()
const click = audio.loadSound(clickBytes)
if (click != null && audio.start()) {
audio.play(click)
}
```
Compile the app normally with `bun build --compile ./app.ts --outfile app`.
## Node.js standalone executables
Coming soon.
## Devices
Select devices before starting the engine:
```typescript
const audio = Audio.create({ autoStart: false })
const devices = audio.listPlaybackDevices() ?? []
const device = devices.find((item) => item.isDefault) ?? devices[0]
if (device) {
audio.selectPlaybackDevice(device.index)
}
if (!audio.start()) {
console.error("No playback device available")
}
```
`listPlaybackDevices()` returns `{ index, name, isDefault }[] | null`. Use `clearPlaybackDeviceSelection()` to return to the default device selection.
## Mixing and analysis
| Method | Description |
| --------------------------------------- | ------------------------------------------------------------------------------------------ |
| `mixFrames(frameCount, channels=2)` | Mix into a `Float32Array`; mono downmixes, channels above stereo keep extras zero |
| `getStats()` | Returns `soundsLoaded`, `voicesActive`, `framesMixed`, `lockMisses`, `lastPeak`, `lastRms` |
| `enableTap(capacityFrames=8192)` | Enable a fixed-size native ring buffer of recent mixed frames |
| `readTapFrames(frameCount, channels=2)` | Copy the latest tapped frames; returns `{ frames, framesRead }` |
| `disableTap()` | Free the tap buffer |
The tap is disabled by default. When enabled, it stores the latest frames and overwrites old data; reads do not drain it. OpenTUI does not compute FFT natively. For spectrum visualization, read tap frames and run FFT in TypeScript.
## Start options
`start(options?)` accepts low-level device options: `periodSizeInFrames`, `periodSizeInMilliseconds`, `periods`, `performanceProfile`, `shareMode`, `noPreSilencedOutputBuffer`, `noClip`, `noDisableDenormals`, `noFixedSizedCallback`, `wasapiNoAutoConvertSrc`, `wasapiNoDefaultQualitySrc`, `alsaNoMMap`, `alsaNoAutoFormat`, `alsaNoAutoChannels`, and `alsaNoAutoResample`.
`performanceProfile` uses `0` for low latency and `1` for conservative. `shareMode` uses `0` for shared and `1` for exclusive.
## Lifecycle and errors
Use `start()`, `startMixer()`, `stop()`, `isStarted()`, `isMixerStarted()`, and `dispose()` for lifecycle. `Audio` emits `started`, `mixerStarted`, `stopped`, `disposed`, and `error`. Methods that fail return `false` or `null` and emit `error` with `{ action, status }` context.
---
title: Console overlay
description: Built-in debugging console for TUI applications
order: 7
---
# Console overlay
OpenTUI includes a built-in console overlay that captures all `console.log`, `console.info`, `console.warn`, `console.error`, and `console.debug` calls. You can position the console at any edge of the terminal. It supports scrolling and focus management.
## Basic usage
```typescript
import { createCliRenderer, ConsolePosition } from "@opentui/core"
const renderer = await createCliRenderer({
consoleOptions: {
position: ConsolePosition.BOTTOM,
sizePercent: 30,
},
})
// These appear in the overlay instead of stdout
console.log("This appears in the overlay")
console.error("Errors are color-coded red")
console.warn("Warnings appear in yellow")
```
## Console vs stdout
`consoleMode` controls only the overlay surface. To disable global `console.*` capture, set `OTUI_USE_CONSOLE=false`.
`externalOutputMode` controls writes through the renderer's configured `stdout.write`. `stderr` is separate.
## Configuration options
```typescript
const renderer = await createCliRenderer({
consoleOptions: {
position: ConsolePosition.BOTTOM, // Position on screen
sizePercent: 30, // Size as percentage of terminal
colorInfo: "#00FFFF", // Color for console.info
colorWarn: "#FFFF00", // Color for console.warn
colorError: "#FF0000", // Color for console.error
startInDebugMode: false, // Show file/line info in logs
},
})
```
## Console positions
```typescript
import { ConsolePosition } from "@opentui/core"
ConsolePosition.TOP // Dock at top
ConsolePosition.BOTTOM // Dock at bottom
ConsolePosition.LEFT // Dock at left
ConsolePosition.RIGHT // Dock at right
```
## Toggling the console
Toggle the console overlay in code:
```typescript
// Toggle visibility and focus
renderer.console.toggle()
// When open but not focused, toggle() focuses the console
// When focused, toggle() closes the console
```
## Console shortcuts
When the console is focused:
| Key | Action |
| ---------- | -------------------------- |
| Arrow keys | Scroll through log history |
| `+` | Increase console size |
| `-` | Decrease console size |
## Keybinding for toggle
Add a keyboard shortcut to toggle the console:
```typescript
renderer.keyInput.on("keypress", (key) => {
// Toggle with backtick key
if (key.name === "`") {
renderer.console.toggle()
}
// Or with a modifier
if (key.ctrl && key.name === "l") {
renderer.console.toggle()
}
})
```
## Why use the console?
Terminal UI applications capture stdout for rendering. Regular `console.log` calls would interfere with your interface. The console overlay solves this problem:
- Captures all console output without disrupting the UI
- Displays logs in a dedicated overlay area
- Color-codes different log levels for easy identification
- Lets you scroll through history for debugging
## Environment variables
Control console behavior with environment variables:
```bash
# Disable console capture entirely
OTUI_USE_CONSOLE=false bun app.ts
# Start with console visible
SHOW_CONSOLE=true bun app.ts
# Dump captured output on exit
OTUI_DUMP_CAPTURES=true bun app.ts
```
---
title: Notifications
description: Trigger terminal-mediated desktop notifications
order: 8
skill:
intents: [notifications, terminal, osc, renderer]
---
# Notifications
OpenTUI can ask the terminal emulator to show a desktop notification using OSC escape sequences. Use this for long-running tasks, background work, or prompts that need attention.
```typescript
const ok = renderer.triggerNotification("Build finished", "OpenTUI")
```
`triggerNotification(message, title?)` returns `true` when OpenTUI has detected a supported notification protocol, and `false` otherwise.
## Capability
Check detection state through renderer capabilities:
```typescript
if (renderer.capabilities?.notifications) {
renderer.triggerNotification("Tests passed", "CI")
}
```
OpenTUI detects and emits terminal OSC notification protocols only. It does not call platform tools such as `notify-send`, AppleScript, or PowerShell toasts.
## Terminal behavior
Terminal and OS settings decide how the notification is presented. Some terminals only show banners when the terminal is unfocused, and macOS may store notifications in Notification Center without showing a banner depending on app notification settings.
## Multiplexers
tmux does not forward raw notification OSC sequences. When OpenTUI detects `renderer.capabilities?.multiplexer === "tmux"`, notification output is wrapped in tmux DCS passthrough. tmux must allow passthrough for the outer terminal to receive the notification:
```text
set -g allow-passthrough on
```
Use `allow-passthrough all` if notifications need to work from panes that are not visible.
Zellij notification forwarding is OSC 99 based. OpenTUI ignores inherited host-terminal notification hints inside Zellij and enables notifications there only after an OSC 99 notification query succeeds, or when explicitly overridden. Zellij needs OSC 99 notification forwarding support for this to work.
## Overrides
Use environment overrides for diagnostics or deployments where terminal detection cannot query the final terminal path:
```sh
OPENTUI_NOTIFICATION_PROTOCOL=osc99
OPENTUI_NOTIFICATIONS=0
```
`OPENTUI_NOTIFICATION_PROTOCOL` accepts `osc9`, `osc777`, `osc99`, or `none`. `OPENTUI_NOTIFICATIONS=0`, `false`, or `off` disables notifications.
See `packages/examples/src/notification-demo.ts` for an interactive example.
Related skills
How it compares
Pick OpenTUI for React-style terminal apps; pick generic CLI skills when you only need bash scripting without interactive component trees.
FAQ
Who is opentui for?
Developers and software engineers working with opentui patterns described in the skill documentation.
When should I use opentui?
When Build terminal UIs with OpenTUI. Covers the core API, native audio, keymaps, React and Solid bindings, components, layout, keyboard input, plugins, and testing.
Is opentui safe to install?
Review the Security Audits panel on this page before installing in production.