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.

referencescomponentsinputs.md

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

Input Components

Components for user input in OpenTUI.

Input Component

Single-line text input field.

Basic Usage

// React
<input
  value={value}
  onChange={(newValue) => setValue(newValue)}
  placeholder="Enter text..."
  focused
/>

// Solid
<input
  value={value()}
  onInput={(newValue) => setValue(newValue)}
  placeholder="Enter text..."
  focused
/>

// Core
const input = new InputRenderable(renderer, {
  id: "name",
  placeholder: "Enter text...",
})
input.on(InputRenderableEvents.CHANGE, (value) => {
  console.log("Value:", value)
})
input.focus()

Styling

<input
  width={30}
  maxLength={100}                // Maximum characters
  minLength={3}                  // Minimum length for submit() to succeed
  backgroundColor="#1a1a1a"
  textColor="#FFFFFF"
  cursorColor="#00FF00"
  focusedBackgroundColor="#2a2a2a"
  placeholderColor="#666666"
/>

minLength (default 0) does not block typing — it only makes submit() (Enter) fail silently while the value is shorter than minLength. Setting minLength > maxLength throws.

Events

// React
<input
  onChange={(value) => console.log("Changed:", value)}
  onFocus={() => console.log("Focused")}
  onBlur={() => console.log("Blurred")}
/>

// Core
input.on(InputRenderableEvents.CHANGE, (value) => {})
input.on(InputRenderableEvents.FOCUS, () => {})
input.on(InputRenderableEvents.BLUR, () => {})

Controlled Input

// React
function ControlledInput() {
  const [value, setValue] = useState("")
  
  return (
    <input
      value={value}
      onChange={setValue}
      focused
    />
  )
}

// Solid
function ControlledInput() {
  const [value, setValue] = createSignal("")
  
  return (
    <input
      value={value()}
      onInput={setValue}
      focused
    />
  )
}

Textarea Component

Multi-line text input field.

Basic Usage

// React / Solid (Textarea is imperative, not a controlled input)
<textarea
  initialValue="Draft text"
  placeholder="Enter multiple lines..."
  width={40}
  height={10}
  focused
/>

// Core
const textarea = new TextareaRenderable(renderer, {
  id: "editor",
  width: 40,
  height: 10,
  placeholder: "Enter text...",
})

Features

<textarea
  initialValue="Draft"
  wrapMode="word"       // "none" | "char" | "word"
  selectionOccupancy="boundary" // Half-open insert-style selection
  cursorStyle={{ style: "line" }}
/>

Textarea does not expose controlled value/onChange, language, showLineNumbers, readOnly, wrapText, or tabSize props. Keep a renderable ref, use plainText/setText(), and listen with onContentChange. For syntax highlighting, create and pass a SyntaxStyle; compose a LineNumberRenderable when line numbers are needed.

Cursor, Selection, and Tab Width

textarea.gotoVisualLineEnd({ select: true })
textarea.setSelection(start, end)          // Half-open [start, end)
textarea.setSelectionInclusive(start, end) // Includes end grapheme in cell mode
textarea.clearSelection()

textarea.editBuffer.setTabWidth(4)
console.log(textarea.editBuffer.getTabWidth())

selectionOccupancy is "cell" (default, both endpoint cells) or "boundary" (half-open insertion range). Cursor style only changes paint; use boundary occupancy with a line/bar cursor when insert-style selection is desired.

Select Component

List selection for choosing from options.

Basic Usage

// React
<select
  options={[
    { name: "Option 1", description: "First option", value: "1" },
    { name: "Option 2", description: "Second option", value: "2" },
    { name: "Option 3", description: "Third option", value: "3" },
  ]}
  onSelect={(index, option) => {
    console.log("Selected:", option.name)  // Called when Enter is pressed
  }}
  focused
/>

// Solid
<select
  options={[
    { name: "Option 1", description: "First option", value: "1" },
    { name: "Option 2", description: "Second option", value: "2" },
  ]}
  onSelect={(index, option) => {
    console.log("Selected:", option.name)  // Called when Enter is pressed
  }}
  focused
/>

// Core
const select = new SelectRenderable(renderer, {
  id: "menu",
  options: [
    { name: "Option 1", description: "First option", value: "1" },
    { name: "Option 2", description: "Second option", value: "2" },
  ],
})
select.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {
  console.log("Selected:", option.name)  // Called when Enter is pressed
})
select.focus()

Option Format

interface SelectOption {
  name: string          // Display text
  description?: string  // Optional description shown below
  value?: any          // Associated value
}

Styling

<select
  height={8}                    // Visible height
  selectedIndex={0}             // Initially selected
  showScrollIndicator           // Show scroll arrows
  showSelectionIndicator={true} // Show "▶ " marker + gutter (default true)
  selectedBackgroundColor="#333"
  selectedTextColor="#fff"
/>

showSelectionIndicator (default true): when false, the ▶ marker is hidden AND its 2-column gutter is reclaimed, so option text shifts left by 2. In Core, toggle at runtime with select.showSelectionIndicator = false.

Navigation

Default keybindings:

  • Up / k - Move up
  • Down / j - Move down
  • Shift+Up / Shift+Down - Move by fastScrollStep (default 5)
  • Enter - Select item

Events

Important: onSelect and onChange serve different purposes:

Event Trigger Use Case
onSelect Enter key pressed - user confirms selection Perform action with selected item
onChange Arrow keys - user navigates list Preview, update UI as user browses
// React/Solid
<select
  onSelect={(index, option) => {
    // Called when Enter is pressed - selection confirmed
    console.log("User selected:", option.name)
    performAction(option)
  }}
  onChange={(index, option) => {
    // Called when navigating with arrow keys
    console.log("Browsing:", option.name)
    showPreview(option)
  }}
/>

// Core
select.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {
  // Called when Enter is pressed
})
select.on(SelectRenderableEvents.SELECTION_CHANGED, (index, option) => {
  // Called when navigating with arrow keys
})

Tab Select Component

Horizontal tab-based selection.

Basic Usage

// React
<tab-select
  options={[
    { name: "Home", description: "Dashboard view" },
    { name: "Settings", description: "Configuration" },
    { name: "Help", description: "Documentation" },
  ]}
  onSelect={(index, option) => {
    console.log("Tab selected:", option.name)  // Called when Enter is pressed
  }}
  focused
/>

// Solid (note underscore)
<tab_select
  options={[
    { name: "Home", description: "Dashboard view" },
    { name: "Settings", description: "Configuration" },
  ]}
  onSelect={(index, option) => {
    console.log("Tab selected:", option.name)  // Called when Enter is pressed
  }}
  focused
/>

// Core
const tabs = new TabSelectRenderable(renderer, {
  id: "tabs",
  options: [...],
  tabWidth: 20,
})
tabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index, option) => {
  console.log("Tab selected:", option.name)  // Called when Enter is pressed
})
tabs.focus()

Events

Same pattern as Select - onSelect for Enter key, onChange for navigation:

<tab-select
  onSelect={(index, option) => {
    // Called when Enter is pressed - switch to tab
    setActiveTab(index)
  }}
  onChange={(index, option) => {
    // Called when navigating with arrow keys
    showTabPreview(option)
  }}
/>

Styling

// React
<tab-select
  tabWidth={20}                // Width of each tab
  selectedIndex={0}            // Initially selected tab
/>

// Solid
<tab_select
  tabWidth={20}
  selectedIndex={0}
/>

Navigation

Default keybindings:

  • Left / [ - Previous tab
  • Right / ] - Next tab
  • Enter - Select tab

Slider Component

A draggable value slider (SliderRenderable, exported from @opentui/core).

// Core
import { SliderRenderable, createCliRenderer } from "@opentui/core"

const slider = new SliderRenderable(renderer, {
  id: "volume",
  orientation: "horizontal",   // "horizontal" | "vertical"
  width: 30,
  height: 1,
  min: 0,
  max: 100,
  value: 25,
  onChange: (value) => console.log("Value:", value),
})
renderer.root.add(slider)
Prop Type Default Description
orientation "vertical" | "horizontal" – Required direction
value number min Current value
min number 0 Minimum
max number 100 Maximum
viewPortSize number range × 0.1 Thumb size relative to content
backgroundColor string | RGBA – Track color
foregroundColor string | RGBA – Thumb color
onChange (value: number) => void – Fired on change

Vertical example: { orientation: "vertical", width: 2, height: 10, min: 0, max: 1, value: 0.5 }.

Focus Management

Single Focused Input

function SingleInput() {
  return <input placeholder="I'm focused" focused />
}

Multiple Inputs with Focus State

// React
function Form() {
  const [focusIndex, setFocusIndex] = useState(0)
  const fields = ["name", "email", "message"]
  
  useKeyboard((key) => {
    if (key.name === "tab") {
      setFocusIndex(i => (i + 1) % fields.length)
    }
  })
  
  return (
    <box flexDirection="column" gap={1}>
      {fields.map((field, i) => (
        <input
          key={field}
          placeholder={`Enter ${field}`}
          focused={i === focusIndex}
        />
      ))}
    </box>
  )
}

Focus Methods (Core)

input.focus()      // Give focus
input.blur()       // Remove focus
input.focused      // Check focus state

Form Patterns

Login Form

function LoginForm() {
  const [username, setUsername] = useState("")
  const [password, setPassword] = useState("")
  const [focusField, setFocusField] = useState<"username" | "password">("username")
  
  useKeyboard((key) => {
    if (key.name === "tab") {
      setFocusField(f => f === "username" ? "password" : "username")
    }
    if (key.name === "enter") {
      handleLogin()
    }
  })
  
  return (
    <box flexDirection="column" gap={1} border padding={2}>
      <box flexDirection="row" gap={1}>
        <text>Username:</text>
        <input
          value={username}
          onChange={setUsername}
          focused={focusField === "username"}
          width={20}
        />
      </box>
      <box flexDirection="row" gap={1}>
        <text>Password:</text>
        <input
          value={password}
          onChange={setPassword}
          focused={focusField === "password"}
          width={20}
        />
      </box>
    </box>
  )
}

Search with Results

function SearchableList({ items, onItemSelected }) {
  const [query, setQuery] = useState("")
  const [focusSearch, setFocusSearch] = useState(true)
  const [preview, setPreview] = useState(null)
  
  const filtered = items.filter(item =>
    item.toLowerCase().includes(query.toLowerCase())
  )
  
  useKeyboard((key) => {
    if (key.name === "tab") {
      setFocusSearch(f => !f)
    }
  })
  
  return (
    <box flexDirection="column">
      <input
        value={query}
        onChange={setQuery}
        placeholder="Search..."
        focused={focusSearch}
      />
      <select
        options={filtered.map(item => ({ name: item }))}
        focused={!focusSearch}
        height={10}
        onSelect={(index, option) => {
          // Enter pressed - confirm selection
          onItemSelected(option)
        }}
        onChange={(index, option) => {
          // Navigating - show preview
          setPreview(option)
        }}
      />
    </box>
  )
}

Gotchas

Focus Required

Inputs must be focused to receive keyboard input:

// WRONG - won't receive input
<input placeholder="Type here" />

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

Select Options Format

Options must be objects with name property:

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

// CORRECT
<select options={[
  { name: "A", description: "Option A" },
  { name: "B", description: "Option B" },
]} />

Solid Uses Underscores

// React
<tab-select />

// Solid
<tab_select />

Value vs onInput (Solid)

Solid uses onInput instead of onChange:

// React
<input value={value} onChange={setValue} />

// Solid
<input value={value()} onInput={setValue} />

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.