Documentation Index
Fetch the complete documentation index at: https://mintlify.com/badlogic/pi-mono/llms.txt
Use this file to discover all available pages before exploring further.
The @mariozechner/pi-tui package provides a differential rendering system and utilities for terminal output.
Import
import {
TUI,
type Component,
type Terminal,
ProcessTerminal,
} from "@mariozechner/pi-tui";
TUI Class
Main rendering engine that manages the screen buffer and input.
Constructor
const tui = new TUI(terminal: Terminal)
Terminal interface (use ProcessTerminal for stdio)
Methods
render
Render a component to the terminal.
tui.render(component: Component): void
Uses differential rendering - only redraws changed lines.
showOverlay
Show an overlay component.
tui.showOverlay(
component: Component,
options?: OverlayOptions
): OverlayHandle
Component to show as overlay
Position anchor. Default: 'center'
Width (number or percentage like '50%')
Max height (number or percentage)
Horizontal offset from anchor
Vertical offset from anchor
Margin from terminal edges
Anchor Positions:
'center' - Center of screen
'top-left', 'top-center', 'top-right'
'bottom-left', 'bottom-center', 'bottom-right'
'left-center', 'right-center'
hideOverlay
Hide an overlay.
tui.hideOverlay(handle: OverlayHandle): void
focus
Focus a component (for keyboard input).
tui.focus(component: Component & Focusable): void
Add a global input listener.
tui.addInputListener(
listener: (data: string) => { consume?: boolean; data?: string } | undefined
): void
Return { consume: true } to prevent other listeners from receiving the input.
close
Clean up and restore terminal.
Component Interface
All components must implement this interface:
interface Component {
render(width: number): string[];
handleInput?(data: string): void;
wantsKeyRelease?: boolean;
invalidate(): void;
}
Render the component to an array of strings (one per line)
Handle keyboard input when focused
Whether component wants key release events. Default: false
Invalidate cached render state (called on theme changes)
Example: Custom Component
import type { Component } from "@mariozechner/pi-tui";
class HelloComponent implements Component {
private name: string;
constructor(name: string) {
this.name = name;
}
render(width: number): string[] {
return [`Hello, ${this.name}!`];
}
handleInput(data: string): void {
if (data === "\r") {
console.log("Enter pressed!");
}
}
invalidate(): void {
// No cached state to invalidate
}
}
Focusable Interface
Components that accept keyboard input should implement Focusable:
interface Focusable {
focused: boolean;
}
When focused, the component should emit CURSOR_MARKER at the cursor position:
import { CURSOR_MARKER } from "@mariozechner/pi-tui";
class MyInput implements Component, Focusable {
focused = false;
private text = "";
private cursor = 0;
render(width: number): string[] {
if (this.focused) {
// Insert cursor marker
const before = this.text.slice(0, this.cursor);
const after = this.text.slice(this.cursor);
return [before + CURSOR_MARKER + after];
}
return [this.text];
}
// ... rest of implementation
}
Terminal Interface
The Terminal interface abstracts terminal operations:
interface Terminal {
write(data: string): void;
getSize(): { rows: number; cols: number };
setRawMode(enabled: boolean): void;
onData(callback: (data: string) => void): void;
onResize(callback: () => void): void;
clear(): void;
}
ProcessTerminal
Default implementation for Node.js stdio:
import { ProcessTerminal } from "@mariozechner/pi-tui";
const terminal = new ProcessTerminal();
const tui = new TUI(terminal);
Rendering Utilities
visibleWidth
Calculate visible width of text (handles ANSI codes and Unicode).
import { visibleWidth } from "@mariozechner/pi-tui";
const width = visibleWidth("\x1b[31mRed text\x1b[0m");
// = 8 (ANSI codes not counted)
truncateToWidth
Truncate text to fit width.
import { truncateToWidth } from "@mariozechner/pi-tui";
const truncated = truncateToWidth("Very long text...", 10, "...");
// = "Very lo..."
wrapTextWithAnsi
Wrap text preserving ANSI codes.
import { wrapTextWithAnsi } from "@mariozechner/pi-tui";
const lines = wrapTextWithAnsi("Long \x1b[31mcolored\x1b[0m text", 10);
// = ["Long \x1b[31mcolor\x1b[0m", "\x1b[31med\x1b[0m text"]
Image Support
renderImage
Render an image using terminal image protocols.
import { renderImage } from "@mariozechner/pi-tui";
const lines = await renderImage({
data: base64Data,
mimeType: "image/png",
maxWidth: 80,
maxHeight: 24,
});
Supports iTerm2 and Kitty image protocols with automatic fallback.
detectCapabilities
Detect terminal capabilities.
import { detectCapabilities } from "@mariozechner/pi-tui";
const caps = await detectCapabilities();
console.log(caps.imageProtocol); // 'kitty' | 'iterm2' | null
console.log(caps.colorDepth); // 24 | 256 | 16
Differential Rendering
TUI uses differential rendering to minimize terminal writes:
- Component renders to string array
- TUI compares with previous frame
- Only changed lines are redrawn
- Cursor positioned efficiently
Render Caching
Components can cache render output:
class CachedComponent implements Component {
private cache: string[] | null = null;
render(width: number): string[] {
if (this.cache) return this.cache;
this.cache = this.computeRender(width);
return this.cache;
}
invalidate(): void {
this.cache = null;
}
private computeRender(width: number): string[] {
// Expensive rendering logic
return ["..."]
}
}
Example: Full Application
import {
TUI,
ProcessTerminal,
Container,
Box,
Text,
Editor,
} from "@mariozechner/pi-tui";
const terminal = new ProcessTerminal();
const tui = new TUI(terminal);
// Build UI
const root = new Container();
const header = new Box({ title: "Chat" });
header.addChild(new Text("Welcome to the chat!"));
root.addChild(header);
const editor = new Editor({ placeholder: "Type a message..." });
root.addChild(editor);
// Handle input
editor.on("submit", () => {
const message = editor.getText();
console.log(`Sent: ${message}`);
editor.clear();
});
// Render
tui.render(root);
tui.focus(editor);
// Handle resize
terminal.onResize(() => {
tui.render(root);
});
// Cleanup
process.on("SIGINT", () => {
tui.close();
process.exit(0);
});