- name
- pi-tui-design
- description
- Create distinctive, crafted TUI components for pi using @mariozechner/pi-tui and @mariozechner/pi-coding-agent. Use when building interactive terminal UIs — custom components, overlays, dialogs, dashboards, widgets, data visualizations, animated elements, game-like interfaces, or any visual TUI work inside pi extensions or custom tools. Triggers on: 'build a TUI component', 'make a dashboard', 'create an overlay', 'interactive widget', 'terminal UI', 'custom component', 'pi-tui', or any request to create visual, interactive terminal interfaces. Also use when beautifying or redesigning existing TUI components.
# TUI Design for Pi
Build terminal interfaces that feel *crafted*, not generated. The terminal is constrained — fixed-width character grid, keyboard only, theme-dependent colors — and those constraints are a design feature.
## Design Thinking
Before coding, commit to a direction:
- **Tone**: Minimal and precise? Dense and information-rich? Playful? Industrial? The terminal has its own aesthetic vocabulary — box-drawing elegance, braille-pattern density, block-element weight, symbol clarity.
- **Scope**: Full-screen takeover (`ctx.ui.custom`)? Floating overlay? Persistent widget? Status line? Tool rendering? Match the delivery surface to the interaction weight.
- **Differentiation**: What detail makes this feel intentional? A progress bar with braille resolution. Aligned columns with accent headers. A dialog with breathing room.
## Terminal Aesthetic Vocabulary
### Unicode Repertoire — Your Typography
Terminal UIs have no font choices. Instead, the *character repertoire* is the typography:
| Category | Characters | Use |
|----------|-----------|-----|
| Box-drawing (light) | `─│┌┐└┘├┤┬┴┼` | Standard borders, tables |
| Box-drawing (rounded) | `╭╮╰╯` | Softer, modern feel |
| Box-drawing (heavy) | `━┃┏┓┗┛┣┫┳┻╋` | Emphasis, headers |
| Box-drawing (double) | `═║╔╗╚╝╠╣╦╩╬` | Formal, structured |
| Block elements | `█▓▒░▀▄▌▐` | Progress bars, density, fill |
| Braille | `⠀⠁⠂⠃...⣿` | High-resolution patterns, sparklines, charts |
| Symbols | `◆●○◉◎✓✗▸▹▶▷△▽★☆♦` | Status indicators, bullets, selections |
| Math/arrows | `→←↑↓↔↕⇒⟶⟵∙⋯` | Navigation hints, flow |
| Powerline | `░` | Segment separators (terminal-dependent) |
**Hierarchy through character weight**: `█` (heavy) → `▓` (medium) → `▒` (light) → `░` (subtle) → ` ` (empty). Use this for visual density gradients, not just fill.
**Aspect ratio**: Terminal cells are ~2:1 (twice as tall as wide). A `██` (two block chars) reads as roughly square. Account for this in any spatial layout — the snake.ts example uses `cellWidth = 2` for this reason.
### Color Discipline
**Always use pi's theme tokens.** Hardcoded ANSI escapes break when users switch themes.
```typescript
// ✗ Hardcoded — breaks on theme change
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
// ✓ Theme-aware — adapts to dark/light, custom themes
const header = theme.fg("accent", theme.bold("Title"));
const status = theme.fg("success", "✓ OK");
const muted = theme.fg("muted", "secondary info");
```
**Color hierarchy** (from the 51-token theme system):
- `accent` — primary attention, selections, active elements
- `text` — default body content (usually `""` = terminal default)
- `muted` — secondary, de-emphasized
- `dim` — tertiary, barely visible
- `success` / `error` / `warning` — semantic status
- `border` / `borderAccent` / `borderMuted` — structural elements
- `toolTitle` — headers in tool-like contexts
**Background colors** for regions: `selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`.
**Rule**: One accent color dominates. Use `muted`/`dim` for everything secondary. Overusing color flattens the hierarchy — a wall of green is worse than no color at all.
### Spatial Composition
The `width` parameter is your canvas edge. Every line from `render()` must not exceed it.
**Padding rhythm**: Consistent horizontal padding creates visual breathing room. `paddingX=1` (one space each side) is the baseline. Headers may deserve `paddingX=2`. Cramped UIs feel hostile.
**Alignment**: Right-align numbers, left-align labels. Use `visibleWidth()` to calculate ANSI-aware column widths. Pad with spaces, not tabs.
**Negative space**: An empty `Spacer(1)` between sections does more than a separator line. Let content breathe.
**Box nesting**: `Container > Box > [children]` gives you padding + background + vertical stacking. Don't flatten everything into one render function — compose components.
### Motion and Animation
No CSS transitions. Timer-based updates via `setInterval` + `tui.requestRender()`.
```typescript
// Spinner pattern (see Loader component)
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
this.interval = setInterval(() => {
this.frame = (this.frame + 1) % frames.length;
this.invalidate();
tui.requestRender();
}, 80);
```
**Patterns**: Braille spinners, block-element progress bars, staggered list reveals (render items one by one with delay), typewriter text, pulsing indicators.
**Cleanup is mandatory**: Clear intervals in `dispose()`. Leaked timers cause rendering after component removal.
### Keyboard Interaction Design
Keyboard is the only input. Make it discoverable and consistent.
**Standard conventions** (users expect these):
- `↑↓` or `j/k` — navigate lists
- `Enter` — confirm/select
- `Escape` — cancel/back
- `Tab` — next field
- `/` or start typing — search/filter
**Always show hints**: Footer line with available keys. Use `keyHint()` for theme-aware formatting.
```typescript
const help = theme.fg("dim", "↑↓ navigate • enter select • esc cancel");
```
**Use `matchesKey()` from pi-tui** — handles terminal escape sequence differences:
```typescript
import { matchesKey, Key } from "@mariozechner/pi-tui";
handleInput(data: string) {
if (matchesKey(data, Key.up)) { /* ... */ }
else if (matchesKey(data, Key.enter)) { /* ... */ }
else if (matchesKey(data, Key.escape)) { /* ... */ }
else if (matchesKey(data, Key.ctrl("c"))) { /* ... */ }
}
```
## The Component Contract
Every pi-tui component implements:
```typescript
interface Component {
render(width: number): string[]; // Lines of output, each ≤ width
handleInput?(data: string): void; // Keyboard input when focused
wantsKeyRelease?: boolean; // Kitty protocol key release events
invalidate(): void; // Clear cached render state
}
```
**Critical rules**:
1. Each line from `render()` must not exceed `width` — use `truncateToWidth()`
2. Call `invalidate()` when state changes, then `tui.requestRender()` to trigger re-render
3. Cache rendered output (`cachedLines`/`cachedWidth`) — re-compute only when state or width changes
4. Reapply styles per line — ANSI resets at line boundaries
### Caching Pattern
```typescript
private cachedWidth?: number;
private cachedLines?: string[];
private version = 0;
private cachedVersion = -1;
render(width: number): string[] {
if (this.cachedLines && this.cachedWidth === width && this.cachedVersion === this.version) {
return this.cachedLines;
}
// ... compute lines ...
this.cachedWidth = width;
this.cachedLines = lines;
this.cachedVersion = this.version;
return lines;
}
invalidate(): void {
this.cachedWidth = undefined;
this.cachedLines = undefined;
}
```
Increment `this.version` on state changes. Theme changes call `invalidate()` automatically.
### Theme Invalidation
If you pre-bake theme colors into child components, rebuild them on `invalidate()`:
```typescript
override invalidate(): void {
super.invalidate(); // Clear child render caches
this.rebuildContent(); // Re-apply current theme colors
}
```
## Available Components
### From `@mariozechner/pi-tui`
| Component | Purpose | Key API |
|-----------|---------|---------|
| `Text` | Multi-line word-wrapped text | `new Text(content, paddingX, paddingY, bgFn?)`, `.setText()` |
| `TruncatedText` | Single-line truncated text | `new TruncatedText(text, paddingX, paddingY)` |
| `Box` | Padded container with background | `new Box(paddingX, paddingY, bgFn)`, `.addChild()`, `.setBgFn()` |
| `Container` | Vertical stack of children | `.addChild()`, `.removeChild()`, `.clear()` |
| `Spacer` | Empty vertical space | `new Spacer(lines)` |
| `Markdown` | Rendered markdown with syntax highlighting | `new Markdown(content, paddingX, paddingY, mdTheme)` |
| `Image` | Terminal image (Kitty/iTerm2/Ghostty/WezTerm) | `new Image(base64, mimeType, theme, options)` |
| `SelectList` | Interactive list with filter/scroll | items, maxVisible, theme; `.onSelect`, `.onCancel` |
| `SettingsList` | Toggle settings with values | items, maxVisible, theme, onChange, onClose |
| `Loader` | Braille spinner with message | `new Loader(tui, spinnerColor, messageColor, message)` |
| `CancellableLoader` | Loader with escape-to-cancel | wraps Loader with abort signal |
| `Input` | Single-line text input | implements `Focusable` for IME cursor positioning |
| `Editor` | Multi-line text editor | full editing with undo, kill-ring, keybindings |
### From `@mariozechner/pi-coding-agent`
| Component | Purpose | Key API |
|-----------|---------|---------|
| `DynamicBorder` | Width-adaptive horizontal border | `new DynamicBorder((s: string) => theme.fg("accent", s))` |
| `BorderedLoader` | Loader with borders + cancel | `new BorderedLoader(tui, theme, message)`, `.signal`, `.onAbort` |
| `CustomEditor` | Editor with app keybindings baked in | Extend for modal editing (vim), custom shortcuts |
### Utilities
```typescript
import { visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
import { matchesKey, Key } from "@mariozechner/pi-tui";
import { DynamicBorder, getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
```
- `visibleWidth(str)` — display width ignoring ANSI escape codes
- `truncateToWidth(str, width, ellipsis?)` — truncate with optional ellipsis
- `wrapTextWithAnsi(str, width)` — word wrap preserving ANSI codes
- `matchesKey(data, key)` — compare keyboard input against key identifiers
- `getMarkdownTheme()` — theme object for `Markdown` component
- `keyHint(action, description)` — theme-aware keybinding hint text
## Delivery Surfaces
Choose the right surface for the interaction:
### Full-screen takeover — `ctx.ui.custom(component)`
For complex interactions: dashboards, games, multi-step wizards.
```typescript
await ctx.ui.custom((tui, theme, keybindings, done) => {
return new MyComponent(tui, theme, () => done(result));
});
```
### Overlay — `ctx.ui.custom(factory, { overlay: true })`
Floats on top of existing content. For quick selections, confirmations, panels.
```typescript
const result = await ctx.ui.custom<string | null>(
(tui, theme, kb, done) => new MyDialog(theme, done),
{
overlay: true,
overlayOptions: {
anchor: "center", // 9 positions: center, top-left, top-center, etc.
width: "50%", // number or percentage string
minWidth: 40,
maxHeight: "80%",
offsetX: -2, offsetY: 0,
margin: 2, // or { top, right, bottom, left }
visible: (w, h) => w >= 80, // responsive hide
},
}
);
```
### Widget — `ctx.ui.setWidget(id, lines|factory, options?)`
Persistent display above or below the editor. For status, progress, lists.
```typescript
ctx.ui.setWidget("my-widget", (tui, theme) => ({
render: () => [theme.fg("accent", "● Active") + " — 3 items pending"],
invalidate: () => {},
}));
// placement: "belowEditor" for below
// Clear: ctx.ui.setWidget("my-widget", undefined)
```
### Status line — `ctx.ui.setStatus(id, content)`
Single-line persistent indicator in footer.
```typescript
ctx.ui.setStatus("mode", theme.fg("accent", "● DESIGN"));
// Clear: ctx.ui.setStatus("mode", undefined)
```
### Tool rendering — `renderCall`/`renderResult`
Custom display for tool calls in the conversation. Return `Text` components with `(0, 0)` padding — the wrapping `Box` handles padding.
```typescript
renderCall(args, theme) {
return new Text(theme.fg("toolTitle", theme.bold("my_tool ")) + theme.fg("muted", args.action), 0, 0);
},
renderResult(result, { expanded, isPartial }, theme) {
GitHub에서 보기