Package Exports
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@mariozechner/pi-tui) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
@mariozechner/pi-tui
Terminal UI framework with surgical differential rendering for building flicker-free interactive CLI applications.
Features
- Surgical Differential Rendering: Three-strategy system that minimizes redraws to 1-2 lines for typical updates
- Scrollback Buffer Preservation: Correctly maintains terminal history when content exceeds viewport
- Zero Flicker: Components like text editors remain perfectly still while other parts update
- Interactive Components: Text editor with autocomplete, selection lists, markdown rendering
- Composable Architecture: Container-based component system with automatic lifecycle management
Quick Start
import { TUI, Container, TextComponent, TextEditor } from "@mariozechner/pi-tui";
// Create TUI manager
const ui = new TUI();
// Create components
const header = new TextComponent("🚀 My TUI App");
const chatContainer = new Container();
const editor = new TextEditor();
// Add components to UI
ui.addChild(header);
ui.addChild(chatContainer);
ui.addChild(editor);
// Set focus to the editor
ui.setFocus(editor);
// Handle editor submissions
editor.onSubmit = (text: string) => {
if (text.trim()) {
const message = new TextComponent(`💬 ${text}`);
chatContainer.addChild(message);
// Note: Container automatically calls requestRender when children change
}
};
// Start the UI
ui.start();Core Components
TUI
Main TUI manager with surgical differential rendering that handles input and component lifecycle.
Key Features:
- Three rendering strategies: Automatically selects optimal approach
- Surgical: Updates only changed lines (1-2 lines typical)
- Partial: Re-renders from first change when structure shifts
- Full: Complete re-render when changes are above viewport
- Performance metrics: Built-in tracking via
getLinesRedrawn()andgetAverageLinesRedrawn() - Terminal abstraction: Works with any Terminal interface implementation
Methods:
addChild(component)- Add a componentremoveChild(component)- Remove a componentsetFocus(component)- Set keyboard focusstart()/stop()- Lifecycle managementrequestRender()- Queue re-render (automatically debounced)configureLogging(config)- Enable debug logging
Container
Component that manages child components. Automatically triggers re-renders when children change.
const container = new Container();
container.addChild(new TextComponent("Child 1"));
container.removeChild(component);
container.clear();TextEditor
Interactive multiline text editor with autocomplete support.
const editor = new TextEditor();
editor.setText("Initial text");
editor.onSubmit = (text) => console.log("Submitted:", text);
editor.setAutocompleteProvider(provider);Key Bindings:
Enter- Submit textShift+Enter- New lineTab- AutocompleteCtrl+K- Delete lineCtrl+A/E- Start/end of line- Arrow keys, Backspace, Delete work as expected
TextComponent
Simple text display with automatic word wrapping.
const text = new TextComponent("Hello World", { top: 1, bottom: 1 });
text.setText("Updated text");MarkdownComponent
Renders markdown content with syntax highlighting and proper formatting.
Constructor:
new MarkdownComponent(text?: string)Methods:
setText(text)- Update markdown contentrender(width)- Render parsed markdown
Features:
- Headings: Styled with colors and formatting
- Code blocks: Syntax highlighting with gray background
- Lists: Bullet points (•) and numbered lists
- Emphasis: Bold and italic text
- Links: Underlined with URL display
- Blockquotes: Styled with left border
- Inline code: Highlighted with background
- Horizontal rules: Terminal-width separator lines
- Differential rendering for performance
SelectList
Interactive selection component for choosing from options.
Constructor:
new SelectList(items: SelectItem[], maxVisible?: number)
interface SelectItem {
value: string;
label: string;
description?: string;
}Properties:
onSelect?: (item: SelectItem) => void- Called when item is selectedonCancel?: () => void- Called when selection is cancelled
Methods:
setFilter(filter)- Filter items by valuegetSelectedItem()- Get currently selected itemhandleInput(keyData)- Handle keyboard navigationrender(width)- Render the selection list
Features:
- Keyboard navigation (arrow keys, Enter)
- Search/filter functionality
- Scrolling for long lists
- Custom option rendering with descriptions
- Visual selection indicator (→)
- Scroll position indicator
Autocomplete System
Comprehensive autocomplete system supporting slash commands and file paths.
AutocompleteProvider Interface
interface AutocompleteProvider {
getSuggestions(
lines: string[],
cursorLine: number,
cursorCol: number,
): {
items: AutocompleteItem[];
prefix: string;
} | null;
applyCompletion(
lines: string[],
cursorLine: number,
cursorCol: number,
item: AutocompleteItem,
prefix: string,
): {
lines: string[];
cursorLine: number;
cursorCol: number;
};
}
interface AutocompleteItem {
value: string;
label: string;
description?: string;
}CombinedAutocompleteProvider
Built-in provider supporting slash commands and file completion.
Constructor:
new CombinedAutocompleteProvider(
commands: (SlashCommand | AutocompleteItem)[] = [],
basePath: string = process.cwd()
)
interface SlashCommand {
name: string;
description?: string;
getArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;
}Features:
Slash Commands:
- Type
/to trigger command completion - Auto-completion for command names
- Argument completion for commands that support it
- Space after command name for argument input
File Completion:
Tabkey triggers file completion@prefix for file attachments- Home directory expansion (
~/) - Relative and absolute path support
- Directory-first sorting
- Filters to attachable files for
@prefix
Path Patterns:
./and../- Relative paths~/- Home directory@path- File attachment syntax- Tab completion from any context
Methods:
getSuggestions()- Get completions for current contextgetForceFileSuggestions()- Force file completion (Tab key)shouldTriggerFileCompletion()- Check if file completion should triggerapplyCompletion()- Apply selected completion
Surgical Differential Rendering
The TUI uses a three-strategy rendering system that minimizes redraws to only what's necessary:
Rendering Strategies
Surgical Updates (most common)
- When: Only content changes, same line counts, all changes in viewport
- Action: Updates only specific changed lines (typically 1-2 lines)
- Example: Loading spinner animation, updating status text
Partial Re-render
- When: Line count changes or structural changes within viewport
- Action: Clears from first change to end of screen, re-renders tail
- Example: Adding new messages to a chat, expanding text editor
Full Re-render
- When: Changes occur above the viewport (in scrollback buffer)
- Action: Clears scrollback and screen, renders everything fresh
- Example: Content exceeds viewport and early components change
How Components Participate
Components implement the simple Component interface:
interface ComponentRenderResult {
lines: string[]; // The lines to display
changed: boolean; // Whether content changed since last render
}
interface Component {
readonly id: number; // Unique ID for tracking
render(width: number): ComponentRenderResult;
handleInput?(keyData: string): void;
}The TUI tracks component IDs and line positions to determine the optimal strategy automatically.
Performance Metrics
Monitor rendering efficiency:
const ui = new TUI();
// After some rendering...
console.log(`Total lines redrawn: ${ui.getLinesRedrawn()}`);
console.log(`Average per render: ${ui.getAverageLinesRedrawn()}`);Typical performance: 1-2 lines redrawn for animations, 0 for static content.
Examples
Run the example applications in the test/ directory:
# Chat application with slash commands and autocomplete
npx tsx test/chat-app.ts
# File browser with navigation
npx tsx test/file-browser.ts
# Multi-component layout demo
npx tsx test/multi-layout.ts
# Performance benchmark with animation
npx tsx test/bench.tsExample Descriptions
- chat-app.ts - Chat interface with slash commands (/clear, /help, /attach) and autocomplete
- file-browser.ts - Interactive file browser with directory navigation
- multi-layout.ts - Complex layout with header, sidebar, main content, and footer
- bench.ts - Performance test with animation showing surgical rendering efficiency
Interfaces and Types
Core Types
interface ComponentRenderResult {
lines: string[];
changed: boolean;
}
interface ContainerRenderResult extends ComponentRenderResult {
keepLines: number;
}
interface Component {
render(width: number): ComponentRenderResult;
handleInput?(keyData: string): void;
}
interface Padding {
top?: number;
bottom?: number;
left?: number;
right?: number;
}Autocomplete Types
interface AutocompleteItem {
value: string;
label: string;
description?: string;
}
interface SlashCommand {
name: string;
description?: string;
getArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;
}
interface AutocompleteProvider {
getSuggestions(
lines: string[],
cursorLine: number,
cursorCol: number,
): {
items: AutocompleteItem[];
prefix: string;
} | null;
applyCompletion(
lines: string[],
cursorLine: number,
cursorCol: number,
item: AutocompleteItem,
prefix: string,
): {
lines: string[];
cursorLine: number;
cursorCol: number;
};
}Selection Types
interface SelectItem {
value: string;
label: string;
description?: string;
}Testing
Running Tests
# Run all tests
npm test
# Run specific test file
npm test -- test/tui-rendering.test.ts
# Run tests matching a pattern
npm test -- --test-name-pattern="preserves existing"Test Infrastructure
The TUI uses a VirtualTerminal for testing that provides accurate terminal emulation via @xterm/headless:
import { VirtualTerminal } from "./test/virtual-terminal.js";
import { TUI, TextComponent } from "../src/index.js";
test("my TUI test", async () => {
const terminal = new VirtualTerminal(80, 24);
const ui = new TUI(terminal);
ui.start();
ui.addChild(new TextComponent("Hello"));
// Wait for render
await new Promise(resolve => process.nextTick(resolve));
// Get rendered output
const viewport = await terminal.flushAndGetViewport();
assert.strictEqual(viewport[0], "Hello");
ui.stop();
});Writing a New Test
- Create test file in
test/directory with.test.tsextension - Use VirtualTerminal for accurate terminal emulation
- Key testing patterns:
import { test, describe } from "node:test";
import assert from "node:assert";
import { VirtualTerminal } from "./virtual-terminal.js";
import { TUI, Container, TextComponent } from "../src/index.js";
describe("My Feature", () => {
test("should handle dynamic content", async () => {
const terminal = new VirtualTerminal(80, 24);
const ui = new TUI(terminal);
ui.start();
// Setup components
const container = new Container();
ui.addChild(container);
// Initial render
await new Promise(resolve => process.nextTick(resolve));
await terminal.flush();
// Check viewport (visible content)
let viewport = terminal.getViewport();
assert.strictEqual(viewport.length, 24);
// Check scrollback buffer (all content including history)
let scrollBuffer = terminal.getScrollBuffer();
// Simulate user input
terminal.sendInput("Hello");
// Wait for processing
await new Promise(resolve => process.nextTick(resolve));
await terminal.flush();
// Verify changes
viewport = terminal.getViewport();
// ... assertions
ui.stop();
});
});VirtualTerminal API
new VirtualTerminal(columns, rows)- Create terminal with dimensionswrite(data)- Write ANSI sequences to terminalsendInput(data)- Simulate keyboard inputflush()- Wait for all writes to completegetViewport()- Get visible lines (what user sees)getScrollBuffer()- Get all lines including scrollbackflushAndGetViewport()- Convenience methodgetCursorPosition()- Get cursor row/columnresize(columns, rows)- Resize terminal
Testing Best Practices
Always flush after renders: Terminal writes are async
await new Promise(resolve => process.nextTick(resolve)); await terminal.flush();
Test both viewport and scrollback: Ensure content preservation
const viewport = terminal.getViewport(); // Visible content const scrollBuffer = terminal.getScrollBuffer(); // All content
Use exact string matching: Don't trim() - whitespace matters
assert.strictEqual(viewport[0], "Expected text"); // Good assert.strictEqual(viewport[0].trim(), "Expected"); // Bad
Test rendering strategies: Verify surgical vs partial vs full
const beforeLines = ui.getLinesRedrawn(); // Make change... const afterLines = ui.getLinesRedrawn(); assert.strictEqual(afterLines - beforeLines, 1); // Only 1 line changed
Performance Testing
Use test/bench.ts as a template for performance testing:
npx tsx test/bench.tsMonitor real-time performance metrics:
- Render count and timing
- Lines redrawn per render
- Visual verification of flicker-free updates
Development
# Install dependencies (from monorepo root)
npm install
# Run type checking
npm run check
# Run tests
npm testDebugging: Enable logging to see detailed component behavior:
ui.configureLogging({
enabled: true,
level: "debug", // "error" | "warn" | "info" | "debug"
logFile: "tui-debug.log",
});