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.

referencessolidgotchas.md

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

Solid Gotchas

Critical

Never use process.exit() directly

This is the most common mistake. Using process.exit() leaves the terminal in a broken state (cursor hidden, raw mode, alternate screen).

// WRONG - Terminal left in broken state
process.exit(0)

// CORRECT - Use renderer.destroy()
import { useRenderer } from "@opentui/solid"

function App() {
  const renderer = useRenderer()
  
  const handleExit = () => {
    renderer.destroy()  // Cleans up and exits properly
  }
}

renderer.destroy() restores the terminal (exits alternate screen, restores cursor, etc.) before exiting.

Configuration Issues

Missing bunfig.toml

Symptom: JSX syntax errors, components not rendering

SyntaxError: Unexpected token '<'

Fix: Create bunfig.toml in project root:

preload = ["@opentui/solid/preload"]

Wrong JSX Settings

Symptom: JSX compiles to React, errors about React not found

Fix: Ensure tsconfig.json has:

{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "@opentui/solid"
  }
}

Build Without Plugin

Symptom: Built bundle has raw JSX

Fix: Add Solid plugin to build:

import solidPlugin from "@opentui/solid/bun-plugin"

await Bun.build({
  // ...
  plugins: [solidPlugin],
})

Reactivity Issues

Accessing Signals Without Calling

Symptom: Value never updates, shows [Function]

// WRONG - Missing ()
const [count, setCount] = createSignal(0)
<text>Count: {count}</text>  // Shows [Function]

// CORRECT
<text>Count: {count()}</text>

Breaking Reactivity with Destructuring

Symptom: Props stop being reactive

// WRONG - Breaks reactivity
function Component(props: { value: number }) {
  const { value } = props  // Destructured once, never updates!
  return <text>{value}</text>
}

// CORRECT - Keep props reactive
function Component(props: { value: number }) {
  return <text>{props.value}</text>
}

// OR use splitProps
function Component(props: { value: number; other: string }) {
  const [local, rest] = splitProps(props, ["value"])
  return <text>{local.value}</text>
}

Effects Not Running

Symptom: createEffect doesn't trigger

// WRONG - Signal not accessed in effect
const [count, setCount] = createSignal(0)

createEffect(() => {
  console.log("Count changed")  // Never runs after initial!
})

// CORRECT - Access the signal
createEffect(() => {
  console.log("Count:", count())  // Runs when count changes
})

HTML Entity Decoding

Solid's reconciler automatically decodes HTML entities in JSX text content. This means &lt;, &gt;, &amp;, etc. render as their literal characters:

// These render correctly in Solid
<text>Use &lt;box&gt; for containers</text>  // Displays: Use <box> for containers
<text>A &amp; B</text>                        // Displays: A & B

This applies to text nodes, the content prop, and the text prop.

Component Naming

Underscore vs Hyphen

Solid uses underscores for multi-word component names:

// WRONG - React-style naming
<tab-select />    // Error!
<ascii-font />    // Error!
<line-number />   // Error!

// CORRECT - Solid naming
<tab_select />
<ascii_font />
<line_number />

Component mapping:

Concept React Solid
Tab Select <tab-select> <tab_select>
ASCII Font <ascii-font> <ascii_font>
Line Number <line-number> <line_number>

Focus Issues

Focus Not Working

Components need explicit focus:

// WRONG
<input placeholder="Type here..." />

// CORRECT
<input placeholder="Type here..." focused />

Select Not Responding

// WRONG
<select options={["a", "b"]} />

// CORRECT
<select
  options={[
    { name: "A", description: "Option A", value: "a" },
    { name: "B", description: "Option B", value: "b" },
  ]}
  onSelect={(index, option) => {
    // Called when Enter is pressed
    console.log("Selected:", option.name)
  }}
  focused
/>

Select Events Confusion

Remember: onSelect fires on Enter (selection confirmed), onChange fires on navigation:

// WRONG - expecting onChange to fire on Enter
<select
  options={options()}
  onChange={(i, opt) => submitForm(opt)}  // This fires on arrow keys!
/>

// CORRECT
<select
  options={options()}
  onSelect={(i, opt) => submitForm(opt)}   // Enter pressed - submit
  onChange={(i, opt) => showPreview(opt)}  // Arrow keys - preview
/>

Control Flow Issues

For vs Index

Use For for arrays of objects, Index for primitives:

// For objects - item is reactive
<For each={objects()}>
  {(obj) => <text>{obj.name}</text>}
</For>

// For primitives - use Index, item() is reactive
<Index each={strings()}>
  {(str, index) => <text>{index}: {str()}</text>}
</Index>

Missing Fallback

Show requires fallback for proper rendering:

// May cause issues
<Show when={data()}>
  <Component />
</Show>

// Better - explicit fallback
<Show when={data()} fallback={<text>Loading...</text>}>
  <Component />
</Show>

Cleanup Issues

Forgetting onCleanup

Symptom: Memory leaks, multiple intervals running

// WRONG - Interval never cleared
function Timer() {
  const [time, setTime] = createSignal(0)
  
  setInterval(() => setTime(t => t + 1), 1000)
  
  return <text>{time()}</text>
}

// CORRECT
function Timer() {
  const [time, setTime] = createSignal(0)
  
  const interval = setInterval(() => setTime(t => t + 1), 1000)
  onCleanup(() => clearInterval(interval))
  
  return <text>{time()}</text>
}

Effect Cleanup

createEffect(() => {
  const subscription = subscribe(data())
  
  // WRONG - No cleanup
  // subscription stays active
  
  // CORRECT
  onCleanup(() => subscription.unsubscribe())
})

Store Issues

Mutating Store Directly

Symptom: Changes don't trigger updates

const [state, setState] = createStore({ items: [] })

// WRONG - Direct mutation
state.items.push(newItem)  // Won't trigger updates!

// CORRECT - Use setState
setState("items", items => [...items, newItem])

Nested Updates

const [state, setState] = createStore({
  user: { profile: { name: "John" } }
})

// WRONG
state.user.profile.name = "Jane"

// CORRECT
setState("user", "profile", "name", "Jane")

Debugging

Console Not Visible

OpenTUI captures console output:

import { useRenderer } from "@opentui/solid"
import { onMount } from "solid-js"

function App() {
  const renderer = useRenderer()
  
  onMount(() => {
    renderer.console.show()
    console.log("Now visible!")
  })
  
  return <box>{/* ... */}</box>
}

Tracking Reactivity

Use createEffect to debug:

createEffect(() => {
  console.log("State:", {
    count: count(),
    items: items(),
  })
})

Runtime Issues

Use Bun

# WRONG
node src/index.tsx
npm run start

# CORRECT
bun run src/index.tsx
bun run start

Async render()

The render function is async when creating a renderer:

// This is fine - Bun supports top-level await
render(() => <App />)

// If you need the renderer
import { createCliRenderer } from "@opentui/core"
import { render } from "@opentui/solid"

const renderer = await createCliRenderer()
render(() => <App />, renderer)

Common Error Messages

"Cannot read properties of undefined"

Usually a missing reactive access:

// Check if signal is being called
<text>{count()}</text>  // Note the ()

// Check if props are being accessed correctly
<text>{props.value}</text>  // Not destructured

"JSX element has no corresponding closing tag"

Check component naming:

// Wrong
<tab-select></tab-select>

// Correct
<tab_select></tab_select>

"store is not a function"

Stores aren't called like signals:

const [store, setStore] = createStore({ count: 0 })

// WRONG
<text>{store().count}</text>

// CORRECT
<text>{store.count}</text>

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.