JSPM

@mariozechner/pi-tui

0.5.2
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 2022577
  • Score
    100M100P100Q210186F
  • License MIT

Terminal User Interface library with differential rendering for efficient text-based applications

Package Exports

  • @mariozechner/pi-tui
  • @mariozechner/pi-tui/dist/index.js

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 differential rendering for building interactive CLI applications.

Features

  • Differential Rendering: Only re-renders content that has changed for optimal performance
  • Interactive Components: Text editor, autocomplete, selection lists, and markdown rendering
  • Composable Architecture: Container-based component system with proper lifecycle management
  • Text Editor Autocomplete System: File completion and slash commands with provider interface

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);
        ui.requestRender();
    }
};

// Start the UI
ui.start();

Core Components

TUI

Main TUI manager that handles rendering, input, and component coordination.

Methods:

  • addChild(component) - Add a component to the TUI
  • removeChild(component) - Remove a component from the TUI
  • setFocus(component) - Set which component receives keyboard input
  • start() - Start the TUI (enables raw mode)
  • stop() - Stop the TUI (disables raw mode)
  • requestRender() - Request a re-render on next tick
  • configureLogging(config) - Configure debug logging
  • cleanupSentinels() - Remove placeholder components after removal operations
  • findComponent(component) - Check if a component exists in the hierarchy (private)
  • findInContainer(container, component) - Search for component in container (private)

Container

Component that manages child components with differential rendering.

Constructor:

new Container(parentTui?: TUI | undefined)

Methods:

  • addChild(component) - Add a child component
  • removeChild(component) - Remove a child component
  • getChild(index) - Get a specific child component
  • getChildCount() - Get the number of child components
  • clear() - Remove all child components
  • setParentTui(tui) - Set the parent TUI reference
  • cleanupSentinels() - Clean up removed component placeholders
  • render(width) - Render all child components (returns ContainerRenderResult)

TextEditor

Interactive multiline text editor with cursor support and comprehensive keyboard shortcuts.

Constructor:

new TextEditor(config?: TextEditorConfig)

Configuration:

interface TextEditorConfig {
    // Configuration options for text editor
}

editor.configure(config: Partial<TextEditorConfig>)

Properties:

  • onSubmit?: (text: string) => void - Callback when user presses Enter
  • onChange?: (text: string) => void - Callback when text content changes

Methods:

  • getText() - Get current text content
  • setText(text) - Set text content and move cursor to end
  • setAutocompleteProvider(provider) - Set autocomplete provider for Tab completion
  • render(width) - Render the editor with current state
  • handleInput(data) - Process keyboard input

Keyboard Shortcuts:

Navigation:

  • Arrow Keys - Move cursor
  • Home / Ctrl+A - Move to start of line
  • End / Ctrl+E - Move to end of line

Editing:

  • Backspace - Delete character before cursor
  • Delete / Fn+Backspace - Delete character at cursor
  • Ctrl+K - Delete current line
  • Enter - Submit text (calls onSubmit)
  • Shift+Enter / Option+Enter - Add new line
  • Tab - Trigger autocomplete

Autocomplete (when active):

  • Tab - Apply selected completion
  • Arrow Up/Down - Navigate suggestions
  • Escape - Cancel autocomplete
  • Enter - Cancel autocomplete and submit

Paste Detection:

  • Automatically handles multi-line paste
  • Converts tabs to 4 spaces
  • Filters non-printable characters

TextComponent

Simple text component with automatic text wrapping and differential rendering.

Constructor:

new TextComponent(text: string, padding?: Padding)

interface Padding {
    top?: number;
    bottom?: number;
    left?: number;
    right?: number;
}

Methods:

  • setText(text) - Update the text content
  • getText() - Get current text content
  • render(width) - Render with word wrapping

Features:

  • Automatic text wrapping to fit terminal width
  • Configurable padding on all sides
  • Preserves line breaks in source text
  • Uses differential rendering to avoid unnecessary updates

MarkdownComponent

Renders markdown content with syntax highlighting and proper formatting.

Constructor:

new MarkdownComponent(text?: string)

Methods:

  • setText(text) - Update markdown content
  • render(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 selected
  • onCancel?: () => void - Called when selection is cancelled

Methods:

  • setFilter(filter) - Filter items by value
  • getSelectedItem() - Get currently selected item
  • handleInput(keyData) - Handle keyboard navigation
  • render(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:

  • Tab key 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 context
  • getForceFileSuggestions() - Force file completion (Tab key)
  • shouldTriggerFileCompletion() - Check if file completion should trigger
  • applyCompletion() - Apply selected completion

Differential Rendering

The core concept: components return {lines: string[], changed: boolean, keepLines?: number}:

  • lines: All lines the component should display
  • changed: Whether the component has changed since last render
  • keepLines: (Containers only) How many lines from the beginning are unchanged

How it works:

  1. TUI calculates total unchanged lines from top (keepLines)
  2. Moves cursor up by (totalLines - keepLines) positions
  3. Clears from cursor position down with \x1b[0J
  4. Prints only the changing lines: result.lines.slice(keepLines)

This approach minimizes screen updates and provides smooth performance even with large amounts of text.

Important: Don't add extra cursor positioning after printing - it interferes with terminal scrolling and causes rendering artifacts.

Advanced Examples

Chat Application with Autocomplete

import { TUI, Container, TextEditor, MarkdownComponent, CombinedAutocompleteProvider } from "@mariozechner/pi-tui";

const ui = new TUI();
const chatHistory = new Container();
const editor = new TextEditor();

// Set up autocomplete with slash commands
const autocompleteProvider = new CombinedAutocompleteProvider([
    { name: "clear", description: "Clear chat history" },
    { name: "help", description: "Show help information" },
    {
        name: "attach",
        description: "Attach a file",
        getArgumentCompletions: (prefix) => {
            // Return file suggestions for attach command
            return null; // Use default file completion
        },
    },
]);

editor.setAutocompleteProvider(autocompleteProvider);

editor.onSubmit = (text) => {
    // Handle slash commands
    if (text.startsWith("/")) {
        const [command, ...args] = text.slice(1).split(" ");
        if (command === "clear") {
            chatHistory.clear();
            return;
        }
        if (command === "help") {
            const help = new MarkdownComponent(`
## Available Commands
- \`/clear\` - Clear chat history
- \`/help\` - Show this help
- \`/attach <file>\` - Attach a file
            `);
            chatHistory.addChild(help);
            ui.requestRender();
            return;
        }
    }

    // Regular message
    const message = new MarkdownComponent(`**You:** ${text}`);
    chatHistory.addChild(message);

    // Add AI response (simulated)
    setTimeout(() => {
        const response = new MarkdownComponent(`**AI:** Response to "${text}"`);
        chatHistory.addChild(response);
        ui.requestRender();
    }, 1000);
};

ui.addChild(chatHistory);
ui.addChild(editor);
ui.setFocus(editor);
ui.start();

File Browser

import { TUI, SelectList } from "@mariozechner/pi-tui";
import { readdirSync, statSync } from "fs";
import { join } from "path";

const ui = new TUI();
let currentPath = process.cwd();

function createFileList(path: string) {
    const entries = readdirSync(path).map((entry) => {
        const fullPath = join(path, entry);
        const isDir = statSync(fullPath).isDirectory();
        return {
            value: entry,
            label: entry,
            description: isDir ? "directory" : "file",
        };
    });

    // Add parent directory option
    if (path !== "/") {
        entries.unshift({
            value: "..",
            label: "..",
            description: "parent directory",
        });
    }

    return entries;
}

function showDirectory(path: string) {
    ui.clear();

    const entries = createFileList(path);
    const fileList = new SelectList(entries, 10);

    fileList.onSelect = (item) => {
        if (item.value === "..") {
            currentPath = join(currentPath, "..");
            showDirectory(currentPath);
        } else if (item.description === "directory") {
            currentPath = join(currentPath, item.value);
            showDirectory(currentPath);
        } else {
            console.log(`Selected file: ${join(currentPath, item.value)}`);
            ui.stop();
        }
    };

    ui.addChild(fileList);
    ui.setFocus(fileList);
}

showDirectory(currentPath);
ui.start();

Multi-Component Layout

import { TUI, Container, TextComponent, TextEditor, MarkdownComponent } from "@mariozechner/pi-tui";

const ui = new TUI();

// Create layout containers
const header = new TextComponent("📝 Advanced TUI Demo", { bottom: 1 });
const mainContent = new Container();
const sidebar = new Container();
const footer = new TextComponent("Press Ctrl+C to exit", { top: 1 });

// Sidebar content
sidebar.addChild(new TextComponent("📁 Files:", { bottom: 1 }));
sidebar.addChild(new TextComponent("- config.json"));
sidebar.addChild(new TextComponent("- README.md"));
sidebar.addChild(new TextComponent("- package.json"));

// Main content area
const chatArea = new Container();
const inputArea = new TextEditor();

// Add welcome message
chatArea.addChild(
    new MarkdownComponent(`
# Welcome to the TUI Demo

This demonstrates multiple components working together:

- **Header**: Static title with padding
- **Sidebar**: File list (simulated)
- **Chat Area**: Scrollable message history
- **Input**: Interactive text editor
- **Footer**: Status information

Try typing a message and pressing Enter!
`),
);

inputArea.onSubmit = (text) => {
    if (text.trim()) {
        const message = new MarkdownComponent(`
**${new Date().toLocaleTimeString()}:** ${text}
        `);
        chatArea.addChild(message);
        ui.requestRender();
    }
};

// Build layout
mainContent.addChild(chatArea);
mainContent.addChild(inputArea);

ui.addChild(header);
ui.addChild(mainContent);
ui.addChild(footer);
ui.setFocus(inputArea);

// Configure debug logging
ui.configureLogging({
    enabled: true,
    level: "info",
    logFile: "tui-debug.log",
});

ui.start();

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;
}

Development

# Install dependencies (from monorepo root)
npm install

# Build the package
npm run build

# Run type checking
npm run check

Testing: Create a test file and run it with tsx:

# From packages/tui directory
npx tsx test/demo.ts

Special input keywords for simulation: "TAB", "ENTER", "SPACE", "ESC"

Debugging: Enable logging to see detailed component behavior:

ui.configureLogging({
    enabled: true,
    level: "debug", // "error" | "warn" | "info" | "debug"
    logFile: "tui-debug.log",
});

Check the log file to debug rendering issues, input handling, and component lifecycle.