> omp can create TUI components. Ask it to build one for your use case.

# TUI Components

Hooks and custom tools can render custom TUI components for interactive user interfaces. This page covers the component system and available building blocks.

**Source:** [`@oh-my-pi/pi-tui`](https://github.com/badlogic/pi-mono/tree/main/packages/tui)

## Component Interface

All components implement:

```typescript
interface Component {
	render(width: number): string[];
	handleInput?(data: string): void;
	invalidate?(): void;
}
```

| Method               | Description                                                                    |
| -------------------- | ------------------------------------------------------------------------------ |
| `render(width)`      | Return array of strings (one per line). Each line **must not exceed `width`**. |
| `handleInput?(data)` | Receive keyboard input when component has focus.                               |
| `invalidate?()`      | Clear cached render state.                                                     |

## Using Components

**In hooks** via `ctx.ui.custom()`:

```typescript
pi.on("session_start", async (_event, ctx) => {
	const handle = ctx.ui.custom(myComponent);
	// handle.requestRender() - trigger re-render
	// handle.close() - restore normal UI
});
```

**In custom tools** via `pi.ui.custom()`:

```typescript
async execute(toolCallId, params, onUpdate, ctx, signal) {
  const handle = pi.ui.custom(myComponent);
  // ...
  handle.close();
}
```

## Built-in Components

Import from `@oh-my-pi/pi-tui`:

```typescript
import { Text, Box, Container, Spacer, Markdown } from "@oh-my-pi/pi-tui";
```

### Text

Multi-line text with word wrapping.

```typescript
const text = new Text(
	"Hello World", // content
	1, // paddingX (default: 1)
	1, // paddingY (default: 1)
	(s) => bgGray(s) // optional background function
);
text.setText("Updated");
```

### Box

Container with padding and background color.

```typescript
const box = new Box(
	1, // paddingX
	1, // paddingY
	(s) => bgGray(s) // background function
);
box.addChild(new Text("Content", 0, 0));
box.setBgFn((s) => bgBlue(s));
```

### Container

Groups child components vertically.

```typescript
const container = new Container();
container.addChild(component1);
container.addChild(component2);
container.removeChild(component1);
```

### Spacer

Empty vertical space.

```typescript
const spacer = new Spacer(2); // 2 empty lines
```

### Markdown

Renders markdown with syntax highlighting.

```typescript
const md = new Markdown(
	"# Title\n\nSome **bold** text",
	1, // paddingX
	1, // paddingY
	theme // MarkdownTheme (see below)
);
md.setText("Updated markdown");
```

### Image

Renders images in supported terminals (Kitty, iTerm2, Ghostty, WezTerm).

```typescript
const image = new Image(
	base64Data, // base64-encoded image
	"image/png", // MIME type
	theme, // ImageTheme
	{ maxWidthCells: 80, maxHeightCells: 24 }
);
```

## Keyboard Input

Use key detection helpers:

```typescript
import {
  isEnter, isEscape, isTab,
  isArrowUp, isArrowDown, isArrowLeft, isArrowRight,
  isCtrlC, isCtrlO, isBackspace, isDelete,
  // ... and more
} from "@oh-my-pi/pi-tui";

handleInput(data: string) {
  if (isArrowUp(data)) {
    this.selectedIndex--;
  } else if (isEnter(data)) {
    this.onSelect?.(this.selectedIndex);
  } else if (isEscape(data)) {
    this.onCancel?.();
  }
}
```

## Line Width

**Critical:** Each line from `render()` must not exceed the `width` parameter. Width calculations and wrapping follow Bun’s built-ins (`Bun.stringWidth`, `Bun.wrapAnsi`).

```typescript
import { visibleWidth, truncateToWidth } from "@oh-my-pi/pi-tui";

render(width: number): string[] {
  // Truncate long lines
  return [truncateToWidth(this.text, width)];
}
```

Utilities:

- `visibleWidth(str)` - Get display width (ANSI-safe, Unicode-width aware)
- `truncateToWidth(str, width, ellipsis?)` - Truncate with optional ellipsis
- `wrapTextWithAnsi(str, width)` - Word wrap preserving ANSI codes (Bun.wrapAnsi)

## Creating Custom Components

Example: Interactive selector

```typescript
import { isEnter, isEscape, isArrowUp, isArrowDown, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";

class MySelector {
	private items: string[];
	private selected = 0;
	private cachedWidth?: number;
	private cachedLines?: string[];

	public onSelect?: (item: string) => void;
	public onCancel?: () => void;

	constructor(items: string[]) {
		this.items = items;
	}

	handleInput(data: string): void {
		if (isArrowUp(data) && this.selected > 0) {
			this.selected--;
			this.invalidate();
		} else if (isArrowDown(data) && this.selected < this.items.length - 1) {
			this.selected++;
			this.invalidate();
		} else if (isEnter(data)) {
			this.onSelect?.(this.items[this.selected]);
		} else if (isEscape(data)) {
			this.onCancel?.();
		}
	}

	render(width: number): string[] {
		if (this.cachedLines && this.cachedWidth === width) {
			return this.cachedLines;
		}

		this.cachedLines = this.items.map((item, i) => {
			const prefix = i === this.selected ? "> " : "  ";
			return truncateToWidth(prefix + item, width);
		});
		this.cachedWidth = width;
		return this.cachedLines;
	}

	invalidate(): void {
		this.cachedWidth = undefined;
		this.cachedLines = undefined;
	}
}
```

Usage in a hook:

```typescript
pi.registerCommand("pick", {
	description: "Pick an item",
	handler: async (args, ctx) => {
		const items = ["Option A", "Option B", "Option C"];
		const selector = new MySelector(items);

		let handle: { close: () => void; requestRender: () => void };

		await new Promise<void>((resolve) => {
			selector.onSelect = (item) => {
				ctx.ui.notify(`Selected: ${item}`, "info");
				handle.close();
				resolve();
			};
			selector.onCancel = () => {
				handle.close();
				resolve();
			};
			handle = ctx.ui.custom(selector);
		});
	},
});
```

## Theming

Components accept theme objects for styling.

**In `renderCall`/`renderResult`**, use the `theme` parameter:

```typescript
renderResult(result, options, theme) {
  // Use theme.fg() for foreground colors
  return new Text(theme.fg("success", "Done!"), 0, 0);

  // Use theme.bg() for background colors
  const styled = theme.bg("toolPendingBg", theme.fg("accent", "text"));
}
```

**Foreground colors** (`theme.fg(color, text)`):

| Category | Colors                                                                                                                                                    |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| General  | `text`, `accent`, `muted`, `dim`                                                                                                                          |
| Status   | `success`, `error`, `warning`                                                                                                                             |
| Borders  | `border`, `borderAccent`, `borderMuted`                                                                                                                   |
| Messages | `userMessageText`, `customMessageText`, `customMessageLabel`                                                                                              |
| Tools    | `toolTitle`, `toolOutput`                                                                                                                                 |
| Diffs    | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext`                                                                                                     |
| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet`                      |
| Syntax   | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |
| Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`                                                        |
| Modes    | `bashMode`                                                                                                                                                |

**Background colors** (`theme.bg(color, text)`):

`selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`

**For Markdown**, use `getMarkdownTheme()`:

```typescript
import { getMarkdownTheme } from "@oh-my-pi/pi-coding-agent";
import { Markdown } from "@oh-my-pi/pi-tui";

renderResult(result, options, theme) {
  const mdTheme = getMarkdownTheme();
  return new Markdown(result.details.markdown, 0, 0, mdTheme);
}
```

**For custom components**, define your own theme interface:

```typescript
interface MyTheme {
	selected: (s: string) => string;
	normal: (s: string) => string;
}
```

## Performance

Cache rendered output when possible:

```typescript
class CachedComponent {
	private cachedWidth?: number;
	private cachedLines?: string[];

	render(width: number): string[] {
		if (this.cachedLines && this.cachedWidth === width) {
			return this.cachedLines;
		}
		// ... compute lines ...
		this.cachedWidth = width;
		this.cachedLines = lines;
		return lines;
	}

	invalidate(): void {
		this.cachedWidth = undefined;
		this.cachedLines = undefined;
	}
}
```

Call `invalidate()` when state changes, then `handle.requestRender()` to trigger re-render.

## Examples

- **Snake game**: [examples/hooks/snake.ts](../examples/hooks/snake.ts) - Full game with keyboard input, game loop, state persistence
- **Custom tool rendering**: [examples/custom-tools/todo/](../examples/custom-tools/todo/) - Custom `renderCall` and `renderResult`
