Skip to main content

Components

The Pi-TUI component system provides a declarative API for building terminal user interfaces with differential rendering.

Component Interface

All TUI components must implement the Component interface.
interface Component {
  render(width: number): string[];
  handleInput?(data: string): void;
  wantsKeyRelease?: boolean;
  invalidate(): void;
}

Methods

render
(width: number) => string[]
required
Renders the component to an array of strings (one per line) for the given viewport width. Must respect the width constraint.
handleInput
(data: string) => void
Optional handler for keyboard input when component has focus. Receives raw terminal input data.
invalidate
() => void
required
Invalidates any cached rendering state. Called when theme changes or when component needs to re-render from scratch.

Properties

wantsKeyRelease
boolean
default:"false"
If true, component receives key release events (Kitty protocol). Default is false - release events are filtered out.

Focusable Interface

Components that can receive focus and display a hardware cursor implement the Focusable interface.
interface Focusable {
  focused: boolean;
}
focused
boolean
required
Set by TUI when focus changes. When true, component should emit CURSOR_MARKER at the cursor position in its render output.

Type Guard

function isFocusable(component: Component | null): component is Component & Focusable

Built-in Components

Container

Container component that renders children sequentially.
class Container implements Component {
  children: Component[];
  
  addChild(component: Component): void;
  removeChild(component: Component): void;
  clear(): void;
}

Example

import { Container, Text } from '@pi-ai/tui';

const container = new Container();
container.addChild(new Text('Header'));
container.addChild(new Text('Body'));
container.addChild(new Text('Footer'));

Text

Displays multi-line text with word wrapping and padding.
class Text implements Component {
  constructor(
    text?: string,
    paddingX?: number,
    paddingY?: number,
    customBgFn?: (text: string) => string
  );
  
  setText(text: string): void;
  setCustomBgFn(customBgFn?: (text: string) => string): void;
}

Parameters

text
string
default:"''"
Text content to display
paddingX
number
default:"1"
Left/right padding in columns
paddingY
number
default:"1"
Top/bottom padding in rows
customBgFn
(text: string) => string
Optional function to apply background color

Example

import { Text } from '@pi-ai/tui';
import chalk from 'chalk';

const text = new Text(
  'Hello, world!',
  2,  // 2 columns padding
  1,  // 1 row padding
  chalk.bgBlue  // Blue background
);

text.setText('Updated content');

Box

Container that applies padding and background to all children.
class Box implements Component {
  children: Component[];
  
  constructor(
    paddingX?: number,
    paddingY?: number,
    bgFn?: (text: string) => string
  );
  
  addChild(component: Component): void;
  removeChild(component: Component): void;
  clear(): void;
  setBgFn(bgFn?: (text: string) => string): void;
}

Example

import { Box, Text } from '@pi-ai/tui';
import chalk from 'chalk';

const box = new Box(2, 1, chalk.bgGray);
box.addChild(new Text('Boxed content'));

Input

Single-line text input with horizontal scrolling.
class Input implements Component, Focusable {
  focused: boolean;
  onSubmit?: (value: string) => void;
  onEscape?: () => void;
  
  getValue(): string;
  setValue(value: string): void;
  handleInput(data: string): void;
}

Example

import { Input } from '@pi-ai/tui';

const input = new Input();
input.onSubmit = (value) => {
  console.log('Submitted:', value);
};
input.onEscape = () => {
  console.log('Cancelled');
};

tui.setFocus(input);

Features

  • Emacs-style keybindings (configurable)
  • Bracketed paste support
  • Kill ring for cut/yank operations
  • Undo/redo support
  • Word navigation
  • Horizontal scrolling for long input

Loader

Animated spinner with customizable message.
class Loader extends Text {
  constructor(
    ui: TUI,
    spinnerColorFn: (str: string) => string,
    messageColorFn: (str: string) => string,
    message?: string
  );
  
  start(): void;
  stop(): void;
  setMessage(message: string): void;
}

Example

import { Loader } from '@pi-ai/tui';
import chalk from 'chalk';

const loader = new Loader(
  tui,
  chalk.cyan,
  chalk.gray,
  'Processing...'
);

loader.setMessage('Almost done...');
loader.stop();

SelectList

Scrollable list with keyboard navigation.
interface SelectItem {
  value: string;
  label: string;
  description?: string;
}

interface SelectListTheme {
  selectedPrefix: (text: string) => string;
  selectedText: (text: string) => string;
  description: (text: string) => string;
  scrollInfo: (text: string) => string;
  noMatch: (text: string) => string;
}

class SelectList implements Component {
  onSelect?: (item: SelectItem) => void;
  onCancel?: () => void;
  onSelectionChange?: (item: SelectItem) => void;
  
  constructor(
    items: SelectItem[],
    maxVisible: number,
    theme: SelectListTheme
  );
  
  setFilter(filter: string): void;
  setSelectedIndex(index: number): void;
  getSelectedItem(): SelectItem | null;
  handleInput(keyData: string): void;
}

Example

import { SelectList } from '@pi-ai/tui';
import chalk from 'chalk';

const items = [
  { value: 'start', label: 'Start Server', description: 'Launch the dev server' },
  { value: 'build', label: 'Build', description: 'Create production build' },
  { value: 'test', label: 'Test', description: 'Run test suite' },
];

const theme = {
  selectedPrefix: chalk.cyan,
  selectedText: chalk.cyan.bold,
  description: chalk.gray,
  scrollInfo: chalk.dim,
  noMatch: chalk.red,
};

const selectList = new SelectList(items, 5, theme);
selectList.onSelect = (item) => {
  console.log('Selected:', item.value);
};

tui.setFocus(selectList);

Cursor Marker

Focusable components should emit the CURSOR_MARKER constant at the cursor position when focused is true. The TUI will position the hardware cursor there for proper IME candidate window positioning.
const CURSOR_MARKER = '\x1b_pi:c\x07';  // Zero-width APC sequence

Example

import { CURSOR_MARKER, Focusable, Component } from '@pi-ai/tui';

class MyInput implements Component, Focusable {
  focused = false;
  private cursor = 0;
  private value = '';
  
  render(width: number): string[] {
    const marker = this.focused ? CURSOR_MARKER : '';
    const beforeCursor = this.value.slice(0, this.cursor);
    const afterCursor = this.value.slice(this.cursor);
    
    return [beforeCursor + marker + afterCursor];
  }
  
  invalidate() {}
}

Creating Custom Components

Implement the Component interface and optionally Focusable:
import { Component, Focusable, CURSOR_MARKER } from '@pi-ai/tui';
import { visibleWidth } from '@pi-ai/tui';

class CustomComponent implements Component, Focusable {
  focused = false;
  
  render(width: number): string[] {
    const lines: string[] = [];
    
    // Build content
    let line = 'Custom content';
    
    // Add cursor marker if focused
    if (this.focused) {
      line += CURSOR_MARKER;
    }
    
    // Ensure line doesn't exceed width
    if (visibleWidth(line) > width) {
      line = truncateToWidth(line, width);
    }
    
    lines.push(line);
    return lines;
  }
  
  handleInput(data: string): void {
    // Handle keyboard input
  }
  
  invalidate(): void {
    // Clear cached state
  }
}

Best Practices

  1. Always respect the width parameter - Lines exceeding terminal width will crash the TUI
  2. Use visibleWidth() to measure text - Accounts for ANSI codes and wide characters
  3. Implement efficient caching - Store rendered output and only regenerate when needed
  4. Call invalidate() on theme changes - Clear cached output with styled colors
  5. Emit CURSOR_MARKER when focused - Enables proper IME positioning

Build docs developers (and LLMs) love