# Understanding pi-tui's Differential Rendering Architecture for Terminal UIs

> Explore pi-tui's differential rendering architecture. Learn how it efficiently updates terminal UIs by sending only necessary escape sequences for changed screen regions.

- Repository: [Mario Zechner/pi-mono](https://github.com/badlogic/pi-mono)
- Tags: architecture
- Published: 2026-02-15

---

**pi-tui implements incremental terminal rendering by maintaining a snapshot of previously drawn lines and emitting only the minimal escape sequences required to update changed screen regions.**

The `pi-tui` library, part of the `badlogic/pi-mono` repository, provides a high-performance terminal UI framework that minimizes CPU and bandwidth usage through its differential rendering architecture. Unlike full-screen redraws that flood the terminal with unnecessary data, pi-tui's line-based diff engine in [`packages/tui/src/tui.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/tui.ts) calculates precise updates, ensuring smooth interactions even over slow connections.

## How Differential Rendering Works in pi-tui

The rendering pipeline follows an 11-step process that transforms component trees into optimized terminal output. Each frame generation begins with `doRender()`, which orchestrates the differential update cycle.

### Frame Generation and Component Rendering

The process starts when `doRender()` invokes `this.render(width)` to collect line arrays from every mounted component. Each component returns an array of strings representing its visual output for the current terminal width.

```typescript
let newLines = this.render(width);

```

This initial collection captures the raw content before any overlay processing or cursor management occurs.

### Overlay Composition and Cursor Handling

Before diffing begins, visible overlays are composited onto the frame through `this.compositeOverlays(newLines, width, height)`. This ensures floating elements like modals or dropdowns merge seamlessly with underlying content.

Simultaneously, the engine extracts cursor positions via `this.extractCursorPosition(newLines, height)`. Components embed a special `CURSOR_MARKER` constant to indicate where the hardware cursor should appear—critical for IME (Input Method Editor) support and text input fields.

### Line Reset Application

To prevent ANSI escape sequence bleeding between frames, `this.applyLineResets(newLines)` appends reset sequences to each non-image line. This ensures that color or style attributes from previous frames don't inadvertently affect new content, maintaining visual consistency across differential updates.

## The Line-Based Diff Algorithm

The core optimization occurs in the diff calculation. Rather than comparing character-by-character, pi-tui performs a line-level diff that identifies the first and last indices where the old and new line arrays differ.

```typescript
for (let i = 0; i < maxLines; i++) { 
  // Comparison logic to find firstChanged and lastChanged
}

```

This approach balances computational efficiency with update granularity. The algorithm detects several edge cases:

- **Append-only updates**: New content added at the bottom of the screen
- **Viewport-top changes**: Modifications above the previous visible area requiring full redraws
- **Full-screen clears**: Triggered by width changes, first-draw scenarios, or screen shrinking

## Building the Minimal Update Buffer

Once the changed range is determined, pi-tui constructs a synchronized output block using the `2026` mode escape sequences (`\x1b[?2026h` to start, `\x1b[?2026l` to end). This tells modern terminals to buffer the entire update and render it atomically, preventing tearing or flickering.

Within this block, the engine:
1. Moves the cursor to the first changed line
2. Clears each modified line using `\x1b[2K` (erase entire line)
3. Writes the new line content

```typescript
let buffer = "\x1b[?2026h";
// ... cursor positioning and line clearing ...
buffer += "\x1b[2K";
buffer += line;

```

## Safety Mechanisms and Unicode Handling

Before writing any line, the engine verifies that `visibleWidth(line) <= width` using utilities from [`packages/tui/src/utils.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/utils.ts). If a line exceeds the terminal width, the system generates a crash log and throws a descriptive error, preventing display corruption.

The [`utils.ts`](https://github.com/badlogic/pi-mono/blob/main/utils.ts) module provides critical helper functions:
- `visibleWidth()`: Calculates display width accounting for wide Unicode characters and ANSI escape sequences
- `sliceByColumn()`: Safely truncates strings by visual columns rather than byte indices
- `extractSegments()`: Parses ANSI/OSC sequences to prevent them from interfering with width calculations

These utilities ensure that complex terminal content— including images via [`packages/tui/src/terminal-image.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/terminal-image.ts) and styled text—renders correctly without breaking the differential engine's assumptions.

## Practical Usage Example

The following example demonstrates how to initialize the TUI and trigger differential renders through the request-render handshake:

```typescript
import { Terminal } from "./terminal.js";
import { TUI, Container } from "./tui.js";
import { Text } from "./components/text.js";

// Create a low-level terminal wrapper
const term = new Terminal(process.stdin, process.stdout);

// Build a component tree
const root = new Container();
root.addChild(new Text("Welcome to pi-tui!"));
root.addChild(new Text("Press ^C to quit."));

// Initialize the TUI
const ui = new TUI(term);
ui.addChild(root);

// Start the render loop
ui.start();

// Dynamic updates trigger differential rendering
setInterval(() => {
  (root.children[0] as any).text = `Time: ${new Date().toLocaleTimeString()}`;
  ui.requestRender();  // Triggers the diff engine
}, 1000);

```

When `requestRender()` is called, the system executes `doRender()`, which runs through the 11-step differential process described above, ensuring only the changed time string updates on screen rather than redrawing the entire interface.

## Summary

- **pi-tui** implements a line-based differential rendering engine in [`packages/tui/src/tui.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/tui.ts) that minimizes terminal output by comparing new frames against cached previous lines.
- The **11-step pipeline** includes frame generation, overlay composition, cursor extraction, line reset application, diff calculation, and synchronized output buffer construction.
- **Edge case handling** detects full-render conditions (width changes, first draw, viewport shifts) and optimizes append-only updates.
- **Safety mechanisms** in [`packages/tui/src/utils.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/utils.ts) ensure Unicode and ANSI sequences don't corrupt width calculations or exceed terminal bounds.
- The architecture supports **hardware cursor positioning** for IME compatibility and **atomic screen updates** via the 2026 synchronized output mode.

## Frequently Asked Questions

### How does pi-tui handle wide Unicode characters in its differential renderer?

The engine uses the `visibleWidth()` function from [`packages/tui/src/utils.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/utils.ts) to calculate display width rather than byte length. This ensures that wide characters (such as CJK ideographs or emoji) occupy the correct number of terminal columns. Before writing any line, the system verifies that `visibleWidth(line) <= width`, throwing a descriptive error if content would overflow the terminal bounds.

### What triggers a full screen redraw instead of a differential update?

The `doRender()` method in [`packages/tui/src/tui.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/tui.ts) checks for three specific conditions that force a full render: terminal width changes since the last frame, the initial draw (`previousLines` is empty), or when the first changed line appears above the previous content viewport top. These conditions ensure visual consistency when the screen geometry changes or when scrolling reveals content outside the previously visible region.

### How does pi-tui prevent screen tearing during updates?

The renderer wraps all output in a **synchronized output block** using the `\x1b[?2026h` (begin) and `\x1b[?2026l` (end) escape sequences. This tells supported terminals to buffer the entire update sequence and render it atomically. Combined with line-based clearing (`\x1b[2K`) and precise cursor positioning, this ensures that users see complete frames rather than partially updated screens.

### Can pi-tui handle interactive elements like text input with IME support?

Yes, the architecture specifically supports interactive input through the `CURSOR_MARKER` system. Components embed this marker in their rendered lines, and `extractCursorPosition()` in [`packages/tui/src/tui.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/tui.ts) strips it while recording the coordinates. After flushing the differential buffer, `positionHardwareCursor()` moves the terminal's hardware cursor to this location, enabling proper IME positioning and text input feedback.