All skills
msmps avatar

/opentui

@0d61d6d
by Matt Simpsonmsmps/opentui-skill225 stars
9

OpenTUI skill for building terminal user interfaces with the Core, React, or Solid APIs. Use for any TUI task including components, layout, keyboard and keymap handling, animations, and testing.

Use this Skill: https://skilld.dev/gh/msmps/opentui-skill/opentui

This session only. Nothing lands on disk.

referencescoregotchas.md

≈2.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Core Gotchas

Runtime Environment

Bun (reference runtime) or Node.js 26.4.0+

Bun is the reference runtime and the smoothest path — prefer it for new projects:

# Recommended
bun install @opentui/core
bun run src/index.ts
bun test

Node.js is now also supported, with caveats:

  • Importing @opentui/core (and @opentui/keymap) works in Node.js without FFI as long as you don't create a native renderer.
  • Creating a native renderer (createCliRenderer()) requires FFI: Node.js 26.4.0 or later launched with --experimental-ffi (and, under Node's permission model, --allow-ffi plus filesystem permissions). OpenTUI does not install Node for you.
  • The Node path is lower-level than Bun; the packages/*/package.json engines fields and root README still list Bun only, but the docs site (getting-started, keymap, solid pages) is the source of truth for Node support.

Bun APIs to Use

Prefer Bun's built-in APIs for your application code:

// CORRECT - Bun APIs
Bun.serve({ ... })                // Instead of express
Bun.$`ls -la`                     // Instead of execa
import { Database } from "bun:sqlite"  // Instead of better-sqlite3

// WRONG - Node.js patterns
import express from "express"

Note: OpenTUI itself uses node:fs internally for file I/O (for broader compatibility), but your application code should still prefer Bun APIs where available.

Avoid process.exit()

Never use process.exit() directly - it prevents proper terminal cleanup and can leave the terminal in a broken state (alternate screen mode, raw input mode, etc.).

// WRONG - Terminal may be left in broken state
if (error) {
  console.error("Fatal error")
  process.exit(1)
}

// CORRECT - Use renderer.destroy() for cleanup
if (error) {
  console.error("Fatal error")
  await renderer.destroy()
  process.exit(1)  // Only after destroy
}

// BETTER - Let destroy handle exit
const renderer = await createCliRenderer({
  exitOnCtrlC: true,  // Handles Ctrl+C properly
})

// For programmatic exit
renderer.destroy()  // Cleans up and exits

renderer.destroy() restores the terminal to its original state before exiting.

Environment Variables

Bun auto-loads .env files. Don't use dotenv:

// CORRECT
const apiKey = process.env.API_KEY

// WRONG
import dotenv from "dotenv"
dotenv.config()

Debugging TUIs

Cannot See console.log Output

OpenTUI captures console output for the debug overlay. You can't see logs in the terminal while the TUI is running.

Solutions:

  1. Use the console overlay:

    const renderer = await createCliRenderer()
    renderer.console.show()
    console.log("This appears in the overlay")
  2. Toggle with keyboard:

    renderer.keyInput.on("keypress", (key) => {
      if (key.name === "f12") {
        renderer.console.toggle()
      }
    })
  3. Write to a file:

    import { appendFileSync } from "node:fs"
    function debugLog(msg: string) {
      appendFileSync("debug.log", `${new Date().toISOString()} ${msg}\n`)
    }
  4. Disable console capture:

    OTUI_USE_CONSOLE=false bun run src/index.ts

Reproduce Issues in Tests

Don't guess at bugs. Create a reproducible test:

import { test, expect } from "bun:test"
import { createTestRenderer } from "@opentui/core/testing"

test("reproduces the issue", async () => {
  const { renderer, snapshot } = await createTestRenderer({
    width: 40,
    height: 10,
  })
  
  // Setup that reproduces the bug
  const box = new BoxRenderable(renderer, { ... })
  renderer.root.add(box)
  
  // Verify with snapshot
  expect(snapshot()).toMatchSnapshot()
})

Focus Management

Components Must Be Focused

Input components only receive keyboard input when focused:

const input = new InputRenderable(renderer, {
  id: "input",
  placeholder: "Type here...",
})

renderer.root.add(input)

// WRONG - input won't receive keystrokes
// (no focus call)

// CORRECT
input.focus()

Focus in Nested Components

When a component is inside a container, focus the component directly:

const container = new BoxRenderable(renderer, { id: "container" })
const input = new InputRenderable(renderer, { id: "input" })
container.add(input)
renderer.root.add(container)

// WRONG
container.focus()

// CORRECT
input.focus()

// Or use getRenderable
container.getRenderable("input")?.focus()

// Or use delegate (constructs)
const form = delegate(
  { focus: "input" },
  Box({}, Input({ id: "input" })),
)
form.focus()  // Routes to the input

Build Requirements

Zig is Required

Native code compilation requires Zig:

# Install Zig first
# macOS
brew install zig

# Linux
# Download from https://ziglang.org/download/

# Then build
bun run build

When to Build

  • TypeScript changes: NO build needed (Bun runs TS directly)
  • Native code changes: Build required
# Only needed when changing native (Zig) code
cd packages/core
bun run build

Common Errors

"Cannot read properties of undefined"

Usually means a renderable wasn't added to the tree:

// WRONG - not added to tree
const text = new TextRenderable(renderer, { content: "Hello" })
// text.someMethod() // May fail

// CORRECT
const text = new TextRenderable(renderer, { content: "Hello" })
renderer.root.add(text)
text.someMethod()

Layout Not Updating

Yoga layout is calculated lazily. Force a recalculation:

// After changing layout properties
box.width = newWidth
renderer.requestRender()

Text Overflow/Clipping

Text doesn't wrap by default. Set explicit width:

// May overflow
const text = new TextRenderable(renderer, {
  content: "Very long text that might overflow the terminal...",
})

// Contained within width
const text = new TextRenderable(renderer, {
  content: "Very long text that might overflow the terminal...",
  width: 40,  // Will clip or wrap based on parent
})

Colors Not Showing

Check terminal capability and color format:

// CORRECT formats
fg: "#FF0000"           // Hex
fg: "red"               // CSS color name
fg: RGBA.fromHex("#FF0000")

// WRONG
fg: "FF0000"            // Missing #
fg: 0xFF0000            // Number (not supported)

Performance

Avoid Frequent Re-renders

Batch updates when possible:

// WRONG - multiple render calls
item1.setContent("...")
item2.setContent("...")
item3.setContent("...")

// BETTER - single render after all updates
// (OpenTUI batches automatically, but be mindful)
items.forEach((item, i) => {
  item.setContent(data[i])
})

Minimize Tree Depth

Deep nesting impacts layout calculation:

// Avoid unnecessary wrappers
// WRONG
Box({}, Box({}, Box({}, Text({ content: "Hello" }))))

// CORRECT
Box({}, Text({ content: "Hello" }))

Toggle visibility

Hide elements instead of removing/re-adding:

// For toggling visibility
element.visible = false  // Hidden and removed from layout
element.visible = true   // Visible

// Instead of
parent.remove(element)
parent.add(element)

Testing

Test Runner

Use Bun's test runner:

import { test, expect, beforeEach, afterEach } from "bun:test"

test("my test", () => {
  expect(1 + 1).toBe(2)
})

Test from Package Directories

Run tests from the specific package directory:

# CORRECT
cd packages/core
bun test

# For native tests
cd packages/core
bun run test:native

Filter Tests

# Bun test-name filter
bun test --test-name-pattern "component name"

# Native test filter
bun run test:native -Dtest-filter="test name"

Keyboard Handling

Key Names

Common key names for KeyEvent.name:

// Letters/numbers
"a", "b", ..., "z"
"1", "2", ..., "0"

// Special keys
"escape", "enter", "return", "tab", "backspace", "delete"
"up", "down", "left", "right"
"home", "end", "pageup", "pagedown"
"f1", "f2", ..., "f12"
"space"

// Modifiers (check boolean properties)
key.ctrl   // Ctrl held
key.shift  // Shift held
key.meta   // Alt held
key.option // Option held (macOS)

Key Event Types

renderer.keyInput.on("keypress", (key) => {
  // eventType: "press" | "release" | "repeat"
  if (key.eventType === "repeat") {
    // Key being held down
  }
})

Source: SKILL.md on GitHub

No alerts17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    This skill contains documentation and usage patterns for the OpenTUI framework, designed for building terminal user interfaces across Core, React, and SolidJS. The analysis confirmed that the skill provides legitimate development guidance; no malicious code, persistence mechanisms, or obfuscation were detected. Findings are limited to standard platform capabilities and recommended scaffolding tools.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    1/26 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 0d61d6d. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last month.

Steadyupdated last month
Other metadata
metadata
{
  "references": "core, react, solid, components, layout, keyboard, keymap, animation, testing"
}
  • React
  • Testing
  • terminal-ui
  • tui
  • solid
  • components
  • keyboard-input
  • animations
  • layout

README badge

README badge for msmps/opentui-skill

Builds terminal user interfaces with OpenTUI using an imperative core API or React/Solid reconcilers. Covers layout, keyboard handling, animations, components, and testing for full-featured TUI applications.

Generated from the current SKILL.md.

Does this skill cover all three OpenTUI frameworks?
Yes. The skill consolidates the core imperative API, React reconciler, and Solid reconciler. Use the decision tree in the skill to pick the right framework for your task.
How do I start a new OpenTUI project?
Use `create-tui` with options before arguments: `bunx create-tui -t react my-app` (not `bunx create-tui my-app -t react`). Then read the REFERENCE.md for your chosen framework.
What runtime does OpenTUI require?
OpenTUI runs on Bun and uses Zig for native builds. See `./references/core/gotchas.md` for runtime requirements and build guidance.
Can I use `process.exit()` to shut down my TUI?
No. Always use `renderer.destroy()` instead. Calling `process.exit()` directly is a common pitfall covered in `core/gotchas.md`.
How do I apply text styling in React or Solid?
Use nested modifier elements, not props. See `components/text-display.md` for the correct pattern.

Generated from the current SKILL.md. These answers refresh after source changes.