返回 Skills
msmps/opentui-skill· MIT 内容可用

opentui

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.

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: opentui description: 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. metadata: references: core, react, solid, components, layout, keyboard, keymap, animation, testing

OpenTUI Platform Skill

Consolidated skill for building terminal user interfaces with OpenTUI. Use decision trees below to find the right framework and components, then load detailed references.

Critical Rules

Follow these rules in all OpenTUI code:

  1. Use create-tui for new projects. See framework REFERENCE.md quick starts.
  2. create-tui options must come before arguments. bunx create-tui -t react my-app works, bunx create-tui my-app -t react does NOT.
  3. Never call process.exit() directly. Use renderer.destroy() (see core/gotchas.md).
  4. Text styling requires nested tags in React/Solid. Use modifier elements, not props (see components/text-display.md).

How to Use This Skill

Reference File Structure

Framework references follow a 5-file pattern. Cross-cutting concepts are single-file guides.

Each framework in ./references/<framework>/ contains:

FilePurposeWhen to Read
REFERENCE.mdOverview, when to use, quick startAlways read first
api.mdRuntime API, components, hooksWriting code
configuration.mdSetup, tsconfig, bundlingConfiguring a project
patterns.mdCommon patterns, best practicesImplementation guidance
gotchas.mdPitfalls, limitations, debuggingTroubleshooting

Cross-cutting concepts in ./references/<concept>/ have REFERENCE.md as the entry point.

Reading Order

  1. Start with REFERENCE.md for your chosen framework
  2. Then read additional files relevant to your task:
    • Building components -> api.md + components/<category>.md
    • Setting up project -> configuration.md
    • Layout/positioning -> layout/REFERENCE.md
    • Keyboard/input handling -> keyboard/REFERENCE.md
    • Layered keybindings/commands -> keymap/REFERENCE.md
    • Animations -> animation/REFERENCE.md
    • Troubleshooting -> gotchas.md + testing/REFERENCE.md

Example Paths

./references/react/REFERENCE.md           # Start here for React
./references/react/api.md              # React components and hooks
./references/solid/configuration.md    # Solid project setup
./references/components/inputs.md      # Input, Textarea, Select docs
./references/core/gotchas.md           # Core debugging tips

Runtime Notes

OpenTUI runs on Bun and uses Zig for native builds. Read ./references/core/gotchas.md for runtime requirements and build guidance.

Quick Decision Trees

"Which framework should I use?"

Which framework?
├─ I want full control, maximum performance, no framework overhead
│  └─ core/ (imperative API)
├─ I know React, want familiar component patterns
│  └─ react/ (React reconciler)
├─ I want fine-grained reactivity, optimal re-renders
│  └─ solid/ (Solid reconciler)
└─ I'm building a library/framework on top of OpenTUI
   └─ core/ (imperative API)

"I need to display content"

Display content?
├─ Plain or styled text -> components/text-display.md
├─ Container with borders/background -> components/containers.md
├─ Scrollable content area -> components/containers.md (scrollbox)
├─ Standalone scrollbar -> components/containers.md (scrollbar)
├─ ASCII art banner/title -> components/text-display.md (ascii-font)
├─ QR code -> components/text-display.md (qr-code, @opentui/qrcode)
├─ Data table with borders/wrapping -> components/code-diff.md (TextTable)
├─ Code with syntax highlighting -> components/code-diff.md
├─ Diff viewer (unified/split, hunk nav) -> components/code-diff.md
├─ Line numbers with diagnostics -> components/code-diff.md
└─ Markdown content (streaming) -> components/code-diff.md (markdown)

"I need user input"

User input?
├─ Single-line text field -> components/inputs.md (input)
├─ Multi-line text editor -> components/inputs.md (textarea)
├─ Select from a list (vertical) -> components/inputs.md (select)
├─ Tab-based selection (horizontal) -> components/inputs.md (tab-select)
├─ Value slider -> components/inputs.md (slider)
├─ Declarative/layered keybindings -> keymap/REFERENCE.md (@opentui/keymap)
└─ Custom keyboard shortcuts -> keyboard/REFERENCE.md

"I need layout/positioning"

Layout?
├─ Flexbox-style layouts (row, column, wrap) -> layout/REFERENCE.md
├─ Absolute positioning -> layout/patterns.md
├─ Responsive to terminal size -> layout/patterns.md
├─ Centering content -> layout/patterns.md
└─ Complex nested layouts -> layout/patterns.md

"I need animations"

Animations?
├─ Timeline-based animations -> animation/REFERENCE.md
├─ Easing functions -> animation/REFERENCE.md
├─ Property transitions -> animation/REFERENCE.md
└─ Looping animations -> animation/REFERENCE.md

"I need to handle input"

Input handling?
├─ Keyboard events (keypress, release) -> keyboard/REFERENCE.md
├─ Layered bindings, commands, leader keys -> keymap/REFERENCE.md
├─ Focus management -> keyboard/REFERENCE.md
├─ Paste events -> keyboard/REFERENCE.md
├─ Mouse events -> components/containers.md
├─ Text selection & copy-on-select -> keyboard/REFERENCE.md (selection)
└─ Clipboard (OSC 52) -> keyboard/REFERENCE.md (clipboard)

"I need to test my TUI"

Testing?
├─ Snapshot testing -> testing/REFERENCE.md
├─ Interaction testing -> testing/REFERENCE.md
├─ Test renderer setup -> testing/REFERENCE.md
└─ Debugging tests -> testing/REFERENCE.md

"I need platform capabilities (audio, notifications, SSH)"

Platform capability?
├─ Play sound / native audio -> core/api.md (Audio)
├─ Desktop notifications (OSC 9/777/99) -> core/api.md (triggerNotification)
├─ Custom stdin/stdout (PTY, xterm.js) -> core/api.md (createCliRenderer)
└─ Serve a TUI over SSH -> core/REFERENCE.md (@opentui/ssh)

"I need to debug/troubleshoot"

Troubleshooting?
├─ Runtime errors, crashes -> <framework>/gotchas.md
├─ Layout issues -> layout/REFERENCE.md + layout/patterns.md
├─ Input/focus issues -> keyboard/REFERENCE.md
└─ Repro + regression tests -> testing/REFERENCE.md

Troubleshooting Index

  • Terminal cleanup, crashes -> core/gotchas.md
  • Text styling not applying -> components/text-display.md
  • Input focus/shortcuts -> keyboard/REFERENCE.md
  • Layout misalignment -> layout/REFERENCE.md
  • Flaky snapshots -> testing/REFERENCE.md

For component naming differences and text modifiers, see components/REFERENCE.md.

Product Index

Frameworks

FrameworkEntry FileDescription
Core./references/core/REFERENCE.mdImperative API, all primitives
React./references/react/REFERENCE.mdReact reconciler for declarative TUI
Solid./references/solid/REFERENCE.mdSolidJS reconciler for declarative TUI

Cross-Cutting Concepts

ConceptEntry FileDescription
Layout./references/layout/REFERENCE.mdYoga/Flexbox layout system
Components./references/components/REFERENCE.mdComponent reference by category
Keyboard./references/keyboard/REFERENCE.mdLow-level keyboard input handling
Keymap./references/keymap/REFERENCE.mdDeclarative layered keybindings (@opentui/keymap)
Animation./references/animation/REFERENCE.mdTimeline-based animations
Testing./references/testing/REFERENCE.mdTest renderer and snapshots

Component Categories

CategoryEntry FileComponents
Text & Display./references/components/text-display.mdtext, ascii-font, styled text, qr-code
Containers./references/components/containers.mdbox, scrollbox, scrollbar, borders
Inputs./references/components/inputs.mdinput, textarea, select, tab-select, slider
Code & Diff./references/components/code-diff.mdcode, line-number, diff, markdown, text-table

Additional Packages

PackageDescriptionDocs
@opentui/keymapLayered keybinding/command engine (Bun or Node, no FFI)./references/keymap/REFERENCE.md
@opentui/qrcodeQR code component./references/components/text-display.md
@opentui/sshServe a TUI over SSH./references/core/REFERENCE.md
@opentui/threeThree.js WebGPU renderer (formerly core/src/3d)upstream packages/three
@opentui/examplesRunnable examples (formerly core/src/examples)upstream packages/examples

Core also ships a native Audio engine and OSC desktop notifications — see ./references/core/api.md.

Resources

Repository: https://github.com/anomalyco/opentui Core Docs: https://github.com/anomalyco/opentui/tree/main/packages/core/docs Examples: https://github.com/anomalyco/opentui/tree/main/packages/examples/src Awesome List: https://github.com/msmps/awesome-opentui

附带文件

references/animation/REFERENCE.md
# Animation System

OpenTUI provides a timeline-based animation system for smooth property transitions.

## Overview

Animations in OpenTUI use:
- **Timeline**: Orchestrates multiple animations
- **Animation Engine**: Manages timelines and rendering
- **Easing Functions**: Control animation curves

## When to Use

Use this reference when you need timeline-driven animations, easing curves, or progressive transitions.

## Basic Usage

### React

```tsx
import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"

function AnimatedBox() {
  const [width, setWidth] = useState(0)
  
  const timeline = useTimeline({
    duration: 2000,
  })
  
  useEffect(() => {
    timeline.add(
      { width: 0 },
      {
        width: 50,
        duration: 2000,
        ease: "easeOutQuad",
        onUpdate: (anim) => {
          setWidth(Math.round(anim.targets[0].width))
        },
      }
    )
  }, [])
  
  return (
    <box
      width={width}
      height={3}
      backgroundColor="#6a5acd"
    />
  )
}
```

### Solid

```tsx
import { useTimeline } from "@opentui/solid"
import { createSignal, onMount } from "solid-js"

function AnimatedBox() {
  const [width, setWidth] = createSignal(0)
  
  const timeline = useTimeline({
    duration: 2000,
  })
  
  onMount(() => {
    timeline.add(
      { width: 0 },
      {
        width: 50,
        duration: 2000,
        ease: "easeOutQuad",
        onUpdate: (anim) => {
          setWidth(Math.round(anim.targets[0].width))
        },
      }
    )
  })
  
  return (
    <box
      width={width()}
      height={3}
      backgroundColor="#6a5acd"
    />
  )
}
```

### Core

```typescript
import { createCliRenderer, Timeline, engine } from "@opentui/core"

const renderer = await createCliRenderer()
engine.attach(renderer)

const timeline = new Timeline({
  duration: 2000,
  autoplay: true,
})

timeline.add(
  { x: 0 },
  {
    x: 50,
    duration: 2000,
    ease: "easeOutQuad",
    onUpdate: (anim) => {
      box.setLeft(Math.round(anim.targets[0].x))
    },
  }
)

engine.addTimeline(timeline)
```

## Timeline Options

```typescript
const timeline = useTimeline({
  duration: 2000,         // Total duration in ms
  loop: false,            // Loop the timeline
  autoplay: true,         // Start automatically
  onComplete: () => {},   // Called when timeline completes
  onPause: () => {},      // Called when timeline pauses
})
```

## Timeline Methods

```typescript
// Add animation
timeline.add(target, properties, startTime?)

// Control playback
timeline.play()           // Start/resume
timeline.pause()          // Pause
timeline.restart()        // Restart from beginning

// State
timeline.progress         // Current progress (0-1)
timeline.duration         // Total duration
```

## Animation Properties

```typescript
timeline.add(
  { value: 0 },           // Target object with initial values
  {
    value: 100,           // Final value
    duration: 1000,       // Animation duration in ms
    ease: "linear",       // Easing function
    delay: 0,             // Delay before starting
    onUpdate: (anim) => {
      // Called each frame
      const current = anim.targets[0].value
    },
    onComplete: () => {
      // Called when this animation completes
    },
  },
  0                       // Start time in timeline (optional)
)
```

## Easing Functions

Available easing functions:

### Linear

| Name | Description |
|------|-------------|
| `linear` | Constant speed |

### Quad (Power of 2)

| Name | Description |
|------|-------------|
| `easeInQuad` | Slow start |
| `easeOutQuad` | Slow end |
| `easeInOutQuad` | Slow start and end |

### Cubic (Power of 3)

| Name | Description |
|------|-------------|
| `easeInCubic` | Slower start |
| `easeOutCubic` | Slower end |
| `easeInOutCubic` | Slower start and end |

### Quart (Power of 4)

| Name | Description |
|------|-------------|
| `easeInQuart` | Even slower start |
| `easeOutQuart` | Even slower end |
| `easeInOutQuart` | Even slower start and end |

### Expo (Exponential)

| Name | Description |
|------|-------------|
| `easeInExpo` | Exponential start |
| `easeOutExpo` | Exponential end |
| `easeInOutExpo` | Exponential start and end |

### Back (Overshoot)

| Name | Description |
|------|-------------|
| `easeInBack` | Pull back, then forward |
| `easeOutBack` | Overshoot, then settle |
| `easeInOutBack` | Both |

### Elastic

| Name | Description |
|------|-------------|
| `easeInElastic` | Elastic start |
| `easeOutElastic` | Elastic end (bouncy) |
| `easeInOutElastic` | Both |

### Bounce

| Name | Description |
|------|-------------|
| `easeInBounce` | Bounce at start |
| `easeOutBounce` | Bounce at end |
| `easeInOutBounce` | Both |

## Patterns

### Progress Bar

```tsx
function ProgressBar({ progress }: { progress: number }) {
  const [width, setWidth] = useState(0)
  const maxWidth = 50
  
  const timeline = useTimeline()
  
  useEffect(() => {
    timeline.add(
      { value: width },
      {
        value: (progress / 100) * maxWidth,
        duration: 300,
        ease: "easeOutQuad",
        onUpdate: (anim) => {
          setWidth(Math.round(anim.targets[0].value))
        },
      }
    )
  }, [progress])
  
  return (
    <box flexDirection="column" gap={1}>
      <text>Progress: {progress}%</text>
      <box width={maxWidth} height={1} backgroundColor="#333">
        <box width={width} height={1} backgroundColor="#00FF00" />
      </box>
    </box>
  )
}
```

### Fade In

```tsx
function FadeIn({ children }) {
  const [opacity, setOpacity] = useState(0)
  
  const timeline = useTimeline()
  
  useEffect(() => {
    timeline.add(
      { opacity: 0 },
      {
        opacity: 1,
        duration: 500,
        ease: "easeOutQuad",
        onUpdate: (anim) => {
          setOpacity(anim.targets[0].opacity)
        },
      }
    )
  }, [])
  
  return (
    <box style={{ opacity }}>
      {children}
    </box>
  )
}
```

### Looping Animation

```tsx
function Spinner() {
  const [frame, setFrame] = useState(0)
  const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
  
  useEffect(() => {
    const interval = setInterval(() => {
      setFrame(f => (f + 1) % frames.length)
    }, 80)
    
    return () => clearInterval(interval)
  }, [])
  
  return <text>{frames[frame]} Loading...</text>
}
```

### Staggered Animation

```tsx
function StaggeredList({ items }) {
  const [visibleCount, setVisibleCount] = useState(0)
  
  useEffect(() => {
    let count = 0
    const interval = setInterval(() => {
      count++
      setVisibleCount(count)
      if (count >= items.length) {
        clearInterval(interval)
      }
    }, 100)
    
    return () => clearInterval(interval)
  }, [items.length])
  
  return (
    <box flexDirection="column">
      {items.slice(0, visibleCount).map((item, i) => (
        <text key={i}>{item}</text>
      ))}
    </box>
  )
}
```

### Slide In

```tsx
function SlideIn({ children, from = "left" }) {
  const [offset, setOffset] = useState(from === "left" ? -20 : 20)
  
  const timeline = useTimeline()
  
  useEffect(() => {
    timeline.add(
      { offset: from === "left" ? -20 : 20 },
      {
        offset: 0,
        duration: 300,
        ease: "easeOutCubic",
        onUpdate: (anim) => {
          setOffset(Math.round(anim.targets[0].offset))
        },
      }
    )
  }, [])
  
  return (
    <box position="relative" left={offset}>
      {children}
    </box>
  )
}
```

## Performance Tips

### Batch Updates

Timeline automatically batches updates within the render loop.

### Use Integer Values

Round animated values for character-based positioning:

```typescript
onUpdate: (anim) => {
  setX(Math.round(anim.targets[0].x))
}
```

### Clean Up Timelines

Hooks automatically clean up, but for core:

```typescript
// When done with timeline
engine.removeTimeline(timeline)
```

## Gotchas

### Terminal Refresh Rate

Terminal UIs typically refresh at 60 FPS max. Very fast animations may appear choppy.

### Character Grid

Animations are constrained to character cells. Sub-pixel positioning isn't possible.

### Cleanup in Effects

Always clean up intervals and timelines:

```tsx
useEffect(() => {
  const interval = setInterval(...)
  return () => clearInterval(interval)
}, [])
```

## See Also

- [React API](../react/api.md) - `useTimeline` hook reference
- [Solid API](../solid/api.md) - `useTimeline` hook reference
- [Core API](../core/api.md) - `AnimationEngine` and `Timeline` classes
- [Layout Patterns](../layout/patterns.md) - Animated positioning and transitions
references/components/code-diff.md
# Code & Diff Components

Components for displaying code with syntax highlighting and diffs in OpenTUI.

## Code Component

Display syntax-highlighted code blocks.

### Basic Usage

```tsx
// React
<code
  code={`function hello() {
  console.log("Hello, World!");
}`}
  language="typescript"
/>

// Solid
<code
  code={sourceCode}
  language="javascript"
/>

// Core
const codeBlock = new CodeRenderable(renderer, {
  id: "code",
  code: sourceCode,
  language: "typescript",
})
```

### Supported Languages

OpenTUI uses Tree-sitter for syntax highlighting. Common languages:
- `typescript`, `javascript`
- `python`
- `rust`
- `go`
- `json`
- `html`, `css`
- `markdown`
- `bash`, `shell`

### Styling

```tsx
<code
  code={sourceCode}
  language="typescript"
  backgroundColor="#1a1a2e"
  showLineNumbers
/>
```

### onHighlight Callback

Intercept and modify syntax highlights before rendering:

```tsx
// Core
const codeBlock = new CodeRenderable(renderer, {
  id: "code",
  code: sourceCode,
  language: "typescript",
  onHighlight: (highlights, context) => {
    // Add custom highlights
    highlights.push([10, 20, "custom.error", {}])
    return highlights
  },
})

// React/Solid
<code
  code={sourceCode}
  language="typescript"
  onHighlight={(highlights, context) => {
    // context: { content, filetype, syntaxStyle }
    // Modify and return highlights array
    return highlights.filter(h => h[2] !== "comment")
  }}
/>
```

**Callback signature:**
- `highlights: SimpleHighlight[]` - Array of `[start, end, scope, metadata]`
- `context: { content, filetype, syntaxStyle }` - Highlighting context
- Return modified highlights array or `undefined` to use original

Supports async callbacks for fetching additional highlight data.

### onChunks Callback

Post-process rendered text chunks after syntax highlighting. Runs after `onHighlight` and receives fully resolved chunks:

```tsx
// Core
const codeBlock = new CodeRenderable(renderer, {
  id: "code",
  code: sourceCode,
  language: "typescript",
  onChunks: (chunks, context) => {
    // Transform chunks (e.g., add link detection)
    return chunks
  },
})

// React/Solid
<code
  code={sourceCode}
  language="typescript"
  onChunks={(chunks, context) => {
    // context: { content, filetype, syntaxStyle, highlights }
    return chunks
  }}
/>
```

### Link Detection Utility

Auto-detect URLs in code and add clickable hyperlinks:

```typescript
import { detectLinks } from "@opentui/core"

<code
  code={sourceCode}
  language="typescript"
  onChunks={(chunks, context) => detectLinks(chunks, context)}
/>
```

`detectLinks` examines Tree-sitter highlights to find URL tokens and sets `chunk.link` on matching chunks. Supports async usage.

## TextTable Component

Render data tables with borders, word wrapping, and selection support.

### Basic Usage

```typescript
// Core
import { TextTableRenderable, type TextTableContent } from "@opentui/core"

const content: TextTableContent = [
  [[ { text: "Name" } ], [ { text: "Age" } ], [ { text: "Role" } ]],
  [[ { text: "Alice" } ], [ { text: "30" } ], [ { text: "Engineer" } ]],
  [[ { text: "Bob" } ], [ { text: "25" } ], [ { text: "Designer" } ]],
]

const table = new TextTableRenderable(renderer, {
  id: "table",
  content,
  wrapMode: "word",           // "none" | "char" | "word"
  columnWidthMode: "content", // "content" | "fill"
  cellPadding: 0,
  border: true,
  outerBorder: true,
  borderStyle: "single",      // single | double | rounded | bold
  selectable: true,           // Allow text selection
  columnFitter: "balanced",   // "proportional" | "balanced"
})
```

### Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `content` | `TextTableContent` | - | 2D array of cell content |
| `wrapMode` | `"none" \| "char" \| "word"` | `"none"` | Text wrapping in cells |
| `columnWidthMode` | `"content" \| "fill"` | `"content"` | Column sizing strategy |
| `cellPadding` | `number` | `0` | Padding inside cells |
| `border` | `boolean` | `true` | Show inner borders |
| `outerBorder` | `boolean` | `true` | Show outer borders |
| `borderStyle` | `string` | `"single"` | Border style |
| `borderColor` | `string \| RGBA` | - | Border color |
| `selectable` | `boolean` | `false` | Allow text selection |
| `columnFitter` | `"proportional" \| "balanced"` | `"proportional"` | Column width distribution |

### Cell Content Format

Each cell is an array of styled text chunks:

```typescript
type TextTableCellContent = { text: string; fg?: RGBA; bg?: RGBA }[]
type TextTableContent = TextTableCellContent[][]  // rows -> cells -> chunks
```

### Selection

```typescript
table.getSelectedText()  // Get selected text
table.hasSelection()     // Check if text is selected
```

Columnar selection is supported: dragging vertically within a single column selects only that column's content.

## Line Number Component

Code display with line numbers, highlighting, and diagnostics.

### Basic Usage

```tsx
// React
<line-number
  code={sourceCode}
  language="typescript"
/>

// Solid (note underscore)
<line_number
  code={sourceCode}
  language="typescript"
/>

// Core
const codeView = new LineNumberRenderable(renderer, {
  id: "code-view",
  code: sourceCode,
  language: "typescript",
})
```

### Line Number Options

```tsx
// React
<line-number
  code={sourceCode}
  language="typescript"
  startLine={1}              // Starting line number
  showLineNumbers={true}     // Display line numbers
/>

// Solid
<line_number
  code={sourceCode}
  language="typescript"
  startLine={1}
  showLineNumbers={true}
/>
```

### Line Highlighting

Highlight specific lines:

```tsx
// React
<line-number
  code={sourceCode}
  language="typescript"
  highlightedLines={[5, 10, 15]}  // Highlight these lines
/>

// Solid
<line_number
  code={sourceCode}
  language="typescript"
  highlightedLines={[5, 10, 15]}
/>
```

### Diagnostics

Show errors, warnings, and info on specific lines:

```tsx
// React
<line-number
  code={sourceCode}
  language="typescript"
  diagnostics={[
    { line: 3, severity: "error", message: "Unexpected token" },
    { line: 7, severity: "warning", message: "Unused variable" },
    { line: 12, severity: "info", message: "Consider using const" },
  ]}
/>

// Solid
<line_number
  code={sourceCode}
  language="typescript"
  diagnostics={[
    { line: 3, severity: "error", message: "Unexpected token" },
  ]}
/>
```

**Diagnostic severity levels:**
- `error` - Red indicator
- `warning` - Yellow indicator
- `info` - Blue indicator
- `hint` - Gray indicator

### Diff Highlighting

Show added/removed lines:

```tsx
<line-number
  code={sourceCode}
  language="typescript"
  addedLines={[5, 6, 7]}      // Green background
  removedLines={[10, 11]}     // Red background
/>
```

## Diff Component

Unified or split diff viewer with syntax highlighting.

### Basic Usage

```tsx
// React
<diff
  oldCode={originalCode}
  newCode={modifiedCode}
  language="typescript"
/>

// Solid
<diff
  oldCode={originalCode}
  newCode={modifiedCode}
  language="typescript"
/>

// Core
const diffView = new DiffRenderable(renderer, {
  id: "diff",
  oldCode: originalCode,
  newCode: modifiedCode,
  language: "typescript",
})
```

### Display Modes

```tsx
// Unified diff (default)
<diff
  oldCode={old}
  newCode={new}
  mode="unified"
/>

// Split/side-by-side diff
<diff
  oldCode={old}
  newCode={new}
  mode="split"
/>
```

### Synchronized Scrolling (Split View)

In split view, enable synchronized scrolling between left and right panes:

```tsx
// React/Solid
<diff
  oldCode={old}
  newCode={new}
  mode="split"
  syncScroll              // Scrolling one pane syncs the other
/>

// Core
const diffView = new DiffRenderable(renderer, {
  id: "diff",
  diff: unifiedDiff,
  view: "split",
  syncScroll: true,
})

// Toggle at runtime
diffView.syncScroll = true
diffView.syncScroll = false
```

### Options

```tsx
<diff
  oldCode={originalCode}
  newCode={modifiedCode}
  language="typescript"
  mode="unified"
  showLineNumbers
  context={3}                // Lines of context around changes
/>
```

### Styling

```tsx
<diff
  oldCode={old}
  newCode={new}
  addedLineColor="#2d4f2d"   // Background for added lines
  removedLineColor="#4f2d2d" // Background for removed lines
  unchangedLineColor="transparent"
/>
```

### Line Highlighting API (Core)

Programmatically highlight specific lines in a diff:

```typescript
// Set a single line's color
diffView.setLineColor(5, "#2d4f2d")
diffView.setLineColor(5, { gutter: "#333", content: "#2d4f2d" })

// Clear a single line's color
diffView.clearLineColor(5)

// Set multiple lines at once
diffView.setLineColors(new Map([
  [1, "#2d4f2d"],
  [2, "#4f2d2d"],
]))

// Highlight a range
diffView.highlightLines(10, 20, "#2d4f2d")
diffView.clearHighlightLines(10, 20)

// Clear all line colors
diffView.clearAllLineColors()
```

The `LineNumberRenderable` also supports programmatic highlighting:

```typescript
lineNumberView.highlightLines(5, 10, "#2d4f2d")
lineNumberView.clearHighlightLines(5, 10)
```

### Hunk Navigation (Core)

`getHunkRowOffsets()` returns the visual row offset (0-based, accounting for
wrapping and concealed lines) where each diff hunk begins, in hunk order. Works
in both `"unified"` and `"split"` views.

```typescript
const offsets = diffView.getHunkRowOffsets()  // number[]

// Jump to the next hunk below the current scroll position
const next = offsets.find((row) => row > diffView.scrollTop)
if (next !== undefined) diffView.scrollTop = next

// Jump to the previous hunk
const prev = [...offsets].reverse().find((row) => row < diffView.scrollTop)
if (prev !== undefined) diffView.scrollTop = prev
```

The result is cached and invalidated when the view rebuilds, `wrapMode` changes,
or line info changes.

## Markdown Component

Render markdown content with syntax highlighting for code blocks.

### Basic Usage

```tsx
// React
<markdown
  content={markdownText}
  syntaxStyle={mySyntaxStyle}
/>

// Solid
<markdown
  content={markdownText}
  syntaxStyle={mySyntaxStyle}
/>

// Core
import { MarkdownRenderable } from "@opentui/core"

const md = new MarkdownRenderable(renderer, {
  id: "markdown",
  content: "# Hello\n\nThis is **markdown**.",
  syntaxStyle: mySyntaxStyle,
})
```

### Options

```tsx
<markdown
  content={markdownText}
  syntaxStyle={syntaxStyle}    // Required
  treeSitterClient={client}    // Optional: custom tree-sitter client
  conceal={true}               // Hide markdown syntax characters (default true)
  concealCode={false}          // Conceal code fences too (default false)
  streaming={true}             // Enable streaming mode for incremental updates
  internalBlockMode="coalesced" // "coalesced" (default) | "top-level"
  tableOptions={{              // Customize markdown table rendering
    style: "columns",          // "grid" | "columns"
    widthMode: "full",         // "content" | "full"
    wrapMode: "word",          // "none" | "char" | "word"
    cellPadding: 0,
    borders: true,
    outerBorder: true,
    borderStyle: "single",
    borderColor: "#555",
    selectable: true,          // Tables are selectable by default
  }}
/>
```

**`internalBlockMode`**: `"top-level"` keeps each top-level block (heading,
paragraph, list, table, fenced code) as its own child renderable — required for
row-by-row committing of streamed output and for stable-block tracking. The
default `"coalesced"` folds siblings together (historical layout). When set to
`"top-level"`, `markdown._stableBlockCount` reports how many head-of-stream
blocks are currently stable.

### Custom Node Rendering

```tsx
// Core
const md = new MarkdownRenderable(renderer, {
  id: "markdown",
  content: "# Custom Heading",
  syntaxStyle,
  renderNode: (node, ctx, defaultRender) => {
    if (node.type === "heading") {
      // Return custom renderable for headings
      return new TextRenderable(ctx, {
        content: `>> ${node.content} <<`,
      })
    }
    return null // Use default rendering
  },
})
```

### Custom Code Block Renderers

`createMarkdownCodeBlockRenderer` builds a `renderNode` that only overrides
fenced code blocks, keyed by the **normalized** fence info string (e.g. `tsx` →
`typescriptreact`; custom names like `taskflow` match directly):

```typescript
import { createMarkdownCodeBlockRenderer, MarkdownRenderable } from "@opentui/core"

const renderNode = createMarkdownCodeBlockRenderer({
  mermaid: (token, ctx) => renderMermaidDiagram(ctx, token.text),
  taskflow: (token, ctx) => renderTaskflow(ctx, token.text),
})

const md = new MarkdownRenderable(renderer, {
  id: "markdown",
  content,
  syntaxStyle,
  renderNode,  // Only the matched fences are replaced; others render normally
})
```

Each renderer receives `(token: Tokens.Code, context: RenderNodeContext)` and
returns a `Renderable`, or `undefined`/`null` to fall back to default rendering.

### Streaming Mode

For real-time content like LLM output:

```tsx
const [content, setContent] = useState("")

// Append text as it arrives
useEffect(() => {
  llmStream.on("token", (token) => {
    setContent(c => c + token)
  })
}, [])

<markdown
  content={content}
  syntaxStyle={syntaxStyle}
  streaming={true}  // Optimizes for incremental updates
/>
```

In Core, set `markdown.streaming = true` while appending chunks (assign or `+=`
to `markdown.content`; each change reparses incrementally). Set
`markdown.streaming = false` when the stream completes to finalize trailing
block parsing. Use `internalBlockMode: "top-level"` if you need per-block commit
boundaries (`markdown._stableBlockCount`).

## Use Cases

### Code Editor

```tsx
function CodeEditor() {
  const [code, setCode] = useState(`function hello() {
  console.log("Hello!");
}`)
  
  return (
    <box flexDirection="column" height="100%">
      <box height={1}>
        <text>editor.ts</text>
      </box>
      <textarea
        value={code}
        onChange={setCode}
        language="typescript"
        showLineNumbers
        flexGrow={1}
        focused
      />
    </box>
  )
}
```

### Code Review

```tsx
function CodeReview({ oldCode, newCode }) {
  return (
    <box flexDirection="column" height="100%">
      <box height={1} backgroundColor="#333">
        <text>Changes in src/utils.ts</text>
      </box>
      <diff
        oldCode={oldCode}
        newCode={newCode}
        language="typescript"
        mode="split"
        showLineNumbers
      />
    </box>
  )
}
```

### Syntax-Highlighted Preview

```tsx
function MarkdownPreview({ content }) {
  // Extract code blocks from markdown
  const codeBlocks = extractCodeBlocks(content)
  
  return (
    <scrollbox height={20}>
      {codeBlocks.map((block, i) => (
        <box key={i} marginBottom={1}>
          <code
            code={block.code}
            language={block.language}
          />
        </box>
      ))}
    </scrollbox>
  )
}
```

### Error Display

```tsx
function ErrorView({ errors, code }) {
  const diagnostics = errors.map(err => ({
    line: err.line,
    severity: "error",
    message: err.message,
  }))
  
  return (
    <line-number
      code={code}
      language="typescript"
      diagnostics={diagnostics}
      highlightedLines={errors.map(e => e.line)}
    />
  )
}
```

## Gotchas

### Solid Uses Underscores

```tsx
// React
<line-number />

// Solid
<line_number />
```

### Language Required for Highlighting

```tsx
// No highlighting (plain text)
<code code={text} />

// With highlighting
<code code={text} language="typescript" />
```

### Large Files

For very large files, consider:
- Pagination or virtual scrolling
- Loading only visible portion
- Using `scrollbox` wrapper

```tsx
<scrollbox height={30}>
  <line-number
    code={largeFile}
    language="typescript"
  />
</scrollbox>
```

### Tree-sitter Loading

Syntax highlighting requires Tree-sitter grammars. If highlighting isn't working:

1. Check the language is supported
2. Verify grammars are installed
3. Check `OTUI_TREE_SITTER_WORKER_PATH` if using custom path
references/components/containers.md
# Container Components

Components for grouping and organizing content in OpenTUI.

## Box Component

The primary container component with borders, backgrounds, and layout capabilities.

### Basic Usage

```tsx
// React/Solid
<box>
  <text>Content inside box</text>
</box>

// Core
const box = new BoxRenderable(renderer, {
  id: "container",
})
box.add(child)
```

### Borders

```tsx
<box border>
  Simple border
</box>

<box
  border
  borderStyle="single"    // single | double | rounded | bold | none
  borderColor="#FFFFFF"
>
  Styled border
</box>

// Individual borders
<box
  borderTop
  borderBottom
  borderLeft={false}
  borderRight={false}
>
  Top and bottom only
</box>
```

**Border Styles:**

| Style | Appearance |
|-------|------------|
| `single` | `┌─┐│ │└─┘` |
| `double` | `╔═╗║ ║╚═╝` |
| `rounded` | `╭─╮│ │╰─╯` |
| `bold` | `┏━┓┃ ┃┗━┛` |

### Title

```tsx
<box
  border
  title="Settings"
  titleColor="#FFCC00"          // Title text color (defaults to border color)
  titleAlignment="center"       // left | center | right
  bottomTitle="Press q to quit" // Title text in the bottom border
  bottomTitleAlignment="right"  // left | center | right
>
  Panel content
</box>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `title` | `string` | – | Title text in the top border |
| `titleColor` | `string \| RGBA` | border color | Color of the title text |
| `titleAlignment` | `"left" \| "center" \| "right"` | `"left"` | Top title position |
| `bottomTitle` | `string` | – | Title text in the bottom border |
| `bottomTitleAlignment` | `"left" \| "center" \| "right"` | `"left"` | Bottom title position |

### Background

```tsx
<box backgroundColor="#1a1a2e">
  Dark background
</box>

<box backgroundColor="transparent">
  No background
</box>
```

### Layout

Boxes are flex containers by default:

```tsx
<box
  flexDirection="row"       // row | column | row-reverse | column-reverse
  justifyContent="center"   // flex-start | flex-end | center | space-between | space-around
  alignItems="center"       // flex-start | flex-end | center | stretch | baseline
  gap={2}                   // Space between children
>
  <text>Item 1</text>
  <text>Item 2</text>
</box>
```

### Spacing

```tsx
<box
  padding={2}               // All sides
  paddingTop={1}
  paddingRight={2}
  paddingBottom={1}
  paddingLeft={2}
  paddingX={2}              // Horizontal (left + right)
  paddingY={1}              // Vertical (top + bottom)
  margin={1}
  marginTop={1}
  marginX={2}               // Horizontal (left + right)
  marginY={1}               // Vertical (top + bottom)
>
  Spaced content
</box>
```

### Dimensions

```tsx
<box
  width={40}                // Fixed width
  height={10}               // Fixed height
  width="50%"               // Percentage of parent
  minWidth={20}             // Minimum width
  maxWidth={80}             // Maximum width
  flexGrow={1}              // Grow to fill space
>
  Sized box
</box>
```

### Mouse Events

```tsx
<box
  onMouseDown={(event) => {
    console.log("Clicked at:", event.x, event.y)
  }}
  onMouseUp={(event) => {}}
  onMouseMove={(event) => {}}
>
  Clickable box
</box>
```

### Focusable Boxes

By default, Box elements are not focusable. Set the `focusable` prop to enable focus behavior:

```tsx
// Make a box focusable - it can receive focus via mouse click
<box focusable border>
  <text>Click to focus</text>
</box>

// Controlled focus state
const [focused, setFocused] = useState(false)

<box
  focusable
  focused={focused}
  border
  borderColor={focused ? "#00ff00" : "#888"}
>
  <text>{focused ? "Focused!" : "Not focused"}</text>
</box>
```

When a focusable Box is clicked, focus bubbles up from the click target to the nearest focusable parent. Use `event.preventDefault()` in `onMouseDown` to prevent auto-focus.

## ScrollBox Component

A scrollable container for content that exceeds the viewport.

### Basic Usage

```tsx
// React
<scrollbox height={10}>
  {items.map((item, i) => (
    <text key={i}>{item}</text>
  ))}
</scrollbox>

// Solid
<scrollbox height={10}>
  <For each={items()}>
    {(item) => <text>{item}</text>}
  </For>
</scrollbox>

// Core
const scrollbox = new ScrollBoxRenderable(renderer, {
  id: "list",
  height: 10,
})
items.forEach(item => {
  scrollbox.add(new TextRenderable(renderer, { content: item }))
})
```

### Focus for Keyboard Scrolling

```tsx
<scrollbox focused height={20}>
  {/* Use arrow keys to scroll */}
</scrollbox>
```

### Scrollbar Styling

```tsx
// React
<scrollbox
  style={{
    rootOptions: {
      backgroundColor: "#24283b",
    },
    wrapperOptions: {
      backgroundColor: "#1f2335",
    },
    viewportOptions: {
      backgroundColor: "#1a1b26",
    },
    contentOptions: {
      backgroundColor: "#16161e",
    },
    scrollbarOptions: {
      showArrows: true,
      trackOptions: {
        foregroundColor: "#7aa2f7",
        backgroundColor: "#414868",
      },
    },
  }}
>
  {content}
</scrollbox>
```

### Scroll Position (Core)

```typescript
const scrollbox = new ScrollBoxRenderable(renderer, {
  id: "list",
  height: 20,
})

// Scroll programmatically
scrollbox.scrollTo(0)           // Scroll to top
scrollbox.scrollTo(100)         // Scroll to position
scrollbox.scrollBy(10)          // Scroll relative
scrollbox.scrollToBottom()      // Scroll to end

// Scroll a child into view (nearest alignment)
scrollbox.scrollChildIntoView("child-id")  // Searches descendants by ID
```

`scrollChildIntoView(childId)` scrolls the minimum amount needed to make the identified descendant visible. It mirrors `Element.scrollIntoView({ block: "nearest" })` from the CSSOM View spec. Works with nested descendants and handles both horizontal and vertical scrolling.

## ScrollBar Component

A **standalone** scrollbar (separate from `scrollbox`) with optional arrows, a
draggable thumb, and keyboard navigation. `ScrollBarRenderable` is exported from
`@opentui/core`. Connect it to any content by wiring its size/position props.

```typescript
import { ScrollBarRenderable } from "@opentui/core"

const scrollbar = new ScrollBarRenderable(renderer, {
  id: "scrollbar",
  orientation: "vertical",   // "vertical" | "horizontal"
  showArrows: false,
  scrollSize: 0,             // Total content size
  viewportSize: 0,           // Visible size
  scrollPosition: 0,         // Current position
  onChange: (position) => {
    // Sync your content to the new scroll position
  },
})

// Drive it from your content dimensions, then focus for keyboard control
scrollbar.scrollSize = content.length
scrollbar.viewportSize = visibleRows
scrollbar.scrollPosition = 0
scrollbar.focus()
```

| Prop | Type | Default |
|------|------|---------|
| `orientation` | `"vertical" \| "horizontal"` | – |
| `showArrows` | `boolean` | `false` |
| `arrowOptions` | `ArrowOptions` | – |
| `trackOptions` | `Partial<SliderOptions>` | – |
| `scrollSize` | `number` | `0` |
| `viewportSize` | `number` | `0` |
| `scrollPosition` | `number` | `0` |
| `scrollStep` | `number` | – |
| `onChange` | `(position: number) => void` | – |

When focused: arrows / `hjkl`, `PageUp`/`PageDown`, `Home`/`End`.

> Most apps should use `scrollbox` (which embeds a scrollbar). Reach for
> `ScrollBarRenderable` only when you need a scrollbar decoupled from a
> `scrollbox` viewport.

## Composition Patterns

### Card Component

```tsx
function Card({ title, children }) {
  return (
    <box
      border
      borderStyle="rounded"
      padding={2}
      marginBottom={1}
    >
      {title && (
        <text fg="#00FFFF" bold>
          {title}
        </text>
      )}
      <box marginTop={title ? 1 : 0}>
        {children}
      </box>
    </box>
  )
}
```

### Panel Component

```tsx
function Panel({ title, children, width = 40 }) {
  return (
    <box
      border
      borderStyle="double"
      width={width}
      backgroundColor="#1a1a2e"
    >
      {title && (
        <box
          borderBottom
          padding={1}
          backgroundColor="#2a2a4e"
        >
          <text bold>{title}</text>
        </box>
      )}
      <box padding={2}>
        {children}
      </box>
    </box>
  )
}
```

### List Container

```tsx
function List({ items, renderItem }) {
  return (
    <scrollbox height={15} focused>
      {items.map((item, i) => (
        <box
          key={i}
          padding={1}
          backgroundColor={i % 2 === 0 ? "#222" : "#333"}
        >
          {renderItem(item, i)}
        </box>
      ))}
    </scrollbox>
  )
}
```

## Nesting Containers

```tsx
<box flexDirection="column" height="100%">
  {/* Header */}
  <box height={3} border>
    <text>Header</text>
  </box>
  
  {/* Main area with sidebar */}
  <box flexDirection="row" flexGrow={1}>
    <box width={20} border>
      <text>Sidebar</text>
    </box>
    <box flexGrow={1}>
      <scrollbox height="100%">
        {/* Scrollable content */}
      </scrollbox>
    </box>
  </box>
  
  {/* Footer */}
  <box height={1}>
    <text>Footer</text>
  </box>
</box>
```

## Gotchas

### Percentage Dimensions Need Parent Size

```tsx
// WRONG - parent has no explicit size
<box>
  <box width="50%">Won't work</box>
</box>

// CORRECT
<box width="100%">
  <box width="50%">Works</box>
</box>
```

### FlexGrow Needs Sized Parent

```tsx
// WRONG
<box>
  <box flexGrow={1}>Won't grow</box>
</box>

// CORRECT
<box height="100%">
  <box flexGrow={1}>Will grow</box>
</box>
```

### ScrollBox Needs Height

```tsx
// WRONG - no height constraint
<scrollbox>
  {items}
</scrollbox>

// CORRECT
<scrollbox height={20}>
  {items}
</scrollbox>
```

### Borders Add to Size

Borders take up space inside the box:

```tsx
<box width={10} border>
  {/* Inner content area is 8 chars (10 - 2 for borders) */}
</box>
```
references/components/inputs.md
# Input Components

Components for user input in OpenTUI.

## Input Component

Single-line text input field.

### Basic Usage

```tsx
// 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

```tsx
<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

```tsx
// 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

```tsx
// 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

```tsx
// React
<textarea
  value={text}
  onChange={(newText) => setText(newText)}
  placeholder="Enter multiple lines..."
  width={40}
  height={10}
  focused
/>

// Solid
<textarea
  value={text()}
  onInput={(newText) => setText(newText)}
  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

```tsx
<textarea
  showLineNumbers        // Display line numbers
  wrapText              // Wrap long lines
  readOnly              // Disable editing
  tabSize={2}           // Tab character width
/>
```

### Syntax Highlighting

```tsx
<textarea
  language="typescript"
  value={code}
  onChange={setCode}
/>
```

## Select Component

List selection for choosing from options.

### Basic Usage

```tsx
// 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

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

### Styling

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

> **`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
- `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 |

```tsx
// 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

```tsx
// 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:

```tsx
<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

```tsx
// 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`).

```typescript
// 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

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

### Multiple Inputs with Focus State

```tsx
// 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)

```typescript
input.focus()      // Give focus
input.blur()       // Remove focus
input.isFocused()  // Check focus state
```

## Form Patterns

### Login Form

```tsx
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

```tsx
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:

```tsx
// 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:

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

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

### Solid Uses Underscores

```tsx
// React
<tab-select />

// Solid
<tab_select />
```

### Value vs onInput (Solid)

Solid uses `onInput` instead of `onChange`:

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

// Solid
<input value={value()} onInput={setValue} />
```
references/components/REFERENCE.md
# OpenTUI Components

Reference for all OpenTUI components, organized by category. Components are available in all three frameworks (Core, React, Solid) with slight API differences.

## When to Use

Use this reference when you need to find the right component category or compare naming across Core, React, and Solid.

## Component Categories

| Category | Components | File |
|----------|------------|------|
| Text & Display | text, ascii-font, styled text, qr-code | [text-display.md](./text-display.md) |
| Containers | box, scrollbox, scrollbar, borders | [containers.md](./containers.md) |
| Inputs | input, textarea, select, tab-select, slider | [inputs.md](./inputs.md) |
| Code & Diff | code, line-number, diff, markdown, text-table | [code-diff.md](./code-diff.md) |

## Component Chooser

```
Need a component?
├─ Styled text, ASCII art, or QR code -> text-display.md
├─ Containers, borders, scrolling, scrollbar -> containers.md
├─ Forms, input controls, sliders -> inputs.md
└─ Code blocks, diffs, line numbers, markdown -> code-diff.md
```

## Component Naming

Components have different names across frameworks:

| Concept | Core (Class) | React (JSX) | Solid (JSX) |
|---------|--------------|-------------|-------------|
| Text | `TextRenderable` | `<text>` | `<text>` |
| Box | `BoxRenderable` | `<box>` | `<box>` |
| ScrollBox | `ScrollBoxRenderable` | `<scrollbox>` | `<scrollbox>` |
| Input | `InputRenderable` | `<input>` | `<input>` |
| Textarea | `TextareaRenderable` | `<textarea>` | `<textarea>` |
| Select | `SelectRenderable` | `<select>` | `<select>` |
| Tab Select | `TabSelectRenderable` | `<tab-select>` | `<tab_select>` |
| ASCII Font | `ASCIIFontRenderable` | `<ascii-font>` | `<ascii_font>` |
| Code | `CodeRenderable` | `<code>` | `<code>` |
| Line Number | `LineNumberRenderable` | `<line-number>` | `<line_number>` |
| Diff | `DiffRenderable` | `<diff>` | `<diff>` |
| Markdown | `MarkdownRenderable` | `<markdown>` | `<markdown>` |
| TextTable | `TextTableRenderable` | N/A (Core only) | N/A (Core only) |
| Slider | `SliderRenderable` | N/A (Core only) | N/A (Core only) |
| ScrollBar | `ScrollBarRenderable` | N/A (Core only) | N/A (Core only) |
| FrameBuffer | `FrameBufferRenderable` | N/A (Core + construct) | N/A (Core + construct) |
| QR Code | `QRCodeRenderable` | `<qr-code>` * | `<qr_code>` * |

**Note**: Solid uses underscores (`tab_select`) while React uses hyphens (`tab-select`). `TextTableRenderable` is used internally by `MarkdownRenderable` for table rendering and is also available as a standalone Core component. `SliderRenderable` and `ScrollBarRenderable` do not yet expose a construct/JSX API — use the Core class. `FrameBufferRenderable` is Core-only with a `FrameBuffer({...})` construct wrapper (no reconciler JSX element). `* QRCodeRenderable` ships in the separate `@opentui/qrcode` package and requires `registerQRCode()` before the JSX element is available (see [text-display.md](./text-display.md)).

## Common Properties

All components share these layout properties (see [Layout](../layout/REFERENCE.md)):

```tsx
// Positioning
position="relative" | "absolute"
left, top, right, bottom

// Dimensions
width, height
minWidth, maxWidth, minHeight, maxHeight

// Flexbox
flexDirection, flexGrow, flexShrink, flexBasis
justifyContent, alignItems, alignSelf
flexWrap, gap

// Spacing
padding, paddingTop, paddingRight, paddingBottom, paddingLeft
paddingX, paddingY              // Axis shorthand (horizontal/vertical)
margin, marginTop, marginRight, marginBottom, marginLeft
marginX, marginY                // Axis shorthand (horizontal/vertical)

// Display
display="flex" | "none"
overflow="visible" | "hidden" | "scroll"
zIndex
```

## Quick Examples

### Core (Imperative)

```typescript
import { createCliRenderer, TextRenderable, BoxRenderable } from "@opentui/core"

const renderer = await createCliRenderer()

const box = new BoxRenderable(renderer, {
  id: "container",
  border: true,
  padding: 2,
})

const text = new TextRenderable(renderer, {
  id: "greeting",
  content: "Hello!",
  fg: "#00FF00",
})

box.add(text)
renderer.root.add(box)
```

### React

```tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"

function App() {
  return (
    <box border padding={2}>
      <text fg="#00FF00">Hello!</text>
    </box>
  )
}

const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
```

### Solid

```tsx
import { render } from "@opentui/solid"

function App() {
  return (
    <box border padding={2}>
      <text fg="#00FF00">Hello!</text>
    </box>
  )
}

render(() => <App />)
```

## See Also

- [Core API](../core/api.md) - Imperative component classes
- [React API](../react/api.md) - React component props
- [Solid API](../solid/api.md) - Solid component props
- [Layout](../layout/REFERENCE.md) - Layout system details
references/components/text-display.md
# Text & Display Components

Components for displaying text content in OpenTUI.

## Text Component

The primary component for displaying styled text.

### Basic Usage

```tsx
// React/Solid
<text>Hello, World!</text>

// With content prop
<text content="Hello, World!" />

// Core
const text = new TextRenderable(renderer, {
  id: "greeting",
  content: "Hello, World!",
})
```

### Styling (React/Solid)

For React and Solid, use **nested modifier tags** for text styling:

```tsx
<text fg="#FFFFFF" bg="#000000">
  <strong>Bold</strong>, <em>italic</em>, and <u>underlined</u>
</text>
```

> **Important**: Do NOT use `bold`, `italic`, `underline`, `dim`, `strikethrough` as props on `<text>` — they don't work. Always use nested tags like `<strong>`, `<em>`, `<u>`, or `<span>` with styling.

### Styling (Core) - Text Attributes

```typescript
import { TextRenderable, TextAttributes } from "@opentui/core"

const text = new TextRenderable(renderer, {
  content: "Styled",
  attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,
})
```

**Available attributes:**
- `TextAttributes.BOLD`
- `TextAttributes.DIM`
- `TextAttributes.ITALIC`
- `TextAttributes.UNDERLINE`
- `TextAttributes.BLINK`
- `TextAttributes.INVERSE`
- `TextAttributes.HIDDEN`
- `TextAttributes.STRIKETHROUGH`

### Text Selection

```tsx
<text selectable>
  This text can be selected by the user
</text>

<text selectable={false}>
  This text cannot be selected
</text>
```

For copy-on-selection and the full selection API, see `keyboard/REFERENCE.md` (selection).

## Text Modifiers

Inline styling elements that must be used inside `<text>`:

### Span

Inline styled text:

```tsx
<text>
  Normal text with <span fg="red">red text</span> inline
</text>
```

### Bold/Strong

```tsx
<text>
  <strong>Bold text</strong>
  <b>Also bold</b>
</text>
```

### Italic/Emphasis

```tsx
<text>
  <em>Italic text</em>
  <i>Also italic</i>
</text>
```

### Underline

```tsx
<text>
  <u>Underlined text</u>
</text>
```

### Line Break

```tsx
<text>
  Line one
  <br />
  Line two
</text>
```

### Link

```tsx
<text>
  Visit <a href="https://example.com">our website</a>
</text>
```

### Combined Modifiers

```tsx
<text>
  <span fg="#00FF00">
    <strong>Bold green</strong>
  </span>
  and
  <span fg="#FF0000">
    <em><u>italic underlined red</u></em>
  </span>
</text>
```

## Styled Text Template (Core)

The `t` template literal for complex styling:

```typescript
import { t, bold, italic, underline, fg, bg, dim } from "@opentui/core"

const styled = t`
  ${bold("Bold")} and ${italic("italic")} text.
  ${fg("#FF0000")("Red text")} with ${bg("#0000FF")("blue background")}.
  ${dim("Dimmed")} and ${underline("underlined")}.
`

const text = new TextRenderable(renderer, {
  content: styled,
})
```

### Style Functions

| Function | Description |
|----------|-------------|
| `bold(text)` | Bold text |
| `italic(text)` | Italic text |
| `underline(text)` | Underlined text |
| `dim(text)` | Dimmed text |
| `strikethrough(text)` | Strikethrough text |
| `fg(color)(text)` | Set foreground color |
| `bg(color)(text)` | Set background color |

## ASCII Font Component

Display large ASCII art text banners.

### Basic Usage

```tsx
// React
<ascii-font text="TITLE" font="tiny" />

// Solid
<ascii_font text="TITLE" font="tiny" />

// Core
const title = new ASCIIFontRenderable(renderer, {
  id: "title",
  text: "TITLE",
  font: "tiny",
})
```

### Available Fonts

| Font | Description |
|------|-------------|
| `tiny` | Compact ASCII font |
| `block` | Block-style letters |
| `slick` | Sleek modern style |
| `shade` | Shaded 3D effect |

### Styling

```tsx
// React
<ascii-font
  text="HELLO"
  font="block"
  color="#00FF00"
/>

// Core
import { RGBA } from "@opentui/core"

const title = new ASCIIFontRenderable(renderer, {
  text: "HELLO",
  font: "block",
  color: RGBA.fromHex("#00FF00"),
})
```

### Example Output

```
Font: tiny
╭─╮╭─╮╭─╮╭╮╭╮╭─╮╶╮╶ ╶╮
│ ││─┘├┤ │╰╯││  │  │
╰─╯╵  ╰─╯╵  ╵╰─╯╶╯╶╰─╯

Font: block
█▀▀█ █▀▀█ █▀▀ █▀▀▄
█  █ █▀▀▀ █▀▀ █  █
▀▀▀▀ ▀    ▀▀▀ ▀  ▀
```

## QR Code Component

Render a QR code in the terminal. Ships as a **separate package**,
`@opentui/qrcode` (not part of `@opentui/core`).

```bash
bun add @opentui/qrcode
```

```typescript
// Core
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 and Solid require explicit element registration (the elements are not
built in):

```tsx
// React — element <qr-code>
import { registerQRCode } from "@opentui/qrcode/react"
registerQRCode()
<qr-code content="https://opentui.com" quietZone={4} scale={2} />

// Solid — element <qr_code> (underscore)
import { registerQRCode } from "@opentui/qrcode/solid"
registerQRCode()
<qr_code content="https://opentui.com" quietZone={4} scale={2} />
```

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| `content` | `string` | `""` | Text/URL to encode |
| `errorCorrectionLevel` | `ErrorCorrectionLevel` | `M` | `.L` / `.M` / `.Q` / `.H` |
| `quietZone` | `number` | `4` | Must be ≥ 4 (throws otherwise) |
| `scale` | `number` | `1` | Columns per module before fitting |
| `fit` | `"contain" \| "none"` | `"contain"` | `contain` shrinks to parent |
| `foregroundColor` | `ColorInput` | `"#000000"` | Dark module color |
| `backgroundColor` | `ColorInput` | `"#ffffff"` | Light module / quiet-zone color |
| `fallbackContent` | `string` | `""` | Shown when too small to render |
| `fallbackColor` | `ColorInput` | `"#ffffff"` | Fallback text color |

Import `ErrorCorrectionLevel` from `@opentui/qrcode`, e.g.
`errorCorrectionLevel: ErrorCorrectionLevel.H`. Read-only getters: `version`,
`moduleCount`.

## Colors

### Color Formats

```tsx
// Hex colors
<text fg="#FF0000">Red</text>
<text fg="#F00">Short hex</text>

// Named colors
<text fg="red">Red</text>
<text fg="blue">Blue</text>

// Transparent
<text bg="transparent">No background</text>
```

### RGBA Class

Color props accept string formats (`"#FF0000"`, `"#F00"`, `"red"`, `"transparent"`)
in all frameworks. For programmatic color manipulation, the `RGBA` class from
`@opentui/core` (`fromHex` / `fromInts` / `fromValues` / `parseColor`) works in
Core, React, and Solid alike:

```tsx
import { RGBA } from "@opentui/core"

<box backgroundColor={RGBA.fromHex("#1a1a2e")} borderColor={RGBA.fromInts(122, 162, 247, 255)}>
  <text fg={RGBA.fromHex("#c0caf5")}>Styled with RGBA</text>
</box>
```

See **[core/api.md → Colors (RGBA)](../core/api.md#colors-rgba)** for the full
constructor reference and the "when to use each method" guidance.

## Text Wrapping

Text wraps based on parent container:

```tsx
<box width={40}>
  <text>
    This long text will wrap when it reaches the edge of the 
    40-character wide parent container.
  </text>
</box>
```

## Dynamic Content

### React

```tsx
function Counter() {
  const [count, setCount] = useState(0)
  return <text>Count: {count}</text>
}
```

### Solid

```tsx
function Counter() {
  const [count, setCount] = createSignal(0)
  return <text>Count: {count()}</text>
}
```

### Core

```typescript
const text = new TextRenderable(renderer, {
  id: "counter",
  content: "Count: 0",
})

// Update later
text.setContent("Count: 1")
```

## Gotchas

### Text Modifiers Outside Text

```tsx
// WRONG - modifiers only work inside <text>
<box>
  <strong>Won't work</strong>
</box>

// CORRECT
<box>
  <text>
    <strong>This works</strong>
  </text>
</box>
```

### Empty Text

```tsx
// May cause layout issues
<text></text>

// Better - use space or conditional
<text>{content || " "}</text>
```

### Color Format

```tsx
// WRONG
<text fg="FF0000">Missing #</text>

// CORRECT
<text fg="#FF0000">With #</text>
```
references/core/api.md
# Core API Reference

## Renderer

### createCliRenderer(config?)

Creates and initializes the CLI renderer.

```typescript
import { createCliRenderer, type CliRendererConfig } from "@opentui/core"

const renderer = await createCliRenderer({
  targetFPS: 60,              // Target frames per second
  exitOnCtrlC: true,          // Exit process on Ctrl+C
  consoleOptions: {           // Debug console overlay
    position: ConsolePosition.BOTTOM,
    sizePercent: 30,
    startInDebugMode: false,
  },
  onDestroy: () => {},        // Cleanup callback
})
```

#### Custom stdin/stdout (SSH, PTY, xterm.js)

`CliRendererConfig` accepts custom streams so the renderer can drive a transport
other than the local terminal. When `stdout` is not `process.stdout`, native
frame bytes are routed through an internal `NativeSpanFeed`.

```typescript
const renderer = await createCliRenderer({
  stdin,                      // NodeJS.ReadStream (default: process.stdin)
  stdout,                     // NodeJS.WriteStream (default: process.stdout)
  width: cols,                // Fallback columns for non-TTY / custom stdout
  height: rows,               // Fallback rows for non-TTY / custom stdout
  remote: true,               // Treat output as a remote terminal (auto-detects SSH/mosh for process.stdout)
  exitOnCtrlC: false,
})

// SIGWINCH is only auto-registered for process.stdout — call resize() manually
// when an external terminal reports a new size:
renderer.resize(newCols, newRows)

// Each stdin/stdout object may be owned by one renderer at a time. destroy()
// releases ownership and restores stdout.write. Allow a microtask to flush
// feed-backed bytes before closing the transport:
renderer.destroy()
await new Promise<void>((resolve) => queueMicrotask(resolve))
```

Size resolution order: `stdout.columns/rows` → `config.width/height` → `80x24`.
Env overrides: `OTUI_OVERRIDE_STDOUT` (force stdout routing),
`OTUI_USE_ALTERNATE_SCREEN`.

### CliRenderer Instance

```typescript
renderer.root              // Root renderable node
renderer.width             // Terminal width in columns
renderer.height            // Terminal height in rows
renderer.keyInput          // Keyboard event emitter
renderer.console           // Console overlay controller

renderer.start()           // Start render loop
renderer.stop()            // Stop render loop
renderer.destroy()         // Cleanup and exit alternate screen
renderer.requestRender()   // Request a re-render

renderer.setCursorStyle(options)  // Set cursor style
renderer.setCursorColor(color)    // Set cursor color
renderer.setMousePointer(style)   // Set mouse pointer shape
```

### Cursor & Mouse Pointer

```typescript
import { type CursorStyleOptions, type MousePointerStyle } from "@opentui/core"

// Set cursor style (options object)
renderer.setCursorStyle({
  style: "block",           // "block" | "line" | "underline" | "default"
  blinking: true,           // Cursor blink
  color: RGBA.fromHex("#FF0000"),  // Cursor color
  cursor: "pointer",        // Mouse pointer shape
})

// Set mouse pointer shape (OSC 22)
renderer.setMousePointer("pointer")
// Available: "default" | "pointer" | "text" | "crosshair" | "move" | "not-allowed"
```

### Renderer Events

```typescript
renderer.on("resize", (width, height) => {})     // Terminal resized
renderer.on("focus", () => {})                    // Terminal window gained focus
renderer.on("blur", () => {})                     // Terminal window lost focus
renderer.on("theme_mode", (mode) => {})           // "dark" | "light"
renderer.on("capabilities", (caps) => {})         // Terminal capabilities detected
renderer.on("selection", (selection) => {})       // Text selection finished (mouse-up)
renderer.on("destroy", () => {})                  // Renderer destroyed
renderer.on("memory:snapshot", (snapshot) => {})  // Memory snapshot
renderer.on("debugOverlay:toggle", () => {})      // Debug overlay toggled
renderer.on("frame", ({ frameId }) => {})         // A frame was committed
renderer.on("focused_renderable", (current, previous) => {})  // Focus moved
```

### Scheduler & Idle

```typescript
await renderer.idle()             // Resolves when no render pass/scheduled render is pending
renderer.getSchedulerState()      // { isRunning, isRendering, hasScheduledRender }
renderer.resize(width, height)    // Apply an external terminal resize
```

### Desktop Notifications (OSC)

Send a terminal notification via OSC 9 / 777 / 99. Returns `true` only when a
supported protocol was detected.

```typescript
if (renderer.capabilities?.notifications) {
  renderer.triggerNotification("Tests passed", "CI")  // (message, title?)
}
```

tmux requires `set -g allow-passthrough on`; Zellij uses OSC 99. Env overrides:
`OPENTUI_NOTIFICATION_PROTOCOL` (`osc9`/`osc777`/`osc99`/`none`),
`OPENTUI_NOTIFICATIONS=0`.

### Audio

Native audio engine exported from `@opentui/core`.

```typescript
import { Audio } from "@opentui/core"

const audio = Audio.create({ autoStart: false })  // or setupAudio(options?)
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()
```

Key methods: `start()`, `stop()`, `loadSound(data)`, `loadSoundFile(path)`,
`play(sound, options?)`, `stopVoice(voice)`, `group(name)`, `setGroupVolume()`,
`setMasterVolume()`, `listPlaybackDevices()`, `getStats()`, `dispose()`.
`AudioPlayOptions`: `{ volume?, pan?, loop?, groupId? }` (32 voice slots).

### Console Overlay

```typescript
renderer.console.show()    // Show console overlay
renderer.console.hide()    // Hide console overlay
renderer.console.toggle()  // Toggle visibility/focus
renderer.console.clear()   // Clear console contents
```

## Renderables

All renderables extend the base `Renderable` class and share common properties.

### Common Properties

```typescript
interface CommonProps {
  id?: string                    // Unique identifier
  
  // Positioning
  position?: "relative" | "absolute"
  left?: number | string
  top?: number | string
  right?: number | string
  bottom?: number | string
  
  // Dimensions
  width?: number | string | "auto"
  height?: number | string | "auto"
  minWidth?: number
  minHeight?: number
  maxWidth?: number
  maxHeight?: number
  
  // Flexbox
  flexDirection?: "row" | "column" | "row-reverse" | "column-reverse"
  flexGrow?: number
  flexShrink?: number
  flexBasis?: number | string
  flexWrap?: "nowrap" | "wrap" | "wrap-reverse"
  justifyContent?: "flex-start" | "flex-end" | "center" | "space-between" | "space-around" | "space-evenly"
  alignItems?: "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
  alignSelf?: "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
  alignContent?: "flex-start" | "flex-end" | "center" | "stretch" | "space-between" | "space-around"
  
  // Spacing
  padding?: number
  paddingTop?: number
  paddingRight?: number
  paddingBottom?: number
  paddingLeft?: number
  margin?: number
  marginTop?: number
  marginRight?: number
  marginBottom?: number
  marginLeft?: number
  gap?: number
  
  // Display
  display?: "flex" | "none"
  overflow?: "visible" | "hidden" | "scroll"
  zIndex?: number
}
```

### Renderable Methods

```typescript
renderable.add(child)              // Add child renderable
renderable.remove(child)           // Remove child renderable
renderable.getRenderable(id)       // Find child by ID
renderable.focus()                 // Focus this renderable
renderable.blur()                  // Remove focus
renderable.destroy()               // Destroy and cleanup

renderable.on(event, handler)      // Add event listener
renderable.off(event, handler)     // Remove event listener
renderable.emit(event, ...args)    // Emit event
```

### TextRenderable

Display styled text content.

```typescript
import { TextRenderable, TextAttributes, t, bold, fg, underline } from "@opentui/core"

const text = new TextRenderable(renderer, {
  id: "text",
  content: "Hello World",
  fg: "#FFFFFF",                   // Foreground color
  bg: "#000000",                   // Background color
  attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,
  selectable: true,                // Allow text selection
})

// Styled text with template literals
const styled = new TextRenderable(renderer, {
  content: t`${bold("Bold")} and ${fg("#FF0000")(underline("red underlined"))}`,
})
```

**TextAttributes flags:**
- `TextAttributes.BOLD`
- `TextAttributes.DIM`
- `TextAttributes.ITALIC`
- `TextAttributes.UNDERLINE`
- `TextAttributes.BLINK`
- `TextAttributes.INVERSE`
- `TextAttributes.HIDDEN`
- `TextAttributes.STRIKETHROUGH`

### Box, Input, Select, Tab Select, ScrollBox, ASCII Font

Every component is `new <Name>Renderable(renderer, options)`, composed with
`.add()`. **Full option props for each live in the shared
[components](../components/REFERENCE.md) references** (e.g. Box titles →
[containers.md](../components/containers.md); `minLength` /
`showSelectionIndicator` → [inputs.md](../components/inputs.md)). This section
covers only the **Core-specific** surface: imperative composition and event
enums.

```typescript
import { BoxRenderable, TextRenderable } from "@opentui/core"

const box = new BoxRenderable(renderer, { id: "box", border: true, title: "Panel" })
box.add(new TextRenderable(renderer, { content: "Hello" }))  // Compose imperatively
box.focus()                                                   // Focusable boxes only
```

**Events (Core uses enums; React/Solid use `onChange`/`onSelect` props):**

```typescript
import {
  InputRenderableEvents,
  SelectRenderableEvents,
  TabSelectRenderableEvents,
} from "@opentui/core"

input.on(InputRenderableEvents.CHANGE, (value: string) => {})

// ITEM_SELECTED = Enter (confirm selection); SELECTION_CHANGED = arrow keys (browse)
select.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {})
select.on(SelectRenderableEvents.SELECTION_CHANGED, (index, option) => {})
tabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index, option) => {})
```

The `ITEM_SELECTED` / `SELECTION_CHANGED` distinction is identical for Select and
Tab Select. Inputs must be focused to receive keys (`input.focus()`).

### FrameBufferRenderable

Low-level 2D rendering surface.

```typescript
import { FrameBufferRenderable, RGBA } from "@opentui/core"

const canvas = new FrameBufferRenderable(renderer, {
  id: "canvas",
  width: 50,
  height: 20,
})

// Direct pixel manipulation
canvas.frameBuffer.fillRect(10, 5, 20, 8, RGBA.fromHex("#FF0000"))
canvas.frameBuffer.drawText("Custom", 12, 7, RGBA.fromHex("#FFFFFF"))
canvas.frameBuffer.setCell(x, y, char, fg, bg)
```

## Constructs (VNode API)

Declarative wrappers that create VNodes instead of direct instances.

```typescript
import { Text, Box, Input, Select, instantiate, delegate } from "@opentui/core"

// Create VNode tree
const ui = Box(
  { border: true, padding: 1 },
  Text({ content: "Hello" }),
  Input({ placeholder: "Type here..." }),
)

// Instantiate onto renderer
renderer.root.add(ui)

// Delegate focus to nested element
const form = delegate(
  { focus: "email-input" },
  Box(
    {},
    Text({ content: "Email:" }),
    Input({ id: "email-input", placeholder: "you@example.com" }),
  ),
)
form.focus()  // Focuses the input, not the box
```

## Colors (RGBA)

The `RGBA` class is exported from `@opentui/core` but works across **all frameworks** (Core, React, Solid). Use it for programmatic color manipulation.

### Creating Colors

```typescript
import { RGBA, parseColor } from "@opentui/core"

// From hex string (most common)
RGBA.fromHex("#FF0000")           // Full hex
RGBA.fromHex("#F00")              // Short hex

// From integers (0-255 range)
RGBA.fromInts(255, 0, 0, 255)     // r, g, b, a - fully opaque red
RGBA.fromInts(255, 0, 0, 128)     // 50% transparent red
RGBA.fromInts(0, 0, 0, 0)         // Fully transparent

// From normalized floats (0.0-1.0 range)
RGBA.fromValues(1.0, 0.0, 0.0, 1.0)   // Fully opaque red
RGBA.fromValues(0.1, 0.1, 0.1, 0.7)   // Dark gray, 70% opaque
RGBA.fromValues(0.0, 0.5, 1.0, 1.0)   // Light blue
```

### Common Color Patterns

```typescript
// Theme colors
const primary = RGBA.fromHex("#7aa2f7")      // Tokyo Night blue
const background = RGBA.fromHex("#1a1a2e")
const foreground = RGBA.fromHex("#c0caf5")
const error = RGBA.fromHex("#f7768e")

// Overlays and shadows
const modalOverlay = RGBA.fromValues(0.0, 0.0, 0.0, 0.5)  // 50% black
const shadow = RGBA.fromInts(0, 0, 0, 77)                  // 30% black

// Borders
const activeBorder = RGBA.fromHex("#7aa2f7")
const inactiveBorder = RGBA.fromInts(65, 72, 104, 255)
```

### parseColor Utility

```typescript
// Accepts multiple formats
parseColor("#FF0000")             // Hex string
parseColor("red")                 // CSS color name
parseColor("transparent")         // Special values
parseColor(RGBA.fromHex("#F00"))  // Pass-through RGBA objects
```

### When to Use Each Method

| Method | Use When |
|--------|----------|
| `fromHex()` | Working with design specs, CSS colors, config files |
| `fromInts()` | You have 8-bit values (0-255), common in graphics |
| `fromValues()` | Doing color interpolation, animations, math |
| `parseColor()` | Accepting user input or config that could be any format |

### Using RGBA in React/Solid

```tsx
// Import from @opentui/core, use in any framework
import { RGBA } from "@opentui/core"

// React or Solid component
function ThemedBox() {
  const bg = RGBA.fromHex("#1a1a2e")
  const border = RGBA.fromInts(122, 162, 247, 255)
  
  return (
    <box backgroundColor={bg} borderColor={border} border>
      <text fg={RGBA.fromHex("#c0caf5")}>Works everywhere!</text>
    </box>
  )
}
```

Color props in React/Solid accept both string formats (`"#FF0000"`, `"red"`) and `RGBA` objects.

## Keyboard Input

```typescript
import { type KeyEvent } from "@opentui/core"

renderer.keyInput.on("keypress", (key: KeyEvent) => {
  console.log(key.name)           // "a", "escape", "f1", etc.
  console.log(key.sequence)       // Raw escape sequence
  console.log(key.ctrl)           // Ctrl held
  console.log(key.shift)          // Shift held
  console.log(key.meta)           // Alt held
  console.log(key.option)         // Option held (macOS)
  console.log(key.eventType)      // "press" | "release" | "repeat"
})

renderer.keyInput.on("paste", (event: PasteEvent) => {
  const text = decodePasteBytes(event.bytes)
  console.log("Pasted:", text)
})
```

## Animation Timeline

```typescript
import { Timeline, engine } from "@opentui/core"

const timeline = new Timeline({
  duration: 2000,
  loop: false,
  autoplay: true,
})

timeline.add(
  { width: 0 },
  {
    width: 50,
    duration: 1000,
    ease: "easeOutQuad",
    onUpdate: (anim) => {
      box.setWidth(anim.targets[0].width)
    },
  },
)

engine.attach(renderer)
engine.addTimeline(timeline)
```

## Type Exports

```typescript
import type {
  CliRenderer,
  CliRendererConfig,
  RenderContext,
  KeyEvent,
  Renderable,
  // ... and more
} from "@opentui/core"
```
references/core/configuration.md
# Core Configuration

## Renderer Configuration

### createCliRenderer Options

```typescript
import { createCliRenderer, ConsolePosition } from "@opentui/core"

const renderer = await createCliRenderer({
  // Rendering
  targetFPS: 60,                    // Target frames per second (default: 60)
  
  // Behavior
  exitOnCtrlC: true,                // Exit on Ctrl+C (default: true)
  
  // Console overlay
  consoleOptions: {
    position: ConsolePosition.BOTTOM,  // BOTTOM | TOP | LEFT | RIGHT
    sizePercent: 30,                   // Percentage of screen
    colorInfo: "#00FFFF",
    colorWarn: "#FFFF00",
    colorError: "#FF0000",
    colorDebug: "#888888",
    startInDebugMode: false,
  },
  
  // Lifecycle
  onDestroy: () => {
    // Cleanup callback
  },
})
```

## Environment Variables

OpenTUI respects several environment variables for configuration and debugging.

### Debug & Development

| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `OTUI_DEBUG` | boolean | false | Enable debug mode, capture raw input |
| `OTUI_DEBUG_FFI` | boolean | false | Debug logging for FFI bindings |
| `OTUI_TRACE_FFI` | boolean | false | Tracing for FFI bindings |
| `OTUI_SHOW_STATS` | boolean | false | Show debug overlay at startup |
| `OTUI_DUMP_CAPTURES` | boolean | false | Dump captured output on exit |

### Console

| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `OTUI_USE_CONSOLE` | boolean | true | Enable console capture |
| `SHOW_CONSOLE` | boolean | false | Show console at startup |

### Rendering

| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `OTUI_NO_NATIVE_RENDER` | boolean | false | Disable ANSI output (for debugging) |
| `OTUI_USE_ALTERNATE_SCREEN` | boolean | true | Use alternate screen buffer |
| `OTUI_OVERRIDE_STDOUT` | boolean | true | Override stdout stream |

### Terminal Capabilities

| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `OPENTUI_NO_GRAPHICS` | boolean | false | Disable Kitty graphics protocol |
| `OPENTUI_FORCE_UNICODE` | boolean | false | Force Mode 2026 Unicode support |
| `OPENTUI_FORCE_WCWIDTH` | boolean | false | Use wcwidth for character width |
| `OPENTUI_FORCE_NOZWJ` | boolean | false | Disable ZWJ emoji joining |
| `OPENTUI_FORCE_EXPLICIT_WIDTH` | string | - | Force explicit width ("true"/"false") |

### Tree-sitter (Syntax Highlighting)

| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `OTUI_TS_STYLE_WARN` | boolean | false | Warn on missing syntax styles |
| `OTUI_TREE_SITTER_WORKER_PATH` | string | "" | Custom tree-sitter worker path |

### XDG Paths

| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `XDG_CONFIG_HOME` | string | "" | User config directory |
| `XDG_DATA_HOME` | string | "" | User data directory |

## Usage Examples

### Development Mode

```bash
# Show debug overlay and console
OTUI_SHOW_STATS=true SHOW_CONSOLE=true bun run src/index.ts

# Debug FFI issues
OTUI_DEBUG_FFI=true OTUI_TRACE_FFI=true bun run src/index.ts

# Disable native rendering for testing
OTUI_NO_NATIVE_RENDER=true bun run src/index.ts
```

### Terminal Compatibility

```bash
# Force wcwidth for problematic terminals
OPENTUI_FORCE_WCWIDTH=true bun run src/index.ts

# Disable graphics for SSH sessions
OPENTUI_NO_GRAPHICS=true bun run src/index.ts
```

## Project Setup

### package.json

```json
{
  "name": "my-tui-app",
  "type": "module",
  "scripts": {
    "start": "bun run src/index.ts",
    "dev": "bun --watch run src/index.ts",
    "test": "bun test"
  },
  "dependencies": {
    "@opentui/core": "latest"
  },
  "devDependencies": {
    "@types/bun": "latest",
    "typescript": "latest"
  }
}
```

### tsconfig.json

```json
{
  "compilerOptions": {
    "lib": ["ESNext"],
    "target": "ESNext",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["bun-types"]
  },
  "include": ["src/**/*"]
}
```

> **Note**: OpenTUI uses `NodeNext` module resolution. All internal imports use `.js` extensions. If you use `bundler` resolution, imports still work but `NodeNext` is recommended for compatibility.

## Building Native Code

Native code changes require rebuilding:

```bash
# From repo root (if developing OpenTUI itself)
bun run build

# Zig is required for native compilation
# Install: https://ziglang.org/learn/getting-started/
```

**Note**: TypeScript changes do NOT require building. Bun runs TypeScript directly.
references/core/gotchas.md
# Core Gotchas

## Runtime Environment

### Bun (reference runtime) or Node.js 26.3.0

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

```bash
# 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.3.0** 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:

```typescript
// 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.).

```typescript
// 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:

```typescript
// 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:**
   ```typescript
   const renderer = await createCliRenderer()
   renderer.console.show()
   console.log("This appears in the overlay")
   ```

2. **Toggle with keyboard:**
   ```typescript
   renderer.keyInput.on("keypress", (key) => {
     if (key.name === "f12") {
       renderer.console.toggle()
     }
   })
   ```

3. **Write to a file:**
   ```typescript
   import { appendFileSync } from "node:fs"
   function debugLog(msg: string) {
     appendFileSync("debug.log", `${new Date().toISOString()} ${msg}\n`)
   }
   ```

4. **Disable console capture:**
   ```bash
   OTUI_USE_CONSOLE=false bun run src/index.ts
   ```

### Reproduce Issues in Tests

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

```typescript
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:

```typescript
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:

```typescript
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:

```bash
# 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

```bash
# 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:

```typescript
// 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:

```typescript
// After changing layout properties
box.setWidth(newWidth)
renderer.requestRender()
```

### Text Overflow/Clipping

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

```typescript
// 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:

```typescript
// 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:

```typescript
// 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:

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

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

### Use display: none

Hide elements instead of removing/re-adding:

```typescript
// For toggling visibility
element.setDisplay("none")   // Hidden
element.setDisplay("flex")   // Visible

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

## Testing

### Test Runner

Use Bun's test runner:

```typescript
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:

```bash
# CORRECT
cd packages/core
bun test

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

### Filter Tests

```bash
# Bun test filter
bun test --filter "component name"

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

## Keyboard Handling

### Key Names

Common key names for `KeyEvent.name`:

```typescript
// 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

```typescript
renderer.keyInput.on("keypress", (key) => {
  // eventType: "press" | "release" | "repeat"
  if (key.eventType === "repeat") {
    // Key being held down
  }
})
```
references/core/patterns.md
# Core Patterns

## Composition Patterns

### Imperative Composition

Create renderables and compose with `.add()`:

```typescript
import { createCliRenderer, BoxRenderable, TextRenderable } from "@opentui/core"

const renderer = await createCliRenderer()

// Create parent
const container = new BoxRenderable(renderer, {
  id: "container",
  flexDirection: "column",
  padding: 1,
})

// Create children
const header = new TextRenderable(renderer, {
  id: "header",
  content: "Header",
  fg: "#00FF00",
})

const body = new TextRenderable(renderer, {
  id: "body",
  content: "Body content",
})

// Compose tree
container.add(header)
container.add(body)
renderer.root.add(container)
```

### Declarative Composition (Constructs)

Use VNode functions for cleaner composition:

```typescript
import { createCliRenderer, Box, Text, Input, delegate } from "@opentui/core"

const renderer = await createCliRenderer()

// Compose as function calls
const ui = Box(
  { flexDirection: "column", padding: 1 },
  Text({ content: "Header", fg: "#00FF00" }),
  Box(
    { flexDirection: "row", gap: 2 },
    Text({ content: "Name:" }),
    Input({ id: "name", placeholder: "Enter name..." }),
  ),
)

renderer.root.add(ui)
```

### Reusable Components

Create factory functions for reusable UI pieces:

```typescript
// Imperative factory
function createLabeledInput(
  renderer: RenderContext,
  props: { id: string; label: string; placeholder: string }
) {
  const container = new BoxRenderable(renderer, {
    id: `${props.id}-container`,
    flexDirection: "row",
    gap: 1,
  })
  
  container.add(new TextRenderable(renderer, {
    id: `${props.id}-label`,
    content: props.label,
  }))
  
  container.add(new InputRenderable(renderer, {
    id: `${props.id}-input`,
    placeholder: props.placeholder,
    width: 20,
  }))
  
  return container
}

// Declarative factory
function LabeledInput(props: { id: string; label: string; placeholder: string }) {
  return delegate(
    { focus: `${props.id}-input` },
    Box(
      { flexDirection: "row", gap: 1 },
      Text({ content: props.label }),
      Input({
        id: `${props.id}-input`,
        placeholder: props.placeholder,
        width: 20,
      }),
    ),
  )
}
```

### Focus Delegation

Route focus calls to nested elements:

```typescript
import { delegate, Box, Input, Text } from "@opentui/core"

const form = delegate(
  {
    focus: "email-input",     // Route .focus() to this child
    blur: "email-input",      // Route .blur() to this child
  },
  Box(
    { border: true, padding: 1 },
    Text({ content: "Email:" }),
    Input({ id: "email-input", placeholder: "you@example.com" }),
  ),
)

// This focuses the input inside, not the box
form.focus()
```

## Event Handling

### Keyboard Events

```typescript
const renderer = await createCliRenderer()

// Global keyboard handler
renderer.keyInput.on("keypress", (key) => {
  if (key.name === "escape") {
    renderer.destroy()
    process.exit(0)
  }
  
  if (key.ctrl && key.name === "c") {
    // Ctrl+C handling (if exitOnCtrlC is false)
  }
  
  if (key.name === "tab") {
    // Tab navigation
    focusNext()
  }
})

// Paste events
renderer.keyInput.on("paste", (event) => {
  const text = decodePasteBytes(event.bytes)
  currentInput?.setValue(currentInput.value + text)
})
```

### Component Events

```typescript
import { InputRenderable, InputRenderableEvents } from "@opentui/core"

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

input.on(InputRenderableEvents.CHANGE, (value) => {
  performSearch(value)
})

// Select events
const select = new SelectRenderable(renderer, {
  id: "menu",
  options: [...],
})

select.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {
  handleSelection(option)
})

select.on(SelectRenderableEvents.SELECTION_CHANGED, (index, option) => {
  showPreview(option)
})
```

### Mouse Events

```typescript
const button = new BoxRenderable(renderer, {
  id: "button",
  border: true,
  onMouseDown: (event) => {
    button.setBackgroundColor("#444444")
  },
  onMouseUp: (event) => {
    button.setBackgroundColor("#222222")
    handleClick()
  },
  onMouseMove: (event) => {
    // Hover effect
  },
})
```

## State Management

### Local State

Manage state in closures or objects:

```typescript
// Closure-based state
function createCounter(renderer: RenderContext) {
  let count = 0
  
  const display = new TextRenderable(renderer, {
    id: "count",
    content: `Count: ${count}`,
  })
  
  const increment = () => {
    count++
    display.setContent(`Count: ${count}`)
  }
  
  return { display, increment }
}

// Class-based state
class CounterWidget {
  private count = 0
  private display: TextRenderable
  
  constructor(renderer: RenderContext) {
    this.display = new TextRenderable(renderer, {
      id: "count",
      content: this.formatCount(),
    })
  }
  
  private formatCount() {
    return `Count: ${this.count}`
  }
  
  increment() {
    this.count++
    this.display.setContent(this.formatCount())
  }
  
  getRenderable() {
    return this.display
  }
}
```

### Focus Management

Track and manage focus across components:

```typescript
class FocusManager {
  private focusables: Renderable[] = []
  private currentIndex = 0
  
  register(renderable: Renderable) {
    this.focusables.push(renderable)
  }
  
  focusNext() {
    this.focusables[this.currentIndex]?.blur()
    this.currentIndex = (this.currentIndex + 1) % this.focusables.length
    this.focusables[this.currentIndex]?.focus()
  }
  
  focusPrevious() {
    this.focusables[this.currentIndex]?.blur()
    this.currentIndex = (this.currentIndex - 1 + this.focusables.length) % this.focusables.length
    this.focusables[this.currentIndex]?.focus()
  }
}

// Usage
const focusManager = new FocusManager()
focusManager.register(input1)
focusManager.register(input2)
focusManager.register(select1)

renderer.keyInput.on("keypress", (key) => {
  if (key.name === "tab") {
    key.shift ? focusManager.focusPrevious() : focusManager.focusNext()
  }
})
```

## Lifecycle Patterns

### Cleanup

Always clean up resources:

```typescript
const renderer = await createCliRenderer()

// Track intervals/timeouts
const intervals: Timer[] = []

intervals.push(setInterval(() => {
  updateClock()
}, 1000))

// Cleanup on exit
process.on("SIGINT", () => {
  intervals.forEach(clearInterval)
  renderer.destroy()
  process.exit(0)
})

// Or use onDestroy callback
const renderer = await createCliRenderer({
  onDestroy: () => {
    intervals.forEach(clearInterval)
  },
})
```

### Dynamic Updates

Update UI based on external data:

```typescript
async function createDashboard(renderer: RenderContext) {
  const statsText = new TextRenderable(renderer, {
    id: "stats",
    content: "Loading...",
  })
  
  // Poll for updates
  const updateStats = async () => {
    const data = await fetchStats()
    statsText.setContent(`CPU: ${data.cpu}% | Memory: ${data.memory}%`)
  }
  
  // Initial load
  await updateStats()
  
  // Periodic updates
  setInterval(updateStats, 5000)
  
  return statsText
}
```

## Layout Patterns

### Responsive Layout

Adapt to terminal size:

```typescript
const renderer = await createCliRenderer()

const mainPanel = new BoxRenderable(renderer, {
  id: "main",
  width: "100%",
  height: "100%",
  flexDirection: renderer.width > 80 ? "row" : "column",
})

// Listen for resize
process.stdout.on("resize", () => {
  mainPanel.setFlexDirection(renderer.width > 80 ? "row" : "column")
})
```

### Split Panels

```typescript
function createSplitView(renderer: RenderContext, ratio = 0.3) {
  const container = new BoxRenderable(renderer, {
    id: "split",
    flexDirection: "row",
    width: "100%",
    height: "100%",
  })
  
  const left = new BoxRenderable(renderer, {
    id: "left",
    width: `${ratio * 100}%`,
    border: true,
  })
  
  const right = new BoxRenderable(renderer, {
    id: "right",
    flexGrow: 1,
    border: true,
  })
  
  container.add(left)
  container.add(right)
  
  return { container, left, right }
}
```

## Debugging Patterns

### Console Overlay

Use the built-in console for debugging:

```typescript
const renderer = await createCliRenderer({
  consoleOptions: {
    startInDebugMode: true,
  },
})

// Show console
renderer.console.show()

// All console methods work
console.log("Debug info")
console.warn("Warning")
console.error("Error")

// Toggle with keyboard
renderer.keyInput.on("keypress", (key) => {
  if (key.name === "f12") {
    renderer.console.toggle()
  }
})
```

### State Inspection

```typescript
function debugState(label: string, state: unknown) {
  console.log(`[${label}]`, JSON.stringify(state, null, 2))
}

// In your update logic
debugState("form", { name: nameInput.value, email: emailInput.value })
```
references/core/REFERENCE.md
# OpenTUI Core (@opentui/core)

The foundational library for building terminal user interfaces. Provides an imperative API with all primitives, giving you maximum control over rendering, state, and behavior.

## Overview

OpenTUI Core runs on Bun with native Zig bindings for performance-critical operations:
- **Renderer**: Manages terminal output, input events, and the rendering loop
- **Renderables**: Hierarchical UI building blocks with Yoga layout
- **Constructs**: Declarative wrappers for composing Renderables
- **FrameBuffer**: Low-level 2D rendering surface for custom graphics

## When to Use Core

Use the core imperative API when:
- Building a library or framework on top of OpenTUI
- Need maximum control over rendering and state
- Want smallest possible bundle size (no React/Solid runtime)
- Building performance-critical applications
- Integrating with existing imperative codebases

## When NOT to Use Core

| Scenario | Use Instead |
|----------|-------------|
| Familiar with React patterns | `@opentui/react` |
| Want fine-grained reactivity | `@opentui/solid` |
| Building typical applications | React or Solid reconciler |
| Rapid prototyping | React or Solid reconciler |

## Quick Start

### Using create-tui (Recommended)

```bash
bunx create-tui@latest -t core my-app
cd my-app
bun run src/index.ts
```

The CLI creates the `my-app` directory for you - it must **not already exist**.

**Agent guidance**: Always use autonomous mode with `-t <template>` flag. Never use interactive mode (`bunx create-tui@latest my-app` without `-t`) as it requires user prompts that agents cannot respond to.

### Manual Setup

```bash
mkdir my-tui && cd my-tui
bun init
bun install @opentui/core
```

```typescript
import { createCliRenderer, TextRenderable, BoxRenderable } from "@opentui/core"

const renderer = await createCliRenderer()

// Create a box container
const container = new BoxRenderable(renderer, {
  id: "container",
  width: 40,
  height: 10,
  border: true,
  borderStyle: "rounded",
  padding: 1,
})

// Create text inside the box
const greeting = new TextRenderable(renderer, {
  id: "greeting",
  content: "Hello, OpenTUI!",
  fg: "#00FF00",
})

// Compose the tree
container.add(greeting)
renderer.root.add(container)
```

## Core Concepts

### Renderer

The `CliRenderer` orchestrates everything:
- Manages the terminal viewport and alternate screen
- Handles input events (keyboard, mouse, paste)
- Runs the rendering loop (configurable FPS)
- Provides the root node for the renderable tree

### Renderables vs Constructs

| Renderables (Imperative) | Constructs (Declarative) |
|--------------------------|--------------------------|
| `new TextRenderable(renderer, {...})` | `Text({...})` |
| Requires renderer at creation | Creates VNode, instantiated later |
| Direct mutation via methods | Chained calls recorded, replayed on instantiation |
| Full control | Cleaner composition |

### Storage Options

Renderables can be composed in two ways:
1. **Imperative**: Create instances, call `.add()` to compose
2. **Declarative (Constructs)**: Create VNodes, pass children as arguments

## Essential Commands

```bash
bun install @opentui/core     # Install
bun run src/index.ts          # Run directly (no build needed)
bun test                      # Run tests
```

## Runtime Requirements

OpenTUI runs on **Bun (reference runtime)** and uses Zig for native builds.
**Node.js 26.3.0** is also supported for the native renderer when launched with
`--experimental-ffi`; importing core/keymap without a native renderer works on
Node without FFI. See [Gotchas](./gotchas.md) for the full Node.js notes.

```bash
# Package management
bun install @opentui/core

# Running
bun run src/index.ts
bun test

# Building (only needed for native code changes)
bun run build
```

**Zig** is required for building native components.

## Additional Capabilities

- **Audio** — native audio engine via `Audio` from `@opentui/core`. See [API](./api.md#audio).
- **Notifications** — `renderer.triggerNotification(message, title?)` (OSC 9/777/99). See [API](./api.md).
- **SSH** — serve a TUI over SSH with the `@opentui/ssh` package:

  ```typescript
  import { createServer } from "@opentui/ssh"
  import { BoxRenderable, TextRenderable } from "@opentui/core"

  const server = createServer({
    hostKey: { path: "./host_key" },  // auto-generated on first run
    auth: { publicKey: "any" },
  }).serve((session) => {
    const { renderer, identity } = session   // renderer is bound to the SSH channel
    const box = new BoxRenderable(renderer, { width: "100%", height: "100%", border: true })
    box.add(new TextRenderable(renderer, { content: `Hello, ${identity.username}!` }))
    renderer.root.add(box)
  })

  await server.listen(2222)  // ssh -p 2222 localhost
  ```

  `@opentui/core` is a peer dependency; works with core, React (`createRoot`),
  and Solid (`render`).

## In This Reference

- [Configuration](./configuration.md) - Renderer options, environment variables
- [API](./api.md) - Renderer, Renderables, types, utilities
- [Patterns](./patterns.md) - Composition, events, state management
- [Gotchas](./gotchas.md) - Common issues, debugging, limitations

## See Also

- [React](../react/REFERENCE.md) - React reconciler for declarative TUI
- [Solid](../solid/REFERENCE.md) - Solid reconciler for declarative TUI
- [Layout](../layout/REFERENCE.md) - Yoga/Flexbox layout system
- [Components](../components/REFERENCE.md) - Component reference by category
- [Keyboard](../keyboard/REFERENCE.md) - Input handling and shortcuts
- [Testing](../testing/REFERENCE.md) - Test renderer and snapshots
references/keyboard/REFERENCE.md
# Keyboard Input Handling

How to handle keyboard input in OpenTUI applications.

## Overview

OpenTUI provides keyboard input handling through:
- **Core**: `renderer.keyInput` EventEmitter
- **React**: `useKeyboard()` hook
- **Solid**: `useKeyboard()` hook

## When to Use

Use this reference when you need keyboard shortcuts, focus-aware input handling, or custom keybindings.

## KeyEvent Object

All keyboard handlers receive a `KeyEvent` object:

```typescript
interface KeyEvent {
  name: string          // Key name: "a", "escape", "f1", etc.
  sequence: string      // Raw escape sequence
  ctrl: boolean         // Ctrl modifier held
  shift: boolean        // Shift modifier held
  meta: boolean         // Alt modifier held
  option: boolean       // Option modifier held (macOS)
  eventType: "press" | "release" | "repeat"
  repeated: boolean     // Key is being held (repeat event)
}
```

## Basic Usage

### Core

```typescript
import { createCliRenderer, type KeyEvent } from "@opentui/core"

const renderer = await createCliRenderer()

renderer.keyInput.on("keypress", (key: KeyEvent) => {
  if (key.name === "escape") {
    renderer.destroy()
    return
  }
  
  if (key.ctrl && key.name === "s") {
    saveDocument()
  }
})
```

### React

```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 exit</text>
}
```


### Solid

```tsx
import { useKeyboard, useRenderer } from "@opentui/solid"

function App() {
  const renderer = useRenderer()
  useKeyboard((key) => {
    if (key.name === "escape") {
      renderer.destroy()
    }
  })
  
  return <text>Press ESC to exit</text>
}
```

## Key Names

### Alphabetic Keys

Lowercase: `a`, `b`, `c`, ... `z`

With Shift: Check `key.shift && key.name === "a"` for uppercase

### Numeric Keys

`0`, `1`, `2`, ... `9`

### Function Keys

`f1`, `f2`, `f3`, ... `f12`

### Special Keys

| Key Name | Description |
|----------|-------------|
| `escape` | Escape key |
| `enter` | Enter/Return |
| `return` | Enter/Return (alias) |
| `tab` | Tab key |
| `backspace` | Backspace |
| `delete` | Delete key |
| `space` | Spacebar |

### Arrow Keys

| Key Name | Description |
|----------|-------------|
| `up` | Up arrow |
| `down` | Down arrow |
| `left` | Left arrow |
| `right` | Right arrow |

### Navigation Keys

| Key Name | Description |
|----------|-------------|
| `home` | Home key |
| `end` | End key |
| `pageup` | Page Up |
| `pagedown` | Page Down |
| `insert` | Insert key |

## Modifier Keys

Check modifier properties on `KeyEvent`:

```typescript
renderer.keyInput.on("keypress", (key) => {
  if (key.ctrl && key.name === "c") {
    // Ctrl+C
  }
  
  if (key.shift && key.name === "tab") {
    // Shift+Tab
  }
  
  if (key.meta && key.name === "s") {
    // Alt+S (meta = Alt on most systems)
  }
  
  if (key.option && key.name === "a") {
    // Option+A (macOS)
  }
})
```

### Modifier Combinations

```typescript
// Ctrl+Shift+S
if (key.ctrl && key.shift && key.name === "s") {
  saveAs()
}

// Ctrl+Alt+Delete (careful with system shortcuts!)
if (key.ctrl && key.meta && key.name === "delete") {
  // ...
}
```

## Event Types

### Press Events (Default)

Normal key press:

```typescript
renderer.keyInput.on("keypress", (key) => {
  if (key.eventType === "press") {
    // Initial key press
  }
})
```

### Repeat Events

Key held down:

```typescript
renderer.keyInput.on("keypress", (key) => {
  if (key.eventType === "repeat" || key.repeated) {
    // Key is being held
  }
})
```

### Release Events

Key released (opt-in):

```tsx
// React
useKeyboard(
  (key) => {
    if (key.eventType === "release") {
      // Key released
    }
  },
  { release: true }  // Enable release events
)

// Solid
useKeyboard(
  (key) => {
    if (key.eventType === "release") {
      // Key released
    }
  },
  { release: true }
)
```

## Patterns

### Navigation Menu

```tsx
function Menu() {
  const [selectedIndex, setSelectedIndex] = useState(0)
  const items = ["Home", "Settings", "Help", "Quit"]
  
  useKeyboard((key) => {
    switch (key.name) {
      case "up":
      case "k":
        setSelectedIndex(i => Math.max(0, i - 1))
        break
      case "down":
      case "j":
        setSelectedIndex(i => Math.min(items.length - 1, i + 1))
        break
      case "enter":
        handleSelect(items[selectedIndex])
        break
    }
  })
  
  return (
    <box flexDirection="column">
      {items.map((item, i) => (
        <text
          key={item}
          fg={i === selectedIndex ? "#00FF00" : "#FFFFFF"}
        >
          {i === selectedIndex ? "> " : "  "}{item}
        </text>
      ))}
    </box>
  )
}
```

### Modal Escape

```tsx
function Modal({ onClose, children }) {
  useKeyboard((key) => {
    if (key.name === "escape") {
      onClose()
    }
  })
  
  return (
    <box border padding={2}>
      {children}
    </box>
  )
}
```

### Vim-style Modes

```tsx
function Editor() {
  const [mode, setMode] = useState<"normal" | "insert">("normal")
  const [content, setContent] = useState("")
  
  useKeyboard((key) => {
    if (mode === "normal") {
      switch (key.name) {
        case "i":
          setMode("insert")
          break
        case "escape":
          // Already in normal mode
          break
        case "j":
          moveCursorDown()
          break
        case "k":
          moveCursorUp()
          break
      }
    } else if (mode === "insert") {
      if (key.name === "escape") {
        setMode("normal")
      }
      // Input component handles text in insert mode
    }
  })
  
  return (
    <box flexDirection="column">
      <text>Mode: {mode}</text>
      <textarea
        value={content}
        onChange={setContent}
        focused={mode === "insert"}
      />
    </box>
  )
}
```

### Game Controls

```tsx
function Game() {
  const [pressed, setPressed] = useState(new Set<string>())
  
  useKeyboard(
    (key) => {
      setPressed(keys => {
        const newKeys = new Set(keys)
        if (key.eventType === "release") {
          newKeys.delete(key.name)
        } else {
          newKeys.add(key.name)
        }
        return newKeys
      })
    },
    { release: true }
  )
  
  // Game logic uses pressed set
  useEffect(() => {
    if (pressed.has("up") || pressed.has("w")) {
      moveUp()
    }
    if (pressed.has("down") || pressed.has("s")) {
      moveDown()
    }
  }, [pressed])
  
  return <text>WASD or arrows to move</text>
}
```

### Keyboard Shortcuts Help

```tsx
function ShortcutsHelp() {
  const shortcuts = [
    { keys: "Ctrl+S", action: "Save" },
    { keys: "Ctrl+Q", action: "Quit" },
    { keys: "Ctrl+F", action: "Find" },
    { keys: "Tab", action: "Next field" },
    { keys: "Shift+Tab", action: "Previous field" },
  ]
  
  return (
    <box border title="Keyboard Shortcuts" padding={1}>
      {shortcuts.map(({ keys, action }) => (
        <box key={keys} flexDirection="row">
          <text width={15} fg="#00FFFF">{keys}</text>
          <text>{action}</text>
        </box>
      ))}
    </box>
  )
}
```

## Paste Events

Handle pasted content. Paste events deliver raw bytes, not decoded text.

### PasteEvent Object

```typescript
import { type PasteEvent } from "@opentui/core"

interface PasteEvent {
  type: "paste"              // Always "paste"
  bytes: Uint8Array          // Raw pasted bytes
  metadata?: PasteMetadata   // Optional metadata
  preventDefault(): void     // Prevent default paste handling
  defaultPrevented: boolean  // Whether preventDefault was called
}

interface PasteMetadata {
  mimeType?: string          // MIME type if available
  kind?: PasteKind           // Paste kind
}
```

### Decoding Paste Bytes

Use `decodePasteBytes` to convert raw bytes to a string, and `stripAnsiSequences` to remove ANSI escape codes:

```typescript
import { decodePasteBytes, stripAnsiSequences } from "@opentui/core"

const text = decodePasteBytes(event.bytes)          // Decode UTF-8
const clean = stripAnsiSequences(decodePasteBytes(event.bytes))  // Decode + strip ANSI
```

### Core

```typescript
import { type PasteEvent, decodePasteBytes } from "@opentui/core"

renderer.keyInput.on("paste", (event: PasteEvent) => {
  const text = decodePasteBytes(event.bytes)
  console.log("Pasted:", text)
})
```

### Solid

Solid provides a dedicated `usePaste` hook:

```tsx
import { usePaste } from "@opentui/solid"
import { decodePasteBytes } from "@opentui/core"

function App() {
  usePaste((event) => {
    const text = decodePasteBytes(event.bytes)
    console.log("Pasted:", text)
  })
  
  return <text>Paste something</text>
}
```

> **Note**: `usePaste` is **Solid-only**. React does not have this hook - handle paste via the Core event emitter or input component's `onChange`.

## Text Selection

Text selection is renderer-managed. The renderer owns a single `Selection` object, walks the renderable tree to find selectable children, and emits a `"selection"` event when the user finishes selecting (mouse-up). The `Selection` object aggregates text from all selected renderables automatically.

### Making Renderables Selectable

A renderable must have `selectable` set to `true` to participate in selection. Text-based renderables (`TextRenderable`, `TextareaRenderable`, `ASCIIFontRenderable`, `TextTableRenderable`) support this:

```tsx
// React / Solid
<text selectable>This text can be selected</text>

// Core
const text = new TextRenderable(renderer, {
  id: "label",
  content: "This text can be selected",
  selectable: true,
})
```

### Copy-on-Selection (Core)

Listen to the renderer's `"selection"` event. The `Selection` object's `getSelectedText()` returns text aggregated from all selected renderables in reading order:

```typescript
import type { Selection } from "@opentui/core"

renderer.on("selection", (selection: Selection) => {
  const text = selection.getSelectedText()
  if (text) {
    renderer.copyToClipboardOSC52(text)
  }
})
```

> **Important**: Call `selection.getSelectedText()` on the `Selection` object from the event -- not `renderer.root.getSelectedText()`. Individual renderables only return their own selected text. The `Selection` object aggregates across the tree.

### Copy-on-Selection (Solid)

```tsx
import { useSelectionHandler } from "@opentui/solid"

function App() {
  useSelectionHandler((selection) => {
    const text = selection.getSelectedText()
    if (text) {
      renderer.copyToClipboardOSC52(text)
    }
  })

  return <text selectable>Select this text</text>
}
```

> **Note**: `useSelectionHandler` is **Solid-only**. React does not have this hook -- use the Core `renderer.on("selection", ...)` event.

### Selection Object

The `Selection` object passed to the event callback:

```typescript
selection.getSelectedText()       // Aggregated text from all selected renderables
selection.bounds                  // { startX, startY, endX, endY } bounding rect
selection.selectedRenderables     // Renderable[] with active selections
selection.isActive                // Whether selection is still active
```

Individual renderables also expose:

```typescript
renderable.hasSelection()         // Does this renderable have selected text?
renderable.getSelectedText()      // Selected text in this renderable only
```

### How Selection Traversal Works

When the user drags to select, the renderer:
1. Identifies the selection container (common ancestor of start and end points)
2. Walks all `selectable` descendants within the selection bounds
3. Calls `onSelectionChanged(selection)` on each, which computes local selection
4. Tracks which renderables have active selections in `selection.selectedRenderables`

This means selection works across multiple renderables. Dragging across two `<text selectable>` elements selects text in both, and `selection.getSelectedText()` joins them with newlines.

## Clipboard API (OSC 52)

Copy text to the system clipboard using OSC 52 escape sequences. Works over SSH and in most modern terminal emulators.

```typescript
// Copy to clipboard
const success = renderer.copyToClipboardOSC52("text to copy")

// Check if OSC 52 is supported
if (renderer.isOsc52Supported()) {
  renderer.copyToClipboardOSC52("Hello!")
}

// Clear clipboard
renderer.clearClipboardOSC52()

// Target specific clipboard (X11)
import { ClipboardTarget } from "@opentui/core"
renderer.copyToClipboardOSC52("text", ClipboardTarget.Primary)   // X11 primary
renderer.copyToClipboardOSC52("text", ClipboardTarget.Clipboard) // System clipboard (default)
```

## Focus and Input Components

Input components (`<input>`, `<textarea>`, `<select>`) capture keyboard events when focused:

```tsx
<input focused />  // Receives keyboard input

// Global useKeyboard still fires, but input consumes characters
```

To prevent conflicts, check if an input is focused before handling global shortcuts:

```tsx
function App() {
  const renderer = useRenderer()
  const [inputFocused, setInputFocused] = useState(false)
  
  useKeyboard((key) => {
    if (inputFocused) return  // Let input handle it
    
    // Global shortcuts
    if (key.name === "escape") {
      renderer.destroy()
    }
  })
  
  return (
    <input
      focused={inputFocused}
      onFocus={() => setInputFocused(true)}
      onBlur={() => setInputFocused(false)}
    />
  )
}
```

## Gotchas

### Terminal Limitations

Some key combinations are captured by the terminal or OS:
- `Ctrl+C` often sends SIGINT (use `exitOnCtrlC: false` to handle)
- `Ctrl+Z` suspends the process
- Some function keys may be intercepted

### SSH and Remote Sessions

Key detection may vary over SSH. Test on target environments.

### Multiple Handlers

Multiple `useKeyboard` calls all receive events. Coordinate handlers to prevent conflicts.

## See Also

- [React API](../react/api.md) - `useKeyboard` hook reference
- [Solid API](../solid/api.md) - `useKeyboard` hook reference
- [Input Components](../components/inputs.md) - Focus management with input, textarea, select
- [Testing](../testing/REFERENCE.md) - Simulating key presses in tests
references/keymap/REFERENCE.md
# Keymap (@opentui/keymap)

A host-agnostic key binding, command, and sequence engine for both terminal
(OpenTUI) and browser (DOM) hosts. It is a **separate package** from
`@opentui/core`, shipped as `@opentui/keymap`, and is **pure JavaScript** — it
imports in Node.js with **no Bun and no native FFI required** (only creating a
native OpenTUI renderer needs FFI).

## When to Use

Use keymap when you need declarative, layered keybindings with commands, leader
keys, multi-key sequences (`dd`, `<leader>s`), counts (`{count}j`), or ex-style
commands — instead of hand-rolling `useKeyboard`/`keyInput` handlers.

## Install

```bash
bun add @opentui/keymap
```

## Entry Points

| Import | Purpose |
|--------|---------|
| `@opentui/keymap` | Main engine: `Keymap`, key stringifiers, shared types |
| `@opentui/keymap/addons` | Universal addons (parser stages, metadata, diagnostics, sequences, ex-commands) |
| `@opentui/keymap/addons/opentui` | Universal addons + OpenTUI base-layout & edit-buffer helpers |
| `@opentui/keymap/extras` | Config/formatting helpers (`commandBindings`, `createBindingLookup`, `formatKeySequence`, `formatCommandBindings`) |
| `@opentui/keymap/extras/graph` | `getGraphSnapshot()` for debug/graph UIs |
| `@opentui/keymap/testing` | Fake host + diagnostics for addon tests |
| `@opentui/keymap/opentui` | OpenTUI terminal host adapter |
| `@opentui/keymap/html` | DOM host adapter |
| `@opentui/keymap/react` | React provider/hooks |
| `@opentui/keymap/solid` | Solid provider/hooks |

## Quick Start (Terminal)

```typescript
import { createCliRenderer } from "@opentui/core"
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"

const renderer = await createCliRenderer()
const keymap = createDefaultOpenTuiKeymap(renderer)

keymap.registerLayer({
  commands: [{ name: "quit", run() { renderer.destroy() } }],
  bindings: [{ key: "q", cmd: "quit" }],
})
```

Construction options:
- Bare: `new Keymap(host)`
- Host helpers: `createOpenTuiKeymap(renderer)` / `createHtmlKeymap(root)`
- With default addons: `createDefaultOpenTuiKeymap(renderer)` / `createDefaultHtmlKeymap(root)`

## Binding Shape

A binding has a required `key`, plus optional fields:

```typescript
{
  key: "ctrl+x",          // string ("dd", "<leader>s", "{count}j") or stroke object { name: "return", ctrl: true }
  cmd: "quit",            // command name to run
  event: "press",         // "press" (default) | "release"
  preventDefault: true,   // default true
  fallthrough: false,     // default false
  // ...custom addon fields
}
```

## Core API (Keymap instance)

- **Register** (each returns a disposer): `registerLayer()`, `registerToken()`,
  `registerSequencePattern()`, `registerLayerFields()`, `registerBindingFields()`,
  `registerCommandFields()`, plus parser/expander/transformer/resolver
  `prepend*/append*` stages.
- **Dispatch/execute**: `runCommand()`, `dispatchCommand()`,
  `intercept("key" | "key:after" | "raw", fn)`.
- **Query**: `getActiveKeys(options?)`, `getCommands()`, `getCommandEntries()`,
  `getCommandBindings()`, `getPendingSequence()`, `hasPendingSequence()`,
  `clearPendingSequence()`, `popPendingSequence()`.
- **Data/state**: `setData(name, value)`, `getData(name)`; events via
  `on("state" | "pendingSequence" | "dispatch" | "warning" | "error", fn)`.
- **Key helpers**: `parseKeySequence()`, `formatKey()`, `createKeyMatcher()`,
  `getHostMetadata()`; exported `stringifyKeyStroke()` / `stringifyKeySequence()`.

## Shipped Addons

`registerDefaultKeys()`, `registerLeader()`, `registerTimedLeader()`,
`registerModBindings()`, `registerCommaBindings()`, `registerEmacsBindings()`,
`registerExCommands()`, `registerNeovimDisambiguation()`,
`registerMetadataFields()`, `registerEnabledFields()`; OpenTUI-specific
`registerBaseLayoutFallback()`, `createTextareaBindings()`,
`registerManagedTextareaLayer()`, `registerEditBufferCommands()`.

## React

```tsx
import { KeymapProvider, useKeymap, useBindings, useActiveKeys } from "@opentui/keymap/react"

// Provide a pre-created Keymap<Renderable, KeyEvent>, then:
useBindings((keymap) => keymap.registerLayer({ /* ... */ }), [deps])
const active = useActiveKeys()
```

Exports: `KeymapProvider`, `useKeymap()`, `useBindings(createLayer, deps?)`,
`useActiveKeys(options?)`, `usePendingSequence()`, `reactiveMatcherFromStore()`.

## Solid

The Solid adapter (`@opentui/keymap/solid`) exposes an equivalent provider and
hooks that consume a pre-created `Keymap`.

## Keymap vs Keyboard

- **Keyboard** (`keyboard/REFERENCE.md`, `useKeyboard`/`keyInput`): low-level raw
  key events. Best for simple, one-off shortcuts.
- **Keymap** (this file): declarative layered bindings, commands, sequences,
  leader keys, and counts. Best for editor-style keymaps and larger apps.
references/layout/patterns.md
# Layout Patterns

Common layout recipes for terminal user interfaces.

## Full-Screen App

Fill the entire terminal:

```tsx
function App() {
  return (
    <box width="100%" height="100%">
      {/* Content fills terminal */}
    </box>
  )
}
```

## Header/Content/Footer

Classic app layout:

```tsx
function AppLayout() {
  return (
    <box flexDirection="column" width="100%" height="100%">
      {/* Header - fixed height */}
      <box height={3} borderStyle="single" borderBottom>
        <text>Header</text>
      </box>
      
      {/* Content - fills remaining space */}
      <box flexGrow={1}>
        <text>Main Content</text>
      </box>
      
      {/* Footer - fixed height */}
      <box height={1}>
        <text>Status: Ready</text>
      </box>
    </box>
  )
}
```

## Sidebar Layout

```tsx
function SidebarLayout() {
  return (
    <box flexDirection="row" width="100%" height="100%">
      {/* Sidebar - fixed width */}
      <box width={25} borderStyle="single" borderRight>
        <text>Sidebar</text>
      </box>
      
      {/* Main - fills remaining space */}
      <box flexGrow={1}>
        <text>Main Content</text>
      </box>
    </box>
  )
}
```

## Resizable Sidebar

Responsive based on terminal width:

```tsx
function ResponsiveSidebar() {
  const dims = useTerminalDimensions()  // React: useTerminalDimensions()
  const showSidebar = dims.width > 60
  const sidebarWidth = Math.min(30, Math.floor(dims.width * 0.3))
  
  return (
    <box flexDirection="row" width="100%" height="100%">
      {showSidebar && (
        <box width={sidebarWidth} border>
          <text>Sidebar</text>
        </box>
      )}
      <box flexGrow={1}>
        <text>Main</text>
      </box>
    </box>
  )
}
```

## Centered Content

### Horizontally Centered

```tsx
<box width="100%" justifyContent="center">
  <box width={40}>
    <text>Centered horizontally</text>
  </box>
</box>
```

### Vertically Centered

```tsx
<box height="100%" alignItems="center">
  <text>Centered vertically</text>
</box>
```

### Both Axes

```tsx
<box
  width="100%"
  height="100%"
  justifyContent="center"
  alignItems="center"
>
  <box width={40} height={10} border>
    <text>Centered both ways</text>
  </box>
</box>
```

## Modal/Dialog

Centered overlay:

```tsx
function Modal({ children, visible }) {
  if (!visible) return null
  
  return (
    <box
      position="absolute"
      left={0}
      top={0}
      width="100%"
      height="100%"
      justifyContent="center"
      alignItems="center"
      backgroundColor="rgba(0,0,0,0.5)"
    >
      <box
        width={50}
        height={15}
        border
        borderStyle="double"
        backgroundColor="#1a1a2e"
        padding={2}
      >
        {children}
      </box>
    </box>
  )
}
```

## Grid Layout

Using flexWrap:

```tsx
function Grid({ items, columns = 3 }) {
  const itemWidth = `${Math.floor(100 / columns)}%`
  
  return (
    <box flexDirection="row" flexWrap="wrap" width="100%">
      {items.map((item, i) => (
        <box key={i} width={itemWidth} padding={1}>
          <text>{item}</text>
        </box>
      ))}
    </box>
  )
}
```

## Split Panels

### Horizontal Split

```tsx
function HorizontalSplit({ ratio = 0.5 }) {
  return (
    <box flexDirection="row" width="100%" height="100%">
      <box width={`${ratio * 100}%`} border>
        <text>Left Panel</text>
      </box>
      <box flexGrow={1} border>
        <text>Right Panel</text>
      </box>
    </box>
  )
}
```

### Vertical Split

```tsx
function VerticalSplit({ ratio = 0.5 }) {
  return (
    <box flexDirection="column" width="100%" height="100%">
      <box height={`${ratio * 100}%`} border>
        <text>Top Panel</text>
      </box>
      <box flexGrow={1} border>
        <text>Bottom Panel</text>
      </box>
    </box>
  )
}
```

## Form Layout

Label + Input pairs:

```tsx
function FormField({ label, children }) {
  return (
    <box flexDirection="row" marginBottom={1}>
      <box width={15}>
        <text>{label}:</text>
      </box>
      <box flexGrow={1}>
        {children}
      </box>
    </box>
  )
}

function LoginForm() {
  return (
    <box flexDirection="column" padding={2} border width={50}>
      <FormField label="Username">
        <input placeholder="Enter username" />
      </FormField>
      <FormField label="Password">
        <input placeholder="Enter password" />
      </FormField>
      <box marginTop={2} justifyContent="flex-end">
        <box border padding={1}>
          <text>Login</text>
        </box>
      </box>
    </box>
  )
}
```

## Navigation Tabs

```tsx
function TabBar({ tabs, activeIndex, onSelect }) {
  return (
    <box flexDirection="row" borderBottom>
      {tabs.map((tab, i) => (
        <box
          key={i}
          padding={1}
          backgroundColor={i === activeIndex ? "#333" : "transparent"}
          onMouseDown={() => onSelect(i)}
        >
          <text fg={i === activeIndex ? "#fff" : "#888"}>
            {tab}
          </text>
        </box>
      ))}
    </box>
  )
}
```

## Sticky Footer

Footer always at bottom:

```tsx
function StickyFooterLayout() {
  return (
    <box flexDirection="column" width="100%" height="100%">
      {/* Content area */}
      <box flexGrow={1} flexDirection="column">
        {/* Your content here */}
        <text>Content that might be short</text>
      </box>
      
      {/* Footer pushed to bottom */}
      <box height={1}>
        <text fg="#888">Press ? for help | q to quit</text>
      </box>
    </box>
  )
}
```

## Absolute Positioning Overlay

Tooltip or popup:

```tsx
function Tooltip({ x, y, children }) {
  return (
    <box
      position="absolute"
      left={x}
      top={y}
      border
      backgroundColor="#333"
      padding={1}
      zIndex={100}
    >
      {children}
    </box>
  )
}
```

## Responsive Breakpoints

Different layouts based on terminal size:

```tsx
function ResponsiveApp() {
  const { width, height } = useTerminalDimensions()
  
  // Define breakpoints
  const isSmall = width < 60
  const isMedium = width >= 60 && width < 100
  const isLarge = width >= 100
  
  if (isSmall) {
    // Mobile-like: stacked layout
    return (
      <box flexDirection="column">
        <Navigation />
        <Content />
      </box>
    )
  }
  
  if (isMedium) {
    // Tablet-like: sidebar + content
    return (
      <box flexDirection="row">
        <box width={20}><Navigation /></box>
        <box flexGrow={1}><Content /></box>
      </box>
    )
  }
  
  // Large: full layout
  return (
    <box flexDirection="row">
      <box width={25}><Navigation /></box>
      <box flexGrow={1}><Content /></box>
      <box width={30}><Sidebar /></box>
    </box>
  )
}
```

## Equal Height Columns

```tsx
function EqualColumns() {
  return (
    <box flexDirection="row" alignItems="stretch" height={20}>
      <box flexGrow={1} border>
        <text>Short content</text>
      </box>
      <box flexGrow={1} border>
        <text>
          Longer content that
          spans multiple lines
          and takes up space
        </text>
      </box>
      <box flexGrow={1} border>
        <text>Medium content</text>
      </box>
    </box>
  )
}
```

## Spacing Utilities

Consistent spacing patterns:

```tsx
// Spacer component
function Spacer({ size = 1 }) {
  return <box height={size} width={size} />
}

// Divider component
function Divider() {
  return <box height={1} width="100%" backgroundColor="#333" />
}

// Usage
<box flexDirection="column">
  <text>Section 1</text>
  <Spacer size={2} />
  <Divider />
  <Spacer size={2} />
  <text>Section 2</text>
</box>
```

### Axis Shorthand Props

Use `paddingX`/`paddingY` and `marginX`/`marginY` for horizontal/vertical spacing:

```tsx
// Horizontal padding (left + right)
<box paddingX={4}>
  <text>4 chars padding left and right</text>
</box>

// Vertical padding (top + bottom)
<box paddingY={2}>
  <text>2 lines padding top and bottom</text>
</box>

// Horizontal margin for centering-like effect
<box marginX={10}>
  <text>Indented content</text>
</box>

// Combined for card-like spacing
<box paddingX={3} paddingY={1} marginY={1} border>
  <text>Nicely spaced card</text>
</box>
```

These are shorthand for:
- `paddingX={n}` = `paddingLeft={n}` + `paddingRight={n}`
- `paddingY={n}` = `paddingTop={n}` + `paddingBottom={n}`
- `marginX={n}` = `marginLeft={n}` + `marginRight={n}`
- `marginY={n}` = `marginTop={n}` + `marginBottom={n}`
references/layout/REFERENCE.md
# OpenTUI Layout System

OpenTUI uses the Yoga layout engine, providing CSS Flexbox-like capabilities for positioning and sizing components in the terminal.

## Overview

Key concepts:
- **Flexbox model**: Familiar CSS Flexbox properties
- **Yoga engine**: Facebook's cross-platform layout engine
- **Terminal units**: Dimensions are in character cells (columns x rows)
- **Percentage support**: Relative sizing based on parent

## Flex Container Properties

### flexDirection

Controls the main axis direction:

```tsx
// Row (default) - children flow horizontally
<box flexDirection="row">
  <text>1</text>
  <text>2</text>
  <text>3</text>
</box>
// Output: 1 2 3

// Column - children flow vertically
<box flexDirection="column">
  <text>1</text>
  <text>2</text>
  <text>3</text>
</box>
// Output:
// 1
// 2
// 3

// Reverse variants
<box flexDirection="row-reverse">...</box>     // 3 2 1
<box flexDirection="column-reverse">...</box>  // Bottom to top
```

### justifyContent

Aligns children along the main axis:

```tsx
<box flexDirection="row" width={40} justifyContent="flex-start">
  {/* Children at start (left for row) */}
</box>

<box flexDirection="row" width={40} justifyContent="flex-end">
  {/* Children at end (right for row) */}
</box>

<box flexDirection="row" width={40} justifyContent="center">
  {/* Children centered */}
</box>

<box flexDirection="row" width={40} justifyContent="space-between">
  {/* First at start, last at end, rest evenly distributed */}
</box>

<box flexDirection="row" width={40} justifyContent="space-around">
  {/* Equal space around each child */}
</box>

<box flexDirection="row" width={40} justifyContent="space-evenly">
  {/* Equal space between all children and edges */}
</box>
```

### alignItems

Aligns children along the cross axis:

```tsx
<box flexDirection="row" height={10} alignItems="flex-start">
  {/* Children at top */}
</box>

<box flexDirection="row" height={10} alignItems="flex-end">
  {/* Children at bottom */}
</box>

<box flexDirection="row" height={10} alignItems="center">
  {/* Children vertically centered */}
</box>

<box flexDirection="row" height={10} alignItems="stretch">
  {/* Children stretch to fill height */}
</box>

<box flexDirection="row" height={10} alignItems="baseline">
  {/* Children aligned by text baseline */}
</box>
```

### flexWrap

Controls whether children wrap to new lines:

```tsx
<box flexDirection="row" flexWrap="nowrap" width={20}>
  {/* Children overflow (default) */}
</box>

<box flexDirection="row" flexWrap="wrap" width={20}>
  {/* Children wrap to next row */}
</box>

<box flexDirection="row" flexWrap="wrap-reverse" width={20}>
  {/* Children wrap upward */}
</box>
```

### gap

Space between children:

```tsx
<box flexDirection="row" gap={2}>
  <text>A</text>
  <text>B</text>
  <text>C</text>
</box>
// Output: A  B  C (2 spaces between)
```

## Flex Item Properties

### flexGrow

How much a child should grow relative to siblings:

```tsx
<box flexDirection="row" width={30}>
  <box flexGrow={1}><text>1</text></box>
  <box flexGrow={2}><text>2</text></box>
  <box flexGrow={1}><text>1</text></box>
</box>
// Widths: 7.5 | 15 | 7.5 (1:2:1 ratio)
```

### flexShrink

How much a child should shrink when space is limited:

```tsx
<box flexDirection="row" width={20}>
  <box width={15} flexShrink={1}><text>Shrinks</text></box>
  <box width={15} flexShrink={0}><text>Fixed</text></box>
</box>
```

### flexBasis

Initial size before growing/shrinking:

```tsx
<box flexDirection="row">
  <box flexBasis={20} flexGrow={1}>Starts at 20, can grow</box>
  <box flexBasis="50%">Half of parent</box>
</box>
```

### alignSelf

Override parent's alignItems for this child:

```tsx
<box flexDirection="row" height={10} alignItems="center">
  <text>Centered</text>
  <text alignSelf="flex-start">Top</text>
  <text alignSelf="flex-end">Bottom</text>
</box>
```

## Dimensions

### Fixed Dimensions

```tsx
<box width={40} height={10}>
  {/* Exactly 40 columns by 10 rows */}
</box>
```

### Percentage Dimensions

Parent must have explicit size:

```tsx
<box width="100%" height="100%">
  <box width="50%" height="50%">
    {/* Half of parent */}
  </box>
</box>
```

### Min/Max Constraints

```tsx
<box
  minWidth={20}
  maxWidth={60}
  minHeight={5}
  maxHeight={20}
>
  {/* Constrained sizing */}
</box>
```

## Spacing

### Padding (inside)

```tsx
// All sides
<box padding={2}>Content</box>

// Individual sides
<box
  paddingTop={1}
  paddingRight={2}
  paddingBottom={1}
  paddingLeft={2}
>
  Content
</box>
```

### Margin (outside)

```tsx
// All sides
<box margin={1}>Content</box>

// Individual sides
<box
  marginTop={1}
  marginRight={2}
  marginBottom={1}
  marginLeft={2}
>
  Content
</box>
```

## Positioning

### Relative (default)

Element flows in normal document order:

```tsx
<box position="relative">
  {/* Normal flow */}
</box>
```

### Absolute

Element positioned relative to nearest positioned ancestor:

```tsx
<box position="relative" width="100%" height="100%">
  <box
    position="absolute"
    left={10}
    top={5}
    width={20}
    height={5}
  >
    Positioned at (10, 5)
  </box>
</box>
```

### Position Properties

```tsx
<box
  position="absolute"
  left={10}      // From left edge
  top={5}        // From top edge
  right={10}     // From right edge
  bottom={5}     // From bottom edge
>
  Content
</box>
```

## Display

### Visibility Control

```tsx
// Visible (default)
<box display="flex">Visible</box>

// Hidden (removed from layout)
<box display="none">Hidden</box>
```

## Overflow

```tsx
<box overflow="visible">
  {/* Content can extend beyond bounds (default) */}
</box>

<box overflow="hidden">
  {/* Content clipped at bounds */}
</box>

<box overflow="scroll">
  {/* Scrollable when content exceeds bounds */}
</box>
```

## Z-Index

Control stacking order for overlapping elements:

```tsx
<box position="relative">
  <box position="absolute" zIndex={1}>Behind</box>
  <box position="absolute" zIndex={2}>In front</box>
</box>
```

## See Also

- [Layout Patterns](./patterns.md) - Common layout recipes
- [Components/Containers](../components/containers.md) - Box and ScrollBox details
references/react/api.md
# React API Reference

> **Requirements**: `@opentui/react` uses `react-reconciler` 0.33 and requires
> **React ≥ 19.2**. Run `bun add react@latest react-dom@latest` before upgrading.

## Rendering

### createRoot(renderer)

Creates a React root for rendering.

```tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"

const renderer = await createCliRenderer({
  exitOnCtrlC: false,  // Handle Ctrl+C yourself
})

const root = createRoot(renderer)
root.render(<App />)
```

## Hooks

### useRenderer()

Access the OpenTUI renderer instance.

```tsx
import { useRenderer } from "@opentui/react"
import { useEffect } from "react"

function App() {
  const renderer = useRenderer()
  
  useEffect(() => {
    // Access renderer properties
    console.log(`Terminal: ${renderer.width}x${renderer.height}`)
    
    // Show debug console
    renderer.console.show()
    
    // Access theme mode (dark/light based on terminal settings)
    console.log(`Theme: ${renderer.themeMode}`)  // "dark" | "light" | null
  }, [renderer])
  
  return <text>Hello</text>
}

// Listen for theme mode changes
function ThemedApp() {
  const renderer = useRenderer()
  const [theme, setTheme] = useState(renderer.themeMode ?? "dark")
  
  useEffect(() => {
    const handler = (mode: "dark" | "light") => setTheme(mode)
    renderer.on("theme_mode", handler)
    return () => renderer.off("theme_mode", handler)
  }, [renderer])
  
  return (
    <box backgroundColor={theme === "dark" ? "#1a1a2e" : "#ffffff"}>
      <text fg={theme === "dark" ? "#fff" : "#000"}>
        Current theme: {theme}
      </text>
    </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()  // Never use process.exit() directly!
    }
    if (key.ctrl && key.name === "s") {
      saveDocument()
    }
  })
  
  return <text>Press ESC to exit</text>
}

// With release events
function GameControls() {
  const [pressed, setPressed] = useState(new Set<string>())
  
  useKeyboard(
    (event) => {
      setPressed(keys => {
        const newKeys = new Set(keys)
        if (event.eventType === "release") {
          newKeys.delete(event.name)
        } else {
          newKeys.add(event.name)
        }
        return newKeys
      })
    },
    { release: true }  // Include release events
  )
  
  return <text>Pressed: {Array.from(pressed).join(", ")}</text>
}
```

**Options:**
- `release?: boolean` - Include key release events (default: false)

**KeyEvent properties:**
- `name: string` - Key name ("a", "escape", "f1", etc.)
- `sequence: string` - Raw escape sequence
- `ctrl: boolean` - Ctrl modifier
- `shift: boolean` - Shift modifier
- `meta: boolean` - Alt modifier
- `option: boolean` - Option modifier (macOS)
- `eventType: "press" | "release" | "repeat"`
- `repeated: boolean` - Key is being held

### 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 the terminal</text>
}
```

### useTerminalDimensions()

Get reactive terminal dimensions.

```tsx
import { useTerminalDimensions } from "@opentui/react"

function ResponsiveLayout() {
  const { width, height } = useTerminalDimensions()
  
  return (
    <box flexDirection={width > 80 ? "row" : "column"}>
      <box flexGrow={1}>
        <text>Width: {width}</text>
      </box>
      <box flexGrow={1}>
        <text>Height: {height}</text>
      </box>
    </box>
  )
}
```

### useTimeline(options?)

Create animations with the timeline system.

```tsx
import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"

function AnimatedBox() {
  const [width, setWidth] = useState(0)
  
  const timeline = useTimeline({
    duration: 2000,
    loop: false,
  })
  
  useEffect(() => {
    timeline.add(
      { width: 0 },
      {
        width: 50,
        duration: 2000,
        ease: "easeOutQuad",
        onUpdate: (anim) => {
          setWidth(Math.round(anim.targets[0].width))
        },
      }
    )
  }, [timeline])
  
  return <box style={{ width, height: 3, backgroundColor: "#6a5acd" }} />
}
```

**Options:**
- `duration?: number` - Default duration (ms)
- `loop?: boolean` - Loop the timeline
- `autoplay?: boolean` - Auto-start (default: true)
- `onComplete?: () => void` - Completion callback
- `onPause?: () => void` - Pause callback

**Timeline methods:**
- `add(target, properties, startTime?)` - Add animation
- `play()` - Start playback
- `pause()` - Pause playback
- `restart()` - Restart from beginning

### usePaste(handler)

Subscribe to bracketed-paste events. `handler` receives a `PasteEvent` with raw
`bytes` (decode with `decodePasteBytes`). The callback is kept stable across
renders.

```tsx
import { usePaste } from "@opentui/react"
import { decodePasteBytes } from "@opentui/core"

function Editor() {
  usePaste((event) => {
    console.log("Pasted:", decodePasteBytes(event.bytes))
  })
  return <textarea focused />
}
```

### useFocus(handler) / useBlur(handler)

Fire when the terminal window gains or loses OS focus.

```tsx
import { useFocus, useBlur } from "@opentui/react"

useFocus(() => console.log("Terminal gained focus"))
useBlur(() => console.log("Terminal lost focus"))
```

### useSelectionHandler(handler)

Fire when the user finishes a text selection (mouse-up). `handler` receives a
`Selection` (from `@opentui/core`); use `selection.getSelectedText()`.

```tsx
import { useSelectionHandler } from "@opentui/react"

useSelectionHandler((selection) => {
  console.log("Selected:", selection.getSelectedText())
})
```

> These four hooks (`usePaste`, `useFocus`, `useBlur`, `useSelectionHandler`)
> mirror the Solid hooks and are available from `@opentui/react`.

## Components

Full props for every component live in the shared **[components](../components/REFERENCE.md)**
references. This section only covers what is **React-specific**; read the linked
category file for the complete prop list.

### JSX Element Names

React uses **hyphenated** tag names. Full props are in the linked file:

| Element | Full props |
|---------|-----------|
| `<text>`, `<span>`, `<strong>`, `<em>`, `<u>`, `<a>`, `<br>` | [text-display.md](../components/text-display.md) |
| `<box>`, `<scrollbox>` | [containers.md](../components/containers.md) |
| `<input>`, `<textarea>`, `<select>`, `<tab-select>` | [inputs.md](../components/inputs.md) |
| `<code>`, `<line-number>`, `<diff>`, `<markdown>` | [code-diff.md](../components/code-diff.md) |
| `<ascii-font>` | [text-display.md](../components/text-display.md) |

### Text Styling Uses Nested Tags (React-specific)

Style text with **nested modifier elements**, not props:

```tsx
<text fg="#FFFFFF" bg="#000000" selectable>
  <span fg="red">Red</span> <strong>Bold</strong> <em>Italic</em> <u>Underline</u>
  <br />
  <a href="https://...">Link</a>
</text>
```

> **Note**: Do NOT use `bold`, `italic`, `underline` as props on `<text>`. Use
> nested modifier tags like `<strong>`, `<em>`, `<u>` instead.

### Controlled Inputs (React-specific)

Inputs are controlled with `value` + `onChange` (single value), and use the
`focused` prop to receive keyboard input:

```tsx
<input value={value} onChange={setValue} focused />
<textarea value={text} onChange={setText} focused width={40} height={10} />

// Select/tab-select: onChange fires on navigation, onSelect on Enter
<select options={opts} onChange={(i, opt) => setSel(opt)} focused />
```

### Scrollbox `style` Nesting (React-specific)

`<scrollbox>` takes a nested `style` object (`rootOptions`, `wrapperOptions`,
`viewportOptions`, `contentOptions`, `scrollbarOptions`). See
[containers.md](../components/containers.md) for the full structure.

## Type Exports

```tsx
import type {
  // Component props
  TextProps,
  BoxProps,
  InputProps,
  SelectProps,
  
  // Hook types
  KeyEvent,
  
  // From core
  CliRenderer,
} from "@opentui/react"
```
references/react/configuration.md
# React Configuration

## Project Setup

### Quick Start

```bash
bunx create-tui@latest -t react my-app
cd my-app && bun install
```

The CLI creates the `my-app` directory for you - it must **not already exist**.

Options: `--no-git` (skip git init), `--no-install` (skip bun install)

### Manual Setup

```bash
mkdir my-tui && cd my-tui
bun init
bun install @opentui/react @opentui/core react
```

## TypeScript Configuration

### tsconfig.json

```json
{
  "compilerOptions": {
    "lib": ["ESNext", "DOM"],
    "target": "ESNext",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    
    "jsx": "react-jsx",
    "jsxImportSource": "@opentui/react",
    
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["bun-types"]
  },
  "include": ["src/**/*"]
}
```

**Critical settings:**
- `jsx: "react-jsx"` - Use the new JSX transform
- `jsxImportSource: "@opentui/react"` - Import JSX runtime from OpenTUI
- `module` / `moduleResolution: "NodeNext"` - Recommended for OpenTUI compatibility

### Why DOM lib?

The `DOM` lib is needed for React types. OpenTUI's JSX types extend React's.

## Package Configuration

### package.json

```json
{
  "name": "my-tui-app",
  "type": "module",
  "scripts": {
    "start": "bun run src/index.tsx",
    "dev": "bun --watch run src/index.tsx",
    "test": "bun test",
    "build": "bun build src/index.tsx --outdir=dist --target=bun"
  },
  "dependencies": {
    "@opentui/core": "latest",
    "@opentui/react": "latest",
    "react": ">=19.0.0"
  },
  "devDependencies": {
    "@types/bun": "latest",
    "@types/react": ">=19.0.0",
    "typescript": "latest"
  }
}
```

## Project Structure

Recommended structure:

```
my-tui-app/
├── src/
│   ├── components/
│   │   ├── Header.tsx
│   │   ├── Sidebar.tsx
│   │   └── MainContent.tsx
│   ├── hooks/
│   │   └── useAppState.ts
│   ├── App.tsx
│   └── index.tsx
├── package.json
└── tsconfig.json
```

### Entry Point (src/index.tsx)

```tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
import { App } from "./App"

const renderer = await createCliRenderer({
  exitOnCtrlC: true,
})

createRoot(renderer).render(<App />)
```

### App Component (src/App.tsx)

```tsx
import { Header } from "./components/Header"
import { Sidebar } from "./components/Sidebar"
import { MainContent } from "./components/MainContent"

export function App() {
  return (
    <box flexDirection="column" width="100%" height="100%">
      <Header />
      <box flexDirection="row" flexGrow={1}>
        <Sidebar />
        <MainContent />
      </box>
    </box>
  )
}
```

## Renderer Configuration

### createCliRenderer Options

```tsx
import { createCliRenderer, ConsolePosition } from "@opentui/core"

const renderer = await createCliRenderer({
  // Rendering
  targetFPS: 60,
  
  // Behavior
  exitOnCtrlC: true,        // Set false to handle Ctrl+C yourself
  autoFocus: true,          // Auto-focus elements on click (default: true)
  useMouse: true,           // Enable mouse support (default: true)
  
  // Debug console
  consoleOptions: {
    position: ConsolePosition.BOTTOM,
    sizePercent: 30,
    startInDebugMode: false,
  },
  
  // Cleanup
  onDestroy: () => {
    // Cleanup code
  },
})
```

## Building for Distribution

### Bundling with Bun

```typescript
// build.ts
await Bun.build({
  entrypoints: ["./src/index.tsx"],
  outdir: "./dist",
  target: "bun",
  minify: true,
})
```

Run: `bun run build.ts`

### Creating Executables

```typescript
// build.ts
await Bun.build({
  entrypoints: ["./src/index.tsx"],
  outdir: "./dist",
  target: "bun",
  compile: {
    target: "bun-darwin-arm64",  // or bun-linux-x64, etc.
    outfile: "my-app",
  },
})
```

## Environment Variables

Create `.env` for development:

```env
# Debug settings
OTUI_SHOW_STATS=false
SHOW_CONSOLE=false

# App settings
API_URL=https://api.example.com
```

Bun auto-loads `.env` files. Access via `process.env`:

```tsx
const apiUrl = process.env.API_URL
```

## React DevTools

OpenTUI React supports React DevTools for debugging.

### Setup

1. Install DevTools as a dev dependency (must use version 7):
   ```bash
   bun add react-devtools-core@7 -d
   ```

2. Run DevTools standalone app:
   ```bash
   npx react-devtools@7
   ```

3. Start your app with `DEV=true` environment variable:
   ```bash
   DEV=true bun run src/index.tsx
   ```

**Important**: Auto-connect to DevTools ONLY happens when `DEV=true` is set. Without this environment variable, the DevTools connection code is not loaded.

### How It Works

OpenTUI checks for `process.env["DEV"] === "true"` at startup. When true, it dynamically imports `react-devtools-core` and connects to the standalone DevTools app.

## Testing Configuration

### Test Setup

```typescript
// src/test-utils.tsx
import { createTestRenderer } from "@opentui/core/testing"
import { createRoot } from "@opentui/react"

export async function renderForTest(
  element: React.ReactElement,
  options = { width: 80, height: 24 }
) {
  const testSetup = await createTestRenderer(options)
  createRoot(testSetup.renderer).render(element)
  return testSetup
}
```

### Test Example

```typescript
// src/components/Counter.test.tsx
import { test, expect } from "bun:test"
import { renderForTest } from "../test-utils"
import { Counter } from "./Counter"

test("Counter renders initial value", async () => {
  const { snapshot } = await renderForTest(<Counter initialValue={5} />)
  expect(snapshot()).toContain("Count: 5")
})
```

## Common Issues

### JSX Types Not Working

Ensure `jsxImportSource` is set:

```json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@opentui/react"
  }
}
```

### React Version Mismatch

Ensure React 19+:

```bash
bun install react@19 @types/react@19
```

### Module Resolution Errors

Use `moduleResolution: "bundler"` for Bun compatibility.
references/react/gotchas.md
# React 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).

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

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

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.

### Signal Handling

OpenTUI automatically handles cleanup for these signals:
- `SIGINT` (Ctrl+C), `SIGTERM`, `SIGQUIT` - Standard termination
- `SIGHUP` - Terminal closed/hangup
- `SIGBREAK` - Ctrl+Break (Windows)
- `SIGPIPE` - Broken pipe (output closed)
- `SIGBUS`, `SIGFPE` - Hardware errors

This ensures terminal state is restored even on unexpected termination. If you need custom signal handling, use `exitOnCtrlC: false` and handle signals yourself while still calling `renderer.destroy()`.

## JSX Configuration

### Missing jsxImportSource

**Symptom**: JSX elements have wrong types, components don't render

```
// Error: Property 'text' does not exist on type 'JSX.IntrinsicElements'
```

**Fix**: Configure tsconfig.json:

```json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@opentui/react"
  }
}
```

### HTML Elements vs TUI Elements

OpenTUI's JSX elements are **not** HTML elements:

```tsx
// WRONG - These are HTML concepts
<div>Not supported</div>
<button>Not supported</button>
<span>Only works inside <text></span>

// CORRECT - OpenTUI elements
<box>Container</box>
<text>Display text</text>
<text><span>Inline styled</span></text>
```

## Component Issues

### Text Modifiers Outside Text

Text modifiers only work inside `<text>`:

```tsx
// WRONG
<box>
  <strong>This won't work</strong>
</box>

// CORRECT
<box>
  <text>
    <strong>This works</strong>
  </text>
</box>
```

### Focus Not Working

Components must be explicitly focused:

```tsx
// WRONG - Won't receive keyboard input
<input placeholder="Type here..." />

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

// Or manage focus state
const [isFocused, setIsFocused] = useState(true)
<input placeholder="Type here..." focused={isFocused} />
```

### Select Not Responding

Select requires focus and proper options format:

```tsx
// WRONG - Missing required properties
<select options={["a", "b", "c"]} />

// CORRECT
<select
  options={[
    { name: "Option A", description: "First option", value: "a" },
    { name: "Option B", description: "Second option", 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:

```tsx
// 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
/>
```

## Hook Issues

### useKeyboard Not Firing

Multiple `useKeyboard` hooks can conflict:

```tsx
// Both handlers fire - may cause issues
function App() {
  useKeyboard((key) => { /* parent handler */ })
  return <ChildWithKeyboard />
}

function ChildWithKeyboard() {
  useKeyboard((key) => { /* child handler */ })
  return <text>Child</text>
}
```

**Solution**: Use a single keyboard handler or implement event stopping:

```tsx
function App() {
  const [handled, setHandled] = useState(false)
  
  useKeyboard((key) => {
    if (handled) {
      setHandled(false)
      return
    }
    // Handle at app level
  })
  
  return <Child onKeyHandled={() => setHandled(true)} />
}
```

### useEffect Cleanup

Always clean up intervals and listeners:

```tsx
// WRONG - Memory leak
useEffect(() => {
  setInterval(() => updateData(), 1000)
}, [])

// CORRECT
useEffect(() => {
  const interval = setInterval(() => updateData(), 1000)
  return () => clearInterval(interval)  // Cleanup!
}, [])
```

## Styling Issues

### Colors Not Applying

Check color format:

```tsx
// CORRECT formats
<text fg="#FF0000">Red</text>
<text fg="red">Red</text>
<box backgroundColor="#1a1a2e">Box</box>

// WRONG
<text fg="FF0000">Missing #</text>
<text color="#FF0000">Wrong prop name (use fg)</text>
```

### Layout Not Working

Ensure parent has dimensions:

```tsx
// WRONG - Parent has no height
<box flexDirection="column">
  <box flexGrow={1}>Won't grow</box>
</box>

// CORRECT
<box flexDirection="column" height="100%">
  <box flexGrow={1}>Will grow</box>
</box>
```

### Percentage Widths Not Working

Parent must have explicit dimensions:

```tsx
// WRONG
<box>
  <box width="50%">Won't work</box>
</box>

// CORRECT
<box width="100%">
  <box width="50%">Works</box>
</box>
```

## Performance Issues

### Too Many Re-renders

Avoid inline objects/functions in props:

```tsx
// WRONG - New object every render
<box style={{ padding: 2 }}>Content</box>

// BETTER - Use direct props
<box padding={2}>Content</box>

// OR memoize style objects
const style = useMemo(() => ({ padding: 2 }), [])
<box style={style}>Content</box>
```

### Heavy Components

Use React.memo for expensive components:

```tsx
const ExpensiveList = React.memo(function ExpensiveList({ 
  items 
}: { 
  items: Item[] 
}) {
  return (
    <box flexDirection="column">
      {items.map(item => (
        <text key={item.id}>{item.name}</text>
      ))}
    </box>
  )
})
```

### State Updates During Render

Don't update state during render:

```tsx
// WRONG
function Component({ value }: { value: number }) {
  const [count, setCount] = useState(0)
  
  // This causes infinite loop!
  if (value > 10) {
    setCount(value)
  }
  
  return <text>{count}</text>
}

// CORRECT
function Component({ value }: { value: number }) {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    if (value > 10) {
      setCount(value)
    }
  }, [value])
  
  return <text>{count}</text>
}
```

## Debugging

### Console Not Visible

OpenTUI captures console output. Show the overlay:

```tsx
import { useRenderer } from "@opentui/react"
import { useEffect } from "react"

function App() {
  const renderer = useRenderer()
  
  useEffect(() => {
    renderer.console.show()
    console.log("Now you can see this!")
  }, [renderer])
  
  return <box>{/* ... */}</box>
}
```

### Component Not Rendering

Check if component is in the tree:

```tsx
// WRONG - Conditional returns nothing
function MaybeComponent({ show }: { show: boolean }) {
  if (!show) return  // Returns undefined!
  return <text>Visible</text>
}

// CORRECT
function MaybeComponent({ show }: { show: boolean }) {
  if (!show) return null  // Explicit null
  return <text>Visible</text>
}
```

### Events Not Firing

Check event handler names:

```tsx
// WRONG
<box onClick={() => {}}>Click</box>  // No onClick in TUI

// CORRECT
<box onMouseDown={() => {}}>Click</box>
<box onMouseUp={() => {}}>Click</box>
```

## Runtime Issues

### Use Bun, Not Node

```bash
# WRONG
node src/index.tsx
npm run start

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

### Async Top-level

Bun supports top-level await, but be careful:

```tsx
// index.tsx - This works in Bun
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)

// If you need to handle errors
try {
  const renderer = await createCliRenderer()
  createRoot(renderer).render(<App />)
} catch (error) {
  console.error("Failed to initialize:", error)
  process.exit(1)
}
```

## Common Error Messages

### "Cannot read properties of undefined (reading 'root')"

Renderer not initialized:

```tsx
// WRONG
const renderer = createCliRenderer()  // Missing await!
createRoot(renderer).render(<App />)

// CORRECT
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
```

### "Invalid hook call"

Hooks called outside component:

```tsx
// WRONG
const dimensions = useTerminalDimensions()  // Outside component!

function App() {
  return <text>{dimensions.width}</text>
}

// CORRECT
function App() {
  const dimensions = useTerminalDimensions()
  return <text>{dimensions.width}</text>
}
```
references/react/patterns.md
# React Patterns

## State Management

### Local State with useState

```tsx
import { useState } from "react"

function Counter() {
  const [count, setCount] = useState(0)
  
  return (
    <box flexDirection="row" gap={2}>
      <text>Count: {count}</text>
      <box border onMouseDown={() => setCount(c => c - 1)}>
        <text>-</text>
      </box>
      <box border onMouseDown={() => setCount(c => c + 1)}>
        <text>+</text>
      </box>
    </box>
  )
}
```

### Complex State with useReducer

```tsx
import { useReducer } from "react"

type State = {
  items: string[]
  selectedIndex: number
}

type Action =
  | { type: "ADD_ITEM"; item: string }
  | { type: "REMOVE_ITEM"; index: number }
  | { type: "SELECT"; index: number }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "ADD_ITEM":
      return { ...state, items: [...state.items, action.item] }
    case "REMOVE_ITEM":
      return {
        ...state,
        items: state.items.filter((_, i) => i !== action.index),
      }
    case "SELECT":
      return { ...state, selectedIndex: action.index }
  }
}

function ItemList() {
  const [state, dispatch] = useReducer(reducer, {
    items: [],
    selectedIndex: 0,
  })
  
  // Use state and dispatch...
}
```

### Context for Global State

```tsx
import { createContext, useContext, useState, ReactNode } from "react"

type Theme = "dark" | "light"

const ThemeContext = createContext<{
  theme: Theme
  setTheme: (theme: Theme) => void
} | null>(null)

function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>("dark")
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) throw new Error("useTheme must be used within ThemeProvider")
  return context
}

// Usage
function App() {
  return (
    <ThemeProvider>
      <ThemedBox />
    </ThemeProvider>
  )
}

function ThemedBox() {
  const { theme } = useTheme()
  return (
    <box backgroundColor={theme === "dark" ? "#1a1a2e" : "#f0f0f0"}>
      <text fg={theme === "dark" ? "#fff" : "#000"}>
        Current theme: {theme}
      </text>
    </box>
  )
}
```

## Focus Management

### Focus State

```tsx
import { useState } from "react"
import { useKeyboard } from "@opentui/react"

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

### Ref-based Focus

```tsx
import { useRef, useEffect } from "react"

function AutoFocusInput() {
  const inputRef = useRef<any>(null)
  
  useEffect(() => {
    // Focus on mount
    inputRef.current?.focus()
  }, [])
  
  return <input ref={inputRef} placeholder="Auto-focused" />
}
```

## Keyboard Navigation

### Global Shortcuts

```tsx
import { useKeyboard, useRenderer } from "@opentui/react"

function App() {
  const renderer = useRenderer()
  
  useKeyboard((key) => {
    // Quit on Escape or Ctrl+C - use renderer.destroy(), never process.exit()
    if (key.name === "escape" || (key.ctrl && key.name === "c")) {
      renderer.destroy()
      return
    }
    
    // Toggle help on ?
    if (key.name === "?" || (key.shift && key.name === "/")) {
      setShowHelp(h => !h)
    }
    
    // Vim-style navigation
    if (key.name === "j") moveDown()
    if (key.name === "k") moveUp()
  })
  
  return <box>{/* ... */}</box>
}
```

### Component-level Shortcuts

```tsx
function Editor() {
  const [mode, setMode] = useState<"normal" | "insert">("normal")
  
  useKeyboard((key) => {
    if (mode === "normal") {
      if (key.name === "i") setMode("insert")
      if (key.name === "escape") setMode("normal")
    } else {
      if (key.name === "escape") setMode("normal")
      // Handle text input in insert mode
    }
  })
  
  return (
    <box>
      <text>Mode: {mode}</text>
      <textarea focused={mode === "insert"} />
    </box>
  )
}
```

## Form Handling

### Controlled Inputs

```tsx
import { useState } from "react"

function LoginForm() {
  const [username, setUsername] = useState("")
  const [password, setPassword] = useState("")
  
  const handleSubmit = () => {
    console.log("Login:", { username, password })
  }
  
  return (
    <box flexDirection="column" gap={1} padding={2} border>
      <text>Login</text>
      
      <box flexDirection="row" gap={1}>
        <text>Username:</text>
        <input
          value={username}
          onChange={setUsername}
          width={20}
        />
      </box>
      
      <box flexDirection="row" gap={1}>
        <text>Password:</text>
        <input
          value={password}
          onChange={setPassword}
          width={20}
        />
      </box>
      
      <box border onMouseDown={handleSubmit}>
        <text>Submit</text>
      </box>
    </box>
  )
}
```

### Form Validation

```tsx
function ValidatedForm() {
  const [email, setEmail] = useState("")
  const [error, setError] = useState("")
  
  const validateEmail = (value: string) => {
    if (!value.includes("@")) {
      setError("Invalid email address")
    } else {
      setError("")
    }
    setEmail(value)
  }
  
  return (
    <box flexDirection="column" gap={1}>
      <input
        value={email}
        onChange={validateEmail}
        placeholder="Email"
      />
      {error && <text fg="red">{error}</text>}
    </box>
  )
}
```

## Responsive Design

### Terminal-size Responsive

```tsx
import { useTerminalDimensions } from "@opentui/react"

function ResponsiveLayout() {
  const { width } = useTerminalDimensions()
  
  // Stack vertically on narrow terminals
  const isNarrow = width < 80
  
  return (
    <box flexDirection={isNarrow ? "column" : "row"}>
      <box flexGrow={isNarrow ? 0 : 1} height={isNarrow ? 10 : "100%"}>
        <text>Sidebar</text>
      </box>
      <box flexGrow={1}>
        <text>Main Content</text>
      </box>
    </box>
  )
}
```

### Dynamic Layouts

```tsx
function DynamicGrid({ items }: { items: string[] }) {
  const { width } = useTerminalDimensions()
  const columns = Math.max(1, Math.floor(width / 20))
  
  return (
    <box flexDirection="row" flexWrap="wrap">
      {items.map((item, i) => (
        <box key={i} width={`${100 / columns}%`} padding={1}>
          <text>{item}</text>
        </box>
      ))}
    </box>
  )
}
```

## Async Data Loading

### Loading States

```tsx
import { useState, useEffect } from "react"

function DataDisplay() {
  const [data, setData] = useState<string[] | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  
  useEffect(() => {
    async function load() {
      try {
        const response = await fetch("https://api.example.com/data")
        const json = await response.json()
        setData(json.items)
      } catch (e) {
        setError(e instanceof Error ? e.message : "Unknown error")
      } finally {
        setLoading(false)
      }
    }
    load()
  }, [])
  
  if (loading) {
    return <text>Loading...</text>
  }
  
  if (error) {
    return <text fg="red">Error: {error}</text>
  }
  
  return (
    <box flexDirection="column">
      {data?.map((item, i) => (
        <text key={i}>{item}</text>
      ))}
    </box>
  )
}
```

## Animation Patterns

### Simple Animations

```tsx
import { useState, useEffect } from "react"
import { useTimeline } from "@opentui/react"

function ProgressBar() {
  const [progress, setProgress] = useState(0)
  
  const timeline = useTimeline({ duration: 3000 })
  
  useEffect(() => {
    timeline.add(
      { value: 0 },
      {
        value: 100,
        duration: 3000,
        ease: "linear",
        onUpdate: (anim) => {
          setProgress(Math.round(anim.targets[0].value))
        },
      }
    )
  }, [])
  
  return (
    <box flexDirection="column" gap={1}>
      <text>Progress: {progress}%</text>
      <box width={50} height={1} backgroundColor="#333">
        <box
          width={`${progress}%`}
          height={1}
          backgroundColor="#00ff00"
        />
      </box>
    </box>
  )
}
```

### Interval-based Updates

```tsx
function Clock() {
  const [time, setTime] = useState(new Date())
  
  useEffect(() => {
    const interval = setInterval(() => {
      setTime(new Date())
    }, 1000)
    
    return () => clearInterval(interval)
  }, [])
  
  return <text>{time.toLocaleTimeString()}</text>
}
```

## Component Composition

### Render Props

```tsx
function Focusable({ 
  children 
}: { 
  children: (focused: boolean) => React.ReactNode 
}) {
  const [focused, setFocused] = useState(false)
  
  return (
    <box
      onMouseDown={() => setFocused(true)}
      onMouseUp={() => setFocused(false)}
    >
      {children(focused)}
    </box>
  )
}

// Usage
<Focusable>
  {(focused) => (
    <text fg={focused ? "#00ff00" : "#ffffff"}>
      {focused ? "Focused!" : "Click me"}
    </text>
  )}
</Focusable>
```

### Higher-Order Components

```tsx
function withBorder<P extends object>(
  Component: React.ComponentType<P>,
  borderStyle: string = "single"
) {
  return function BorderedComponent(props: P) {
    return (
      <box border borderStyle={borderStyle} padding={1}>
        <Component {...props} />
      </box>
    )
  }
}

// Usage
const BorderedText = withBorder(({ content }: { content: string }) => (
  <text>{content}</text>
))

<BorderedText content="Hello!" />
```
references/react/REFERENCE.md
# OpenTUI React (@opentui/react)

A React reconciler for building terminal user interfaces with familiar React patterns. Write TUIs using JSX, hooks, and component composition.

## Overview

OpenTUI React provides:
- **Custom reconciler**: React components render to OpenTUI renderables
- **JSX intrinsics**: `<text>`, `<box>`, `<input>`, etc.
- **Hooks**: `useKeyboard`, `useRenderer`, `useTimeline`, etc.
- **Full React compatibility**: useState, useEffect, context, and more

## When to Use React

Use the React reconciler when:
- You're familiar with React patterns
- You want declarative UI composition
- You need React's ecosystem (context, state management libraries)
- Building applications with complex state
- Team knows React already

## When NOT to Use React

| Scenario | Use Instead |
|----------|-------------|
| Maximum performance critical | `@opentui/core` (imperative) |
| Fine-grained reactivity | `@opentui/solid` |
| Smallest bundle size | `@opentui/core` |
| Building a framework/library | `@opentui/core` |

## Quick Start

```bash
bunx create-tui@latest -t react my-app
cd my-app
bun run src/index.tsx
```

The CLI creates the `my-app` directory for you - it must **not already exist**.

**Agent guidance**: Always use autonomous mode with `-t <template>` flag. Never use interactive mode (`bunx create-tui@latest my-app` without `-t`) as it requires user prompts that agents cannot respond to.

Or manual setup:

```bash
mkdir my-tui && cd my-tui
bun init
bun install @opentui/react @opentui/core react
```

```tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
import { useState } from "react"

function App() {
  const [count, setCount] = useState(0)
  
  return (
    <box border padding={2}>
      <text>Count: {count}</text>
      <box
        border
        onMouseDown={() => setCount(c => c + 1)}
      >
        <text>Click me!</text>
      </box>
    </box>
  )
}

const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
```

## Core Concepts

### JSX Elements

React maps JSX intrinsic elements to OpenTUI renderables:

```tsx
// These are not HTML elements!
<text>Hello</text>           // TextRenderable
<box border>Content</box>    // BoxRenderable
<input placeholder="..." />  // InputRenderable
<select options={[...]} />   // SelectRenderable
```

### Text Modifiers

Inside `<text>`, use modifier elements:

```tsx
<text>
  <strong>Bold</strong>, <em>italic</em>, and <u>underlined</u>
  <span fg="red">Colored text</span>
  <br />
  New line with <a href="https://example.com">link</a>
</text>
```

### Styling

Two approaches to styling:

```tsx
// Direct props
<box backgroundColor="blue" padding={2} border>
  <text fg="#00FF00">Green text</text>
</box>

// Style prop
<box style={{ backgroundColor: "blue", padding: 2, border: true }}>
  <text style={{ fg: "#00FF00" }}>Green text</text>
</box>
```

## Available Components

### Layout & Display
- `<text>` - Styled text content
- `<box>` - Container with borders and layout
- `<scrollbox>` - Scrollable container
- `<ascii-font>` - ASCII art text

### 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>` - Code with line numbers
- `<diff>` - Unified or split diff viewer

### Text Modifiers (inside `<text>`)
- `<span>` - Inline styled text
- `<strong>`, `<b>` - Bold
- `<em>`, `<i>` - Italic
- `<u>` - Underline
- `<br>` - Line break
- `<a>` - Link

## Essential Hooks

```tsx
import {
  useRenderer,
  useKeyboard,
  useOnResize,
  useTerminalDimensions,
  useTimeline,
} from "@opentui/react"
```

See [API Reference](./api.md) for detailed hook documentation.

## In This Reference

- [Configuration](./configuration.md) - Project setup, tsconfig, bundling
- [API](./api.md) - Components, hooks, createRoot
- [Patterns](./patterns.md) - State management, keyboard handling, forms
- [Gotchas](./gotchas.md) - Common issues, debugging, limitations

## See Also

- [Core](../core/REFERENCE.md) - Underlying imperative API
- [Solid](../solid/REFERENCE.md) - Alternative declarative approach
- [Components](../components/REFERENCE.md) - Component reference by category
- [Layout](../layout/REFERENCE.md) - Flexbox layout system
- [Keyboard](../keyboard/REFERENCE.md) - Input handling and shortcuts
- [Testing](../testing/REFERENCE.md) - Test renderer and snapshots
references/solid/api.md
# Solid API Reference

## Rendering

### render(node, rendererOrConfig?)

Renders a Solid component tree into a CLI renderer.

```tsx
import { render } from "@opentui/solid"

// Simple usage - creates renderer automatically
render(() => <App />)

// With config
render(() => <App />, {
  exitOnCtrlC: false,
  targetFPS: 60,
})

// With existing renderer
import { createCliRenderer } from "@opentui/core"

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

### testRender(node, options?)

Create a test renderer for snapshots and tests.

```tsx
import { testRender } from "@opentui/solid"

const testSetup = await testRender(() => <App />, {
  width: 40,
  height: 10,
})

// Access test utilities
testSetup.snapshot()  // Get current render
testSetup.renderer    // Access renderer
```

### extend(components)

Register custom renderables as JSX intrinsic elements.

```tsx
import { extend } from "@opentui/solid"
import { CustomRenderable } from "./custom"

extend({
  custom: CustomRenderable,
})

// Now usable in JSX
<custom prop="value" />
```

### getComponentCatalogue()

Returns the current component catalogue.

```tsx
import { getComponentCatalogue } from "@opentui/solid"

const catalogue = getComponentCatalogue()
console.log(Object.keys(catalogue))
```

## Hooks

### useRenderer()

Access the OpenTUI renderer instance.

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

function App() {
  const renderer = useRenderer()
  
  onMount(() => {
    console.log(`Terminal: ${renderer.width}x${renderer.height}`)
    renderer.console.show()
    
    // Access theme mode (dark/light based on terminal settings)
    console.log(`Theme: ${renderer.themeMode}`)  // "dark" | "light" | null
  })
  
  return <text>Hello</text>
}

// Listen for theme mode changes
function ThemedApp() {
  const renderer = useRenderer()
  const [theme, setTheme] = createSignal(renderer.themeMode ?? "dark")
  
  onMount(() => {
    renderer.on("theme_mode", (mode: "dark" | "light") => setTheme(mode))
  })
  
  return (
    <box backgroundColor={theme() === "dark" ? "#1a1a2e" : "#ffffff"}>
      <text fg={theme() === "dark" ? "#fff" : "#000"}>
        Current theme: {theme()}
      </text>
    </box>
  )
}
```

### useKeyboard(handler, options?)

Handle keyboard events.

```tsx
import { useKeyboard, useRenderer } from "@opentui/solid"

function App() {
  const renderer = useRenderer()
  
  useKeyboard((key) => {
    if (key.name === "escape") {
      renderer.destroy()  // Never use process.exit() directly!
    }
    if (key.ctrl && key.name === "s") {
      saveDocument()
    }
  })
  
  return <text>Press ESC to exit</text>
}

// With release events
function GameControls() {
  const [pressed, setPressed] = createSignal(new Set<string>())
  
  useKeyboard(
    (event) => {
      setPressed(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(pressed()).join(", ")}</text>
}
```

### usePaste(handler)

Handle paste events. Receives a `PasteEvent` with raw bytes.

```tsx
import { usePaste } from "@opentui/solid"
import { decodePasteBytes } from "@opentui/core"

function PasteHandler() {
  usePaste((event) => {
    const text = decodePasteBytes(event.bytes)
    console.log("Pasted:", text)
  })
  
  return <text>Paste something</text>
}
```

### onResize(callback)

Handle terminal resize events.

```tsx
import { onResize } from "@opentui/solid"

function App() {
  onResize((width, height) => {
    console.log(`Resized to ${width}x${height}`)
  })
  
  return <text>Resize the terminal</text>
}
```

### useTerminalDimensions()

Get reactive terminal dimensions.

```tsx
import { useTerminalDimensions } from "@opentui/solid"

function ResponsiveLayout() {
  const dimensions = useTerminalDimensions()
  
  return (
    <box flexDirection={dimensions().width > 80 ? "row" : "column"}>
      <text>Width: {dimensions().width}</text>
      <text>Height: {dimensions().height}</text>
    </box>
  )
}
```

### onFocus(callback) / onBlur(callback)

Handle terminal window focus and blur events. Solid-only hooks.

```tsx
import { onFocus, onBlur } from "@opentui/solid"

function App() {
  onFocus(() => {
    console.log("Terminal window gained focus")
  })
  
  onBlur(() => {
    console.log("Terminal window lost focus")
  })
  
  return <text>Focus/blur tracking</text>
}
```

These hooks fire when the terminal emulator window gains or loses operating system focus. The renderer deduplicates events (won't re-emit the same focus state).

### useSelectionHandler(handler)

Handle text selection events. Fires when the user finishes a mouse selection (mouse-up). Solid-only hook - React does not have this.

```tsx
import { useSelectionHandler } from "@opentui/solid"
import type { Selection } from "@opentui/core"

function SelectableText() {
  const [selected, setSelected] = createSignal("")
  const renderer = useRenderer()
  
  useSelectionHandler((selection: Selection) => {
    const text = selection.getSelectedText()
    if (text) {
      setSelected(text)
      renderer.copyToClipboardOSC52(text)
    }
  })
  
  return (
    <box flexDirection="column">
      <text selectable>Select this text with your mouse</text>
      <text fg="#888">Selected: {selected()}</text>
    </box>
  )
}
```

The `Selection` object aggregates selected text from all selectable renderables in the tree. See `keyboard/REFERENCE.md` (selection) for full details on the selection API and traversal model.

### useTimeline(options?)

Create animations with the timeline system.

```tsx
import { useTimeline } from "@opentui/solid"
import { createSignal, onMount } from "solid-js"

function AnimatedBox() {
  const [width, setWidth] = createSignal(0)
  
  const timeline = useTimeline({
    duration: 2000,
    loop: false,
  })
  
  onMount(() => {
    timeline.add(
      { width: 0 },
      {
        width: 50,
        duration: 2000,
        ease: "easeOutQuad",
        onUpdate: (anim) => {
          setWidth(Math.round(anim.targets[0].width))
        },
      }
    )
  })
  
  return <box style={{ width: width(), height: 3, backgroundColor: "#6a5acd" }} />
}
```

## Components

Full props for every component live in the shared **[components](../components/REFERENCE.md)**
references. This section only covers what is **Solid-specific**; read the linked
category file for the complete prop list.

### JSX Element Names (Solid uses underscores)

Multi-word elements use **underscores** (not hyphens like React):

| Element | Full props |
|---------|-----------|
| `<text>`, `<span>`, `<strong>`, `<em>`, `<u>`, `<a>`, `<br>` | [text-display.md](../components/text-display.md) |
| `<box>`, `<scrollbox>` | [containers.md](../components/containers.md) |
| `<input>`, `<textarea>`, `<select>`, `<tab_select>` | [inputs.md](../components/inputs.md) |
| `<code>`, `<line_number>`, `<diff>`, `<markdown>` | [code-diff.md](../components/code-diff.md) |
| `<ascii_font>` | [text-display.md](../components/text-display.md) |

### Text Styling Uses Nested Tags (Solid-specific)

```tsx
<text fg="#FFFFFF" bg="#000000" selectable>
  <span fg="red">Red</span> <strong>Bold</strong> <em>Italic</em> <u>Underline</u>
</text>
```

> **Note**: Do NOT use `bold`, `italic`, `underline` as props on `<text>`. Use
> nested modifier tags like `<strong>`, `<em>`, `<u>` instead.

### Reactive Values + `onInput` (Solid-specific)

Solid uses **`onInput`** (not React's `onChange`) for text inputs, and values are
signals. Use `<For>` inside `<scrollbox>` for lists:

```tsx
<input value={value()} onInput={setValue} focused />
<textarea value={text()} onInput={setText} focused width={40} height={10} />

// select/tab_select still use onChange (navigate) / onSelect (Enter)
<select options={opts} onChange={(i, opt) => setSelected(opt)} focused />

<scrollbox focused>
  <For each={items()}>{(item) => <text>{item}</text>}</For>
</scrollbox>
```

Scrollbox takes the same nested `style` object as React — see
[containers.md](../components/containers.md).

## Control Flow

Solid's control flow components work with OpenTUI:

### For

```tsx
import { For } from "solid-js"

<For each={items()}>
  {(item, index) => (
    <box key={index()}>
      <text>{item.name}</text>
    </box>
  )}
</For>
```

### Show

```tsx
import { Show } from "solid-js"

<Show when={isVisible()} fallback={<text>Hidden</text>}>
  <text>Visible content</text>
</Show>
```

### Switch/Match

```tsx
import { Switch, Match } from "solid-js"

<Switch>
  <Match when={status() === "loading"}>
    <text>Loading...</text>
  </Match>
  <Match when={status() === "error"}>
    <text fg="red">Error!</text>
  </Match>
  <Match when={status() === "success"}>
    <text fg="green">Success!</text>
  </Match>
</Switch>
```

### Index

```tsx
import { Index } from "solid-js"

<Index each={items()}>
  {(item, index) => (
    <text>{index}: {item().name}</text>
  )}
</Index>
```

## Special Components

### Portal

```tsx
import { Portal } from "@opentui/solid"

<Portal mount={targetNode}>
  <box>Portal content</box>
</Portal>
```

### Dynamic

```tsx
import { Dynamic } from "@opentui/solid"

<Dynamic
  component={isMultiline() ? "textarea" : "input"}
  placeholder="Enter text..."
  focused
/>
```
references/solid/configuration.md
# Solid Configuration

## Project Setup

### Quick Start

```bash
bunx create-tui@latest -t solid my-app
cd my-app && bun install
```

The CLI creates the `my-app` directory for you - it must **not already exist**.

Options: `--no-git` (skip git init), `--no-install` (skip bun install)

### Manual Setup

```bash
mkdir my-tui && cd my-tui
bun init
bun install @opentui/solid @opentui/core solid-js
```

## TypeScript Configuration

### tsconfig.json

```json
{
  "compilerOptions": {
    "lib": ["ESNext"],
    "target": "ESNext",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    
    "jsx": "preserve",
    "jsxImportSource": "@opentui/solid",
    
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["bun-types"]
  },
  "include": ["src/**/*"]
}
```

**Critical settings:**
- `jsx: "preserve"` - Let Solid's compiler handle JSX
- `jsxImportSource: "@opentui/solid"` - Import JSX runtime from OpenTUI Solid
- `module` / `moduleResolution: "NodeNext"` - Recommended for OpenTUI compatibility

## Bun Configuration

### bunfig.toml

**Required** for the Solid compiler:

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

This loads the Solid JSX transform before your code runs.

## Package Configuration

### package.json

```json
{
  "name": "my-tui-app",
  "type": "module",
  "scripts": {
    "start": "bun run src/index.tsx",
    "dev": "bun --watch run src/index.tsx",
    "test": "bun test",
    "build": "bun run build.ts"
  },
  "dependencies": {
    "@opentui/core": "latest",
    "@opentui/solid": "latest",
    "solid-js": "latest"
  },
  "devDependencies": {
    "@types/bun": "latest",
    "typescript": "latest"
  }
}
```

## Project Structure

Recommended structure:

```
my-tui-app/
├── src/
│   ├── components/
│   │   ├── Header.tsx
│   │   ├── Sidebar.tsx
│   │   └── MainContent.tsx
│   ├── stores/
│   │   └── appStore.ts
│   ├── App.tsx
│   └── index.tsx
├── bunfig.toml           # Required!
├── package.json
└── tsconfig.json
```

### Entry Point (src/index.tsx)

```tsx
import { render } from "@opentui/solid"
import { App } from "./App"

render(() => <App />)
```

### App Component (src/App.tsx)

```tsx
import { Header } from "./components/Header"
import { Sidebar } from "./components/Sidebar"
import { MainContent } from "./components/MainContent"

export function App() {
  return (
    <box flexDirection="column" width="100%" height="100%">
      <Header />
      <box flexDirection="row" flexGrow={1}>
        <Sidebar />
        <MainContent />
      </box>
    </box>
  )
}
```

## Renderer Configuration

### render() Options

```tsx
import { render } from "@opentui/solid"
import { ConsolePosition } from "@opentui/core"

render(() => <App />, {
  // Rendering
  targetFPS: 60,
  
  // Behavior
  exitOnCtrlC: true,
  autoFocus: true,          // Auto-focus elements on click (default: true)
  useMouse: true,           // Enable mouse support (default: true)
  
  // Debug console
  consoleOptions: {
    position: ConsolePosition.BOTTOM,
    sizePercent: 30,
    startInDebugMode: false,
  },
  
  // Cleanup
  onDestroy: () => {
    // Cleanup code
  },
})
```

### Using Existing Renderer

```tsx
import { render } from "@opentui/solid"
import { createCliRenderer } from "@opentui/core"

const renderer = await createCliRenderer({
  exitOnCtrlC: false,
})

render(() => <App />, renderer)
```

## Building for Distribution

### Build Script (build.ts)

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

await Bun.build({
  entrypoints: ["./src/index.tsx"],
  outdir: "./dist",
  target: "bun",
  minify: true,
  plugins: [solidPlugin],
})

console.log("Build complete!")
```

Run: `bun run build.ts`

### Creating Executables

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

await Bun.build({
  entrypoints: ["./src/index.tsx"],
  target: "bun",
  plugins: [solidPlugin],
  compile: {
    target: "bun-darwin-arm64",  // or bun-linux-x64, etc.
    outfile: "my-app",
  },
})
```

**Available targets:**
- `bun-darwin-arm64` - macOS Apple Silicon
- `bun-darwin-x64` - macOS Intel
- `bun-linux-x64` - Linux x64
- `bun-linux-arm64` - Linux ARM64
- `bun-windows-x64` - Windows x64

## Environment Variables

Create `.env` for development:

```env
# Debug settings
OTUI_SHOW_STATS=false
SHOW_CONSOLE=false

# App settings
API_URL=https://api.example.com
```

Bun auto-loads `.env` files:

```tsx
const apiUrl = process.env.API_URL
```

## Testing Configuration

### Test Setup

```typescript
// src/test-utils.tsx
import { testRender } from "@opentui/solid"

export async function renderForTest(
  Component: () => JSX.Element,
  options = { width: 80, height: 24 }
) {
  return await testRender(Component, options)
}
```

### Test Example

```typescript
// src/components/Counter.test.tsx
import { test, expect } from "bun:test"
import { renderForTest } from "../test-utils"
import { Counter } from "./Counter"

test("Counter renders initial value", async () => {
  const { snapshot } = await renderForTest(() => <Counter initialValue={5} />)
  expect(snapshot()).toContain("Count: 5")
})
```

## Common Configuration Issues

### Missing bunfig.toml

**Symptom**: JSX not transformed, syntax errors

**Fix**: Create `bunfig.toml` with preload:

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

### Wrong JSX Settings

**Symptom**: JSX compiles to React calls

**Fix**: Ensure tsconfig has:

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

### Build Missing Plugin

**Symptom**: Built output has untransformed JSX

**Fix**: Add Solid plugin to build:

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

await Bun.build({
  // ...
  plugins: [solidPlugin],
})
```
references/solid/gotchas.md
# 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).

```tsx
// 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:

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

### Wrong JSX Settings

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

**Fix**: Ensure tsconfig.json has:

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

### Build Without Plugin

**Symptom**: Built bundle has raw JSX

**Fix**: Add Solid plugin to build:

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

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

## Reactivity Issues

### Accessing Signals Without Calling

**Symptom**: Value never updates, shows `[Function]`

```tsx
// 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

```tsx
// 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

```tsx
// 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:

```tsx
// 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:

```tsx
// 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:

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

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

### Select Not Responding

```tsx
// 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:

```tsx
// 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:

```tsx
// 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:

```tsx
// 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

```tsx
// 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

```tsx
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

```tsx
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

```tsx
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:

```tsx
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:

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

## Runtime Issues

### Use Bun

```bash
# 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:

```tsx
// 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:

```tsx
// 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:

```tsx
// Wrong
<tab-select></tab-select>

// Correct
<tab_select></tab_select>
```

### "store is not a function"

Stores aren't called like signals:

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

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

// CORRECT
<text>{store.count}</text>
```
references/solid/patterns.md
# Solid Patterns

## Reactive State

### Signals

Basic reactive state with signals:

```tsx
import { createSignal } from "solid-js"

function Counter() {
  const [count, setCount] = createSignal(0)
  
  return (
    <box flexDirection="row" gap={2}>
      <text>Count: {count()}</text>
      <box border onMouseDown={() => setCount(c => c - 1)}>
        <text>-</text>
      </box>
      <box border onMouseDown={() => setCount(c => c + 1)}>
        <text>+</text>
      </box>
    </box>
  )
}
```

### Derived State

Compute values from signals:

```tsx
import { createSignal, createMemo } from "solid-js"

function PriceCalculator() {
  const [quantity, setQuantity] = createSignal(1)
  const [price, setPrice] = createSignal(9.99)
  
  // Derived value - only recalculates when dependencies change
  const total = createMemo(() => quantity() * price())
  const formatted = createMemo(() => `$${total().toFixed(2)}`)
  
  return (
    <box flexDirection="column">
      <text>Quantity: {quantity()}</text>
      <text>Price: ${price()}</text>
      <text>Total: {formatted()}</text>
    </box>
  )
}
```

### Effects

React to state changes:

```tsx
import { createSignal, createEffect, onCleanup } from "solid-js"

function AutoSave() {
  const [content, setContent] = createSignal("")
  
  createEffect(() => {
    const text = content()
    
    // Debounced save
    const timeout = setTimeout(() => {
      saveToFile(text)
    }, 1000)
    
    // Cleanup on next run or disposal
    onCleanup(() => clearTimeout(timeout))
  })
  
  return (
    <textarea
      value={content()}
      onInput={setContent}
      placeholder="Auto-saves after 1 second..."
    />
  )
}
```

## Stores

### createStore for Complex State

```tsx
import { createStore } from "solid-js/store"

interface AppState {
  user: { name: string; email: string } | null
  items: Array<{ id: number; name: string; done: boolean }>
  settings: { theme: "dark" | "light" }
}

function App() {
  const [state, setState] = createStore<AppState>({
    user: null,
    items: [],
    settings: { theme: "dark" },
  })
  
  const addItem = (name: string) => {
    setState("items", items => [
      ...items,
      { id: Date.now(), name, done: false }
    ])
  }
  
  const toggleItem = (id: number) => {
    setState("items", item => item.id === id, "done", done => !done)
  }
  
  const setTheme = (theme: "dark" | "light") => {
    setState("settings", "theme", theme)
  }
  
  return (
    <box backgroundColor={state.settings.theme === "dark" ? "#1a1a2e" : "#f0f0f0"}>
      <For each={state.items}>
        {(item) => (
          <text
            fg={item.done ? "#888" : "#fff"}
            onMouseDown={() => toggleItem(item.id)}
          >
            {item.done ? "[x]" : "[ ]"} {item.name}
          </text>
        )}
      </For>
    </box>
  )
}
```

### Store with Context

Share state across components:

```tsx
import { createStore } from "solid-js/store"
import { createContext, useContext, ParentComponent } from "solid-js"

interface Store {
  count: number
  items: string[]
}

type StoreContextValue = [
  Store,
  {
    increment: () => void
    addItem: (item: string) => void
  }
]

const StoreContext = createContext<StoreContextValue>()

const StoreProvider: ParentComponent = (props) => {
  const [state, setState] = createStore<Store>({
    count: 0,
    items: [],
  })
  
  const actions = {
    increment: () => setState("count", c => c + 1),
    addItem: (item: string) => setState("items", i => [...i, item]),
  }
  
  return (
    <StoreContext.Provider value={[state, actions]}>
      {props.children}
    </StoreContext.Provider>
  )
}

function useStore() {
  const context = useContext(StoreContext)
  if (!context) throw new Error("useStore must be used within StoreProvider")
  return context
}

// Usage
function Counter() {
  const [state, { increment }] = useStore()
  return (
    <box onMouseDown={increment}>
      <text>Count: {state.count}</text>
    </box>
  )
}
```

## Control Flow

### Conditional Rendering with Show

```tsx
import { Show, createSignal } from "solid-js"

function ToggleableContent() {
  const [visible, setVisible] = createSignal(false)
  
  return (
    <box flexDirection="column">
      <box border onMouseDown={() => setVisible(v => !v)}>
        <text>Toggle</text>
      </box>
      
      <Show
        when={visible()}
        fallback={<text fg="#888">Content is hidden</text>}
      >
        <text fg="#0f0">Content is visible!</text>
      </Show>
    </box>
  )
}
```

### Lists with For

```tsx
import { For, createSignal } from "solid-js"

function TodoList() {
  const [todos, setTodos] = createSignal([
    { id: 1, text: "Learn Solid", done: false },
    { id: 2, text: "Build TUI", done: false },
  ])
  
  const toggle = (id: number) => {
    setTodos(todos =>
      todos.map(t =>
        t.id === id ? { ...t, done: !t.done } : t
      )
    )
  }
  
  return (
    <box flexDirection="column">
      <For each={todos()}>
        {(todo) => (
          <box onMouseDown={() => toggle(todo.id)}>
            <text fg={todo.done ? "#888" : "#fff"}>
              {todo.done ? "[x]" : "[ ]"} {todo.text}
            </text>
          </box>
        )}
      </For>
    </box>
  )
}
```

### Index for Primitive Arrays

Use `Index` when array items are primitives:

```tsx
import { Index, createSignal } from "solid-js"

function StringList() {
  const [items, setItems] = createSignal(["apple", "banana", "cherry"])
  
  return (
    <box flexDirection="column">
      <Index each={items()}>
        {(item, index) => (
          <text>{index}: {item()}</text>
        )}
      </Index>
    </box>
  )
}
```

### Switch/Match for Multiple Conditions

```tsx
import { Switch, Match, createSignal } from "solid-js"

type Status = "idle" | "loading" | "success" | "error"

function StatusDisplay() {
  const [status, setStatus] = createSignal<Status>("idle")
  
  return (
    <Switch>
      <Match when={status() === "idle"}>
        <text>Ready</text>
      </Match>
      <Match when={status() === "loading"}>
        <text fg="#ff0">Loading...</text>
      </Match>
      <Match when={status() === "success"}>
        <text fg="#0f0">Success!</text>
      </Match>
      <Match when={status() === "error"}>
        <text fg="#f00">Error occurred</text>
      </Match>
    </Switch>
  )
}
```

## Focus Management

### Focus State

```tsx
import { createSignal } from "solid-js"
import { useKeyboard } from "@opentui/solid"

function FocusableForm() {
  const [focusIndex, setFocusIndex] = createSignal(0)
  const fields = ["name", "email", "message"]
  
  useKeyboard((key) => {
    if (key.name === "tab") {
      setFocusIndex(i => (i + 1) % fields.length)
    }
    if (key.shift && key.name === "tab") {
      setFocusIndex(i => (i - 1 + fields.length) % fields.length)
    }
  })
  
  return (
    <box flexDirection="column" gap={1}>
      <Index each={fields}>
        {(field, i) => (
          <input
            placeholder={`Enter ${field()}...`}
            focused={i === focusIndex()}
          />
        )}
      </Index>
    </box>
  )
}
```

## Keyboard Navigation

### Global Shortcuts

```tsx
import { useKeyboard } from "@opentui/solid"

function App() {
  const renderer = useRenderer()
  
  useKeyboard((key) => {
    if (key.name === "escape") {
      renderer.destroy()  // Never use process.exit() directly!
    }
    
    if (key.ctrl && key.name === "s") {
      save()
    }
    
    // Vim-style
    if (key.name === "j") moveDown()
    if (key.name === "k") moveUp()
  })
  
  return <box>{/* ... */}</box>
}
```

## Responsive Design

### Terminal-size Responsive

```tsx
import { useTerminalDimensions } from "@opentui/solid"

function ResponsiveLayout() {
  const dims = useTerminalDimensions()
  
  return (
    <box flexDirection={dims().width > 80 ? "row" : "column"}>
      <box flexGrow={1}>
        <text>Panel 1</text>
      </box>
      <box flexGrow={1}>
        <text>Panel 2</text>
      </box>
    </box>
  )
}
```

## Async Data

### Resources

```tsx
import { createResource, Suspense } from "solid-js"

async function fetchData() {
  const response = await fetch("https://api.example.com/data")
  return response.json()
}

function DataDisplay() {
  const [data] = createResource(fetchData)
  
  return (
    <Suspense fallback={<text>Loading...</text>}>
      <Show when={data()}>
        {(items) => (
          <For each={items()}>
            {(item) => <text>{item.name}</text>}
          </For>
        )}
      </Show>
    </Suspense>
  )
}
```

### Error Handling

```tsx
import { createResource, Show, ErrorBoundary } from "solid-js"

function SafeDataDisplay() {
  const [data] = createResource(fetchData)
  
  return (
    <ErrorBoundary fallback={(err) => <text fg="red">Error: {err.message}</text>}>
      <Show
        when={!data.loading}
        fallback={<text>Loading...</text>}
      >
        <Show
          when={!data.error}
          fallback={<text fg="red">Failed to load</text>}
        >
          <For each={data()}>
            {(item) => <text>{item.name}</text>}
          </For>
        </Show>
      </Show>
    </ErrorBoundary>
  )
}
```

## Component Composition

### Props and Children

```tsx
import { ParentComponent, JSX } from "solid-js"

interface PanelProps {
  title: string
  children: JSX.Element
}

const Panel: ParentComponent<{ title: string }> = (props) => {
  return (
    <box border padding={1} flexDirection="column">
      <text fg="#0ff">{props.title}</text>
      <box marginTop={1}>
        {props.children}
      </box>
    </box>
  )
}

// Usage
<Panel title="Settings">
  <text>Panel content here</text>
</Panel>
```

### Spread Props

```tsx
import { splitProps } from "solid-js"

interface ButtonProps {
  label: string
  onClick: () => void
  // ...rest goes to box
}

function Button(props: ButtonProps) {
  const [local, rest] = splitProps(props, ["label", "onClick"])
  
  return (
    <box border onMouseDown={local.onClick} {...rest}>
      <text>{local.label}</text>
    </box>
  )
}
```

## Animation

### With Timeline

```tsx
import { createSignal, onMount } from "solid-js"
import { useTimeline } from "@opentui/solid"

function AnimatedProgress() {
  const [width, setWidth] = createSignal(0)
  
  const timeline = useTimeline({
    duration: 2000,
  })
  
  onMount(() => {
    timeline.add(
      { value: 0 },
      {
        value: 50,
        duration: 2000,
        ease: "easeOutQuad",
        onUpdate: (anim) => {
          setWidth(Math.round(anim.targets[0].value))
        },
      }
    )
  })
  
  return (
    <box flexDirection="column" gap={1}>
      <text>Progress: {width()}%</text>
      <box width={50} height={1} backgroundColor="#333">
        <box width={width()} height={1} backgroundColor="#0f0" />
      </box>
    </box>
  )
}
```

### Interval-based

```tsx
import { createSignal, onCleanup } from "solid-js"

function Clock() {
  const [time, setTime] = createSignal(new Date())
  
  const interval = setInterval(() => {
    setTime(new Date())
  }, 1000)
  
  onCleanup(() => clearInterval(interval))
  
  return <text>{time().toLocaleTimeString()}</text>
}
```
references/solid/REFERENCE.md
# OpenTUI Solid (@opentui/solid)

A SolidJS reconciler for building terminal user interfaces with fine-grained reactivity. Get optimal performance with Solid's signal-based approach.

## Overview

OpenTUI Solid provides:
- **Custom reconciler**: Solid components render to OpenTUI renderables
- **JSX intrinsics**: `<text>`, `<box>`, `<input>`, etc.
- **Hooks**: `useKeyboard`, `useRenderer`, `useTimeline`, etc.
- **Fine-grained reactivity**: Only what changes re-renders
- **Portal & Dynamic**: Advanced composition primitives

## When to Use Solid

Use the Solid reconciler when:
- You want optimal re-rendering performance
- You prefer signal-based reactivity
- You need fine-grained control over updates
- Building performance-critical applications
- You already know SolidJS

## When NOT to Use Solid

| Scenario | Use Instead |
|----------|-------------|
| Team knows React, not Solid | `@opentui/react` |
| Maximum control needed | `@opentui/core` |
| Smallest bundle size | `@opentui/core` |
| Building a framework/library | `@opentui/core` |

## Quick Start

```bash
bunx create-tui@latest -t solid my-app
cd my-app && bun install
```

The CLI creates the `my-app` directory for you - it must **not already exist**.

Options: `--no-git` (skip git init), `--no-install` (skip bun install)

**Agent guidance**: Always use autonomous mode with `-t <template>` flag. Never use interactive mode (`bunx create-tui@latest my-app` without `-t`) as it requires user prompts that agents cannot respond to.

Or manually:

```bash
bun install @opentui/solid @opentui/core solid-js
```

```tsx
import { render } from "@opentui/solid"
import { createSignal } from "solid-js"

function App() {
  const [count, setCount] = createSignal(0)
  
  return (
    <box border padding={2}>
      <text>Count: {count()}</text>
      <box
        border
        onMouseDown={() => setCount(c => c + 1)}
      >
        <text>Click me!</text>
      </box>
    </box>
  )
}

render(() => <App />)
```

## Core Concepts

### Signals

Solid uses signals for reactive state:

```tsx
import { createSignal, createEffect } from "solid-js"

function Counter() {
  const [count, setCount] = createSignal(0)
  
  // Effect runs when count changes
  createEffect(() => {
    console.log("Count is now:", count())
  })
  
  return <text>Count: {count()}</text>
}
```

### JSX Elements

Solid maps JSX intrinsic elements to OpenTUI renderables:

```tsx
// Note: Some use underscores (Solid convention)
<text>Hello</text>           // TextRenderable
<box border>Content</box>    // BoxRenderable
<input placeholder="..." />  // InputRenderable
<select options={[...]} />   // SelectRenderable
<tab_select />               // TabSelectRenderable (underscore!)
<ascii_font />               // ASCIIFontRenderable (underscore!)
<line_number />              // LineNumberRenderable (underscore!)
```

### Text Modifiers

Inside `<text>`, use modifier elements:

```tsx
<text>
  <strong>Bold</strong>, <em>italic</em>, and <u>underlined</u>
  <span fg="red">Colored text</span>
  <br />
  New line with <a href="https://example.com">link</a>
</text>
```

## Available Components

### Layout & Display
- `<text>` - Styled text content
- `<box>` - Container with borders and layout
- `<scrollbox>` - Scrollable container
- `<ascii_font>` - ASCII art text (note underscore)

### Input
- `<input>` - Single-line text input
- `<textarea>` - Multi-line text input
- `<select>` - List selection
- `<tab_select>` - Tab-based selection (note underscore)

### Code & Diff
- `<code>` - Syntax-highlighted code
- `<line_number>` - Code with line numbers (note underscore)
- `<diff>` - Unified or split diff viewer

### Text Modifiers (inside `<text>`)
- `<span>` - Inline styled text
- `<strong>`, `<b>` - Bold
- `<em>`, `<i>` - Italic
- `<u>` - Underline
- `<br>` - Line break
- `<a>` - Link

## Special Components

### Portal

Render children to a different mount node:

```tsx
import { Portal } from "@opentui/solid"

function Overlay() {
  return (
    <Portal mount={renderer.root}>
      <box position="absolute" left={10} top={5} border>
        <text>Overlay content</text>
      </box>
    </Portal>
  )
}
```

### Dynamic

Render components dynamically:

```tsx
import { Dynamic } from "@opentui/solid"

function DynamicInput(props: { multiline: boolean }) {
  return (
    <Dynamic
      component={props.multiline ? "textarea" : "input"}
      placeholder="Enter text..."
    />
  )
}
```

## In This Reference

- [Configuration](./configuration.md) - Project setup, tsconfig, bunfig, building
- [API](./api.md) - Components, hooks, render function
- [Patterns](./patterns.md) - Signals, stores, control flow, composition
- [Gotchas](./gotchas.md) - Common issues, debugging, limitations

## See Also

- [Core](../core/REFERENCE.md) - Underlying imperative API
- [React](../react/REFERENCE.md) - Alternative declarative approach
- [Components](../components/REFERENCE.md) - Component reference by category
- [Layout](../layout/REFERENCE.md) - Flexbox layout system
- [Keyboard](../keyboard/REFERENCE.md) - Input handling and shortcuts
- [Testing](../testing/REFERENCE.md) - Test renderer and snapshots
references/testing/REFERENCE.md
# Testing OpenTUI Applications

How to test terminal user interfaces built with OpenTUI.

## Overview

OpenTUI provides:
- **Test Renderer**: Headless renderer for testing
- **Snapshot Testing**: Verify visual output
- **Interaction Testing**: Simulate user input

## When to Use

Use this reference when you need snapshot tests, interaction testing, or renderer-based regression checks.

## Test Setup

### Bun Test Runner

OpenTUI uses Bun's built-in test runner:

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

### Test Renderer

Create a test renderer for headless testing:

```typescript
import { createTestRenderer } from "@opentui/core/testing"

const testSetup = await createTestRenderer({
  width: 80,     // Terminal width
  height: 24,    // Terminal height
})
```

## Core Testing

### Basic Test

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

test("renders text", async () => {
  const testSetup = await createTestRenderer({
    width: 40,
    height: 10,
  })
  
  const text = new TextRenderable(testSetup.renderer, {
    id: "greeting",
    content: "Hello, World!",
  })
  
  testSetup.renderer.root.add(text)
  await testSetup.renderOnce()
  
  expect(testSetup.captureCharFrame()).toContain("Hello, World!")
})
```

### Snapshot Testing

```typescript
import { test, expect, afterEach } from "bun:test"
import { createTestRenderer } from "@opentui/core/testing"
import { BoxRenderable, TextRenderable } from "@opentui/core"

let testSetup: Awaited<ReturnType<typeof createTestRenderer>>

afterEach(() => {
  if (testSetup) {
    testSetup.renderer.destroy()
  }
})

test("component matches snapshot", async () => {
  testSetup = await createTestRenderer({
    width: 40,
    height: 10,
  })
  
  const box = new BoxRenderable(testSetup.renderer, {
    id: "box",
    border: true,
    width: 20,
    height: 5,
  })
  box.add(new TextRenderable(testSetup.renderer, {
    content: "Content",
  }))
  
  testSetup.renderer.root.add(box)
  await testSetup.renderOnce()
  
  expect(testSetup.captureCharFrame()).toMatchSnapshot()
})
```

### Test Renderer Setup Object

`createTestRenderer()` returns an expanded `TestRendererSetup` with helpers for
driving frames, input, and asserting on output:

| Property | Type | Description |
|----------|------|-------------|
| `renderer` | `TestRenderer` | Headless renderer instance |
| `mockInput` | `MockInput` | Simulate keyboard input |
| `mockMouse` | `MockMouse` | Simulate mouse events |
| `renderOnce()` | `() => Promise<void>` | Trigger a single render cycle |
| `flush(options?)` | `(o?: { maxPasses? }) => Promise<void>` | Drain scheduled render work (default 20 passes) |
| `waitFor(predicate, options?)` | `Promise<void>` | Advance frames until a boolean/async predicate passes |
| `waitForFrame(predicate, options?)` | `Promise<string>` | Like `waitFor`, predicate receives the frame string; resolves to it |
| `waitForVisualIdle(options?)` | `(o?: { quietFrames?, maxFrames? }) => Promise<void>` | Wait until rendering settles (defaults 1 / 20) |
| `captureCharFrame()` | `() => string` | Capture output as text |
| `captureSpans()` | `() => CapturedFrame` | Capture styled spans |
| `externalOutput` | `TestExternalOutput` | Capture scrollback / external output (`take()`, `takeText()`, `clear()`) |
| `getNativeStats()` | `() => NativeRenderStats` | Native render statistics |
| `resize(width, height)` | `(w, h) => void` | Resize the virtual terminal |

```typescript
import { createTestRenderer } from "@opentui/core/testing"

const t = await createTestRenderer({ width: 40, height: 10 })

// Wait for async content instead of guessing at renderOnce() timing
await t.waitForFrame((frame) => frame.includes("Loaded"))

// Or wait until the UI stops changing
await t.waitForVisualIdle()

// Assert on scrollback written above the viewport
const commits = t.externalOutput.take()
```

`TestRendererOptions` extends `CliRendererConfig` with `width`, `height`,
`kittyKeyboard`, and `otherModifiersMode`. Other exports from
`@opentui/core/testing`: `createMockKeys`, `KeyCodes`, `createMockMouse`,
`MouseButtons`, `createSpy`, `TestRecorder`, `ManualClock`,
`createTerminalCapabilities`, `setRendererCapabilities`,
`createMockTreeSitterClient`.

## React Testing

### Test Utilities

React provides a built-in `testRender` utility via the `@opentui/react/test-utils` subpath export:

```tsx
import { testRender } from "@opentui/react/test-utils"
```

This utility:
- Creates a headless test renderer
- Sets up the React Act environment automatically
- Handles proper unmounting on destroy
- Returns the standard test setup object

### Basic Component Test

```tsx
import { test, expect } from "bun:test"
import { testRender } from "@opentui/react/test-utils"

function Greeting({ name }: { name: string }) {
  return <text>Hello, {name}!</text>
}

test("Greeting renders name", async () => {
  const testSetup = await testRender(
    <Greeting name="World" />,
    { width: 80, height: 24 }
  )
  
  await testSetup.renderOnce()
  const frame = testSetup.captureCharFrame()
  
  expect(frame).toContain("Hello, World!")
})
```

### Snapshot Testing

```tsx
import { test, expect, afterEach } from "bun:test"
import { testRender } from "@opentui/react/test-utils"

let testSetup: Awaited<ReturnType<typeof testRender>>

afterEach(() => {
  if (testSetup) {
    testSetup.renderer.destroy()
  }
})

test("component matches snapshot", async () => {
  testSetup = await testRender(
    <box style={{ width: 20, height: 5, border: true }}>
      <text>Content</text>
    </box>,
    { width: 25, height: 8 }
  )
  
  await testSetup.renderOnce()
  const frame = testSetup.captureCharFrame()
  
  expect(frame).toMatchSnapshot()
})
```

### State Testing

```tsx
import { test, expect, afterEach } from "bun:test"
import { useState } from "react"
import { testRender } from "@opentui/react/test-utils"

let testSetup: Awaited<ReturnType<typeof testRender>>

afterEach(() => {
  if (testSetup) {
    testSetup.renderer.destroy()
  }
})

function Counter() {
  const [count, setCount] = useState(0)
  return (
    <box>
      <text>Count: {count}</text>
    </box>
  )
}

test("Counter shows initial value", async () => {
  testSetup = await testRender(
    <Counter />,
    { width: 20, height: 5 }
  )
  
  await testSetup.renderOnce()
  const frame = testSetup.captureCharFrame()
  
  expect(frame).toContain("Count: 0")
})
```

### Test Setup/Teardown Pattern

For multiple tests, use beforeEach/afterEach to manage the renderer lifecycle:

```tsx
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import { testRender } from "@opentui/react/test-utils"

let testSetup: Awaited<ReturnType<typeof testRender>>

describe("MyComponent", () => {
  beforeEach(async () => {
    if (testSetup) {
      testSetup.renderer.destroy()
    }
  })

  afterEach(() => {
    if (testSetup) {
      testSetup.renderer.destroy()
    }
  })

  test("renders correctly", async () => {
    testSetup = await testRender(<MyComponent />, {
      width: 40,
      height: 10,
    })

    await testSetup.renderOnce()
    const frame = testSetup.captureCharFrame()
    expect(frame).toMatchSnapshot()
  })
})
```

### Test Setup Return Object

The `testRender` function returns a test setup object with these properties:

| Property | Type | Description |
|----------|------|-------------|
| `renderer` | `Renderer` | The headless renderer instance |
| `renderOnce` | `() => Promise<void>` | Triggers a single render cycle |
| `captureCharFrame` | `() => string` | Captures current output as text |
| `resize` | `(width, height) => void` | Resize the virtual terminal |

## Solid Testing

### Test Utilities

Solid exports `testRender` directly from the main package:

```tsx
import { testRender } from "@opentui/solid"
```

Note: Unlike React, Solid's `testRender` takes a **function component** (not a JSX element).

### Basic Component Test

```tsx
import { test, expect } from "bun:test"
import { testRender } from "@opentui/solid"

function Greeting(props: { name: string }) {
  return <text>Hello, {props.name}!</text>
}

test("Greeting renders name", async () => {
  const testSetup = await testRender(
    () => <Greeting name="World" />,
    { width: 80, height: 24 }
  )
  
  await testSetup.renderOnce()
  const frame = testSetup.captureCharFrame()
  
  expect(frame).toContain("Hello, World!")
})
```

### Snapshot Testing

```tsx
import { test, expect, afterEach } from "bun:test"
import { testRender } from "@opentui/solid"

let testSetup: Awaited<ReturnType<typeof testRender>>

afterEach(() => {
  if (testSetup) {
    testSetup.renderer.destroy()
  }
})

test("component matches snapshot", async () => {
  testSetup = await testRender(
    () => (
      <box style={{ width: 20, height: 5, border: true }}>
        <text>Content</text>
      </box>
    ),
    { width: 25, height: 8 }
  )
  
  await testSetup.renderOnce()
  const frame = testSetup.captureCharFrame()
  
  expect(frame).toMatchSnapshot()
})
```

## Snapshot Format

Snapshots capture the rendered terminal output as text:

```
┌──────────────────┐
│ Hello, World!    │
│                  │
└──────────────────┘
```

### Updating Snapshots

```bash
bun test --update-snapshots
```

## Interaction Testing

### Simulating Key Presses

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

let testSetup: Awaited<ReturnType<typeof createTestRenderer>>

afterEach(() => {
  if (testSetup) {
    testSetup.renderer.destroy()
  }
})

test("responds to keyboard", async () => {
  testSetup = await createTestRenderer({
    width: 40,
    height: 10,
  })
  
  // Create component that responds to keys
  // ...
  
  // Simulate keypress
  testSetup.renderer.keyInput.emit("keypress", {
    name: "enter",
    sequence: "\r",
    ctrl: false,
    shift: false,
    meta: false,
    option: false,
    eventType: "press",
    repeated: false,
  })
  
  // Render after the keypress
  await testSetup.renderOnce()
  
  expect(testSetup.captureCharFrame()).toContain("Selected")
})
```

### Testing Focus

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

let testSetup: Awaited<ReturnType<typeof createTestRenderer>>

afterEach(() => {
  if (testSetup) {
    testSetup.renderer.destroy()
  }
})

test("input receives focus", async () => {
  testSetup = await createTestRenderer({
    width: 40,
    height: 10,
  })
  
  const input = new InputRenderable(testSetup.renderer, {
    id: "test-input",
    placeholder: "Type here",
  })
  testSetup.renderer.root.add(input)
  
  input.focus()
  
  expect(input.isFocused()).toBe(true)
})
```

## Test Organization

### File Structure

```
src/
├── components/
│   ├── Button.tsx
│   └── Button.test.tsx
├── hooks/
│   ├── useCounter.ts
│   └── useCounter.test.ts
└── test-utils.tsx
```

### Running Tests

```bash
# Run all tests
bun test

# Run specific test file
bun test src/components/Button.test.tsx

# Run with filter
bun test --filter "Button"

# Watch mode
bun test --watch
```

## Patterns

### Testing Conditional Rendering (React)

```tsx
import { test, expect, afterEach } from "bun:test"
import { testRender } from "@opentui/react/test-utils"

let testSetup: Awaited<ReturnType<typeof testRender>>

afterEach(() => {
  if (testSetup) {
    testSetup.renderer.destroy()
  }
})

test("shows loading state", async () => {
  testSetup = await testRender(
    <DataLoader loading={true} />,
    { width: 40, height: 10 }
  )
  
  await testSetup.renderOnce()
  expect(testSetup.captureCharFrame()).toContain("Loading...")
})

test("shows data when loaded", async () => {
  testSetup = await testRender(
    <DataLoader loading={false} data={["Item 1", "Item 2"]} />,
    { width: 40, height: 10 }
  )
  
  await testSetup.renderOnce()
  const frame = testSetup.captureCharFrame()
  expect(frame).toContain("Item 1")
  expect(frame).toContain("Item 2")
})
```

### Testing Lists

```tsx
test("renders all items", async () => {
  const items = ["Apple", "Banana", "Cherry"]
  
  testSetup = await testRender(
    <ItemList items={items} />,
    { width: 40, height: 10 }
  )
  
  await testSetup.renderOnce()
  const frame = testSetup.captureCharFrame()
  
  items.forEach(item => {
    expect(frame).toContain(item)
  })
})
```

### Testing Layouts

```tsx
test("matches layout snapshot", async () => {
  testSetup = await testRender(
    <AppLayout />,
    { width: 120, height: 40 }  // Larger viewport
  )
  
  await testSetup.renderOnce()
  expect(testSetup.captureCharFrame()).toMatchSnapshot()
})
```

## Debugging Tests

### Print Frame Output

```tsx
import { testRender } from "@opentui/react/test-utils"

test("debug output", async () => {
  const testSetup = await testRender(
    <MyComponent />,
    { width: 40, height: 10 }
  )
  
  await testSetup.renderOnce()
  const frame = testSetup.captureCharFrame()
  
  // Print to see what's rendered
  console.log(frame)
  
  expect(frame).toContain("expected")
})
```

### Verbose Mode

```bash
bun test --verbose
```

## Gotchas

### Async Rendering

Always call `renderOnce()` after setting up your component to ensure rendering is complete:

```typescript
const testSetup = await testRender(<MyComponent />, { width: 40, height: 10 })
await testSetup.renderOnce()  // Required before capturing frame
const frame = testSetup.captureCharFrame()
```

### Test Isolation and Cleanup

Always destroy the renderer after each test to avoid resource leaks:

```typescript
import { afterEach } from "bun:test"

let testSetup: Awaited<ReturnType<typeof testRender>>

afterEach(() => {
  if (testSetup) {
    testSetup.renderer.destroy()
  }
})

test("test 1", async () => {
  testSetup = await testRender(<Component1 />, { width: 40, height: 10 })
  // ...
})

test("test 2", async () => {
  testSetup = await testRender(<Component2 />, { width: 40, height: 10 })
  // ...
})
```

### Snapshot Dimensions

Be consistent with test dimensions for stable snapshots:

```typescript
const testSetup = await createTestRenderer({
  width: 80,   // Standard width
  height: 24,  // Standard height
})
```

### Running from Package Directory

Run tests from the package directory:

```bash
cd packages/core
bun test

# Not from repo root for package-specific tests
```

## See Also

- [Core API](../core/api.md) - `createTestRenderer` and renderable classes
- [React Configuration](../react/configuration.md) - React test setup
- [Solid Configuration](../solid/configuration.md) - Solid test setup
- [Keyboard](../keyboard/REFERENCE.md) - Simulating key events in tests