# How the TUI Differential Rendering System Works in pi-ai

> Discover how pi-ai's TUI differential rendering minimizes terminal output by efficiently updating only changed lines, not the whole screen. Optimize your terminal apps.

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

---

**The TUI differential rendering system in pi-ai minimizes terminal output by comparing each new frame against the previous one and emitting only the escape sequences necessary to update changed lines, rather than redrawing the entire screen.**

The terminal UI (TUI) powering *pi-ai*—found in the `badlogic/pi-mono` repository—implements a **single-pass, differential renderer** designed for sub-millisecond latency. By tracking the previous frame state, the system avoids the flickering and bandwidth overhead associated with full-screen clears, making it ideal for interactive command-line interfaces with rich text, images, and IME support.

## Architecture Overview

The rendering pipeline is orchestrated by the `TUI` class, which manages a tree of `Component` instances and coordinates the differential update cycle. Key modules include:

| Component | Responsibility | Source |
|-----------|----------------|--------|
| **`TUI`** (extends `Container`) | Holds the component tree, triggers render passes, manages overlay stacking, tracks cursor position, and determines when full redraws are necessary. | [`packages/tui/src/tui.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/tui.ts) |
| **`Component`** interface | Defines the contract for any renderable element; implementations return an array of strings representing lines for a given viewport width. | [`packages/tui/src/tui.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/tui.ts) |
| **[`utils.ts`](https://github.com/badlogic/pi-mono/blob/main/utils.ts)** | Provides width-aware string manipulation functions (`visibleWidth`, `sliceByColumn`, `extractSegments`) that correctly handle ANSI SGR codes, OSC hyperlinks, APC markers, wide Unicode characters, and image sequences. | [`packages/tui/src/utils.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/utils.ts) |
| **`Terminal`** | A thin wrapper around the underlying terminal device (or `@xterm/headless` in tests) that handles raw escape sequence I/O, cursor visibility, and size queries. | [`packages/tui/src/terminal.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/terminal.ts) |
| **Overlay stack** | A stack of modal components (pop-ups, tooltips) that are composited onto the base UI before the differential comparison runs. | `TUI.showOverlay`, `TUI.compositeOverlays` |

## The Rendering Cycle

The differential renderer operates in a strict 10-step pipeline each time `requestRender()` is invoked. This ensures that only the minimal set of terminal updates is emitted.

### Component Collection and Overlay Compositing

First, `TUI.render(width)` walks the component tree via `Container.render` and aggregates each child’s output into a string array called `newLines`. 

If overlays are active, `compositeOverlays` calculates each overlay’s screen position and merges its lines into the base content using `compositeLineAt`. This happens *before* the differential pass, ensuring that pop-ups participate in the same efficiency optimizations as base components.

*Source*: [`packages/tui/src/tui.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/tui.ts) lines 1899–1904 (tree render), lines 78–84 (layout), and lines 79–90 (compositing).

### Cursor Tracking and Line Resets

The system supports precise hardware cursor placement for IME (Input Method Editor) candidate windows. Components inject a special APC sequence—`\x1b_pi:c\x07` (defined as `CURSOR_MARKER`)—to mark the desired cursor location. 

`extractCursorPosition` scans the bottom-visible lines, locates the marker, strips it from the output, and returns `{row, col}` coordinates.

Before comparison, `applyLineResets` appends `\x1b[0m` to every non-image line to prevent SGR attribute bleeding between lines.

*Source*: [`packages/tui/src/tui.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/tui.ts) lines 282–289 (cursor extraction), lines 58–65 (line resets).

### Differential Comparison and Update Buffer

If no full-redraw condition is met (width unchanged, content not shrinking with `clearOnShrink` disabled, and not the first frame), the renderer performs a differential comparison.

It walks `previousLines` and `newLines` simultaneously to find `firstChanged` and `lastChanged` indices. Only the range between these indices is processed.

`computeLineDiff` builds the output buffer:
- Moves the cursor to the first changed line.
- Emits `\x1b[2K` (erase line) for each modified line.
- Writes the new line content, truncated to terminal width if necessary.

If new content extends beyond the previous line count, whole lines are appended without clearing.

*Source*: [`packages/tui/src/tui.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/tui.ts) lines 133–146 (diff detection), lines 158–215 (buffer building).

### Safety Guards and Hardware Cursor Placement

A **hard-stop safety guard** prevents terminal corruption: before emitting any line, the renderer checks `visibleWidth(line) > width`. If a component produces a line exceeding the terminal width, the system writes a crash log and throws an error.

After the update buffer is flushed, `positionHardwareCursor` moves the physical cursor to the coordinates extracted earlier (or hides it if no `CURSOR_MARKER` was found). This ensures IME pop-ups appear at the correct location relative to focused input components.

Finally, state variables (`previousLines`, `previousWidth`, `maxLinesRendered`, `hardwareCursorRow`) are updated for the next frame.

*Source*: [`packages/tui/src/tui.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/tui.ts) lines 250–267 (width guard), lines 514–527 (cursor placement), lines 534–545 (state update).

## Core Helper Functions

The differential renderer relies on ANSI-aware string utilities implemented in [`packages/tui/src/utils.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/utils.ts):

| Function | Purpose | Source |
|----------|---------|--------|
| `visibleWidth(str)` | Calculates column width accounting for ANSI SGR, OSC hyperlinks, APC markers, wide Unicode, and image sequences. | [`utils.ts`](https://github.com/badlogic/pi-mono/blob/main/utils.ts) lines 81–86 |
| `extractSegments(line, beforeEnd, afterStart, afterLen, strictAfter)` | Single-pass extraction of line segments while preserving SGR state; used by the overlay compositor to splice overlay lines into base content. | [`utils.ts`](https://github.com/badlogic/pi-mono/blob/main/utils.ts) lines 818–888 |
| `sliceByColumn(line, start, len, strict)` / `sliceWithWidth` | Safe column-based slicing that respects ANSI codes and optional strictness (prevents wide characters from crossing slice boundaries). | [`utils.ts`](https://github.com/badlogic/pi-mono/blob/main/utils.ts) lines 558–572 |
| `compositeLineAt(baseLine, overlayLine, startCol, overlayWidth, totalWidth)` | Inserts an overlay line into a base line, handling image lines, padding, and final width clamping. | [`tui.ts`](https://github.com/badlogic/pi-mono/blob/main/tui.ts) lines 71–90 |

## Practical Example

Below is a complete TypeScript example demonstrating how to instantiate the `TUI`, attach a custom `Component`, and trigger differential updates:

```typescript
import { TUI, Component } from "@badlogic/pi-mono/packages/tui";
import { VirtualTerminal } from "@badlogic/pi-mono/packages/tui/test/virtual-terminal";

// 1️⃣ Minimal component that renders a mutable string.
class Message implements Component {
  private text = "";
  render(_w: number): string[] { return [this.text]; }
  invalidate() {}                         // No caching in this toy example.
  handleInput?(data: string) {}           // No interactivity needed.
}

// 2️⃣ Create a headless terminal (useful for tests or scripts).
const term = new VirtualTerminal(80, 24);
const ui = new TUI(term);

// 3️⃣ Wire the component into the UI tree.
const msg = new Message();
ui.addChild(msg);

// 4️⃣ Start the UI loop.
ui.start();

// 5️⃣ Update the UI – only the changed line is sent.
msg["text"] = "Hello, pi‑ai! 🎉";
ui.requestRender();   // Triggers a differential render.

// 6️⃣ Later we change just a part of the line.
msg["text"] = "Hello, pi‑ai! 🎉 (v2)";
ui.requestRender();   // Only the changed portion is sent.

// 7️⃣ Clean up.
ui.stop();

```

When executed against a real terminal, this script updates the line without full-screen flicker. Inspecting the raw output reveals a single `\x1b[2K` (erase line) followed by the new text, rather than a complete screen clear.

## Summary

- **Single-pass differential rendering** compares the new frame against `previousLines` and emits only the escape sequences needed to update changed rows, minimizing terminal bandwidth and eliminating flicker.
- **Overlay compositing** occurs before the diff pass, allowing modal pop-ups to benefit from the same efficiency optimizations as base components.
- **Hardware cursor tracking** via the `CURSOR_MARKER` APC sequence enables precise IME support while maintaining the diff state machine.
- **Safety guards** enforce strict width constraints; any component producing lines wider than the terminal triggers a hard error to prevent display corruption.
- **ANSI-aware utilities** in [`utils.ts`](https://github.com/badlogic/pi-mono/blob/main/utils.ts) handle complex text measurements (SGR codes, wide Unicode, images) that make accurate differential updates possible.

## Frequently Asked Questions

### How does pi-ai handle terminal resizing?

When the terminal width changes, `TUI` detects `previousWidth !== width` and triggers a **full redraw**. This clears the entire screen and redraws all lines because line-wrapping behavior may have changed, making simple line-by-line diffs unsafe. The same full-clear logic applies when content shrinks and `clearOnShrink` is enabled.

### What happens if a component generates a line wider than the terminal?

The renderer implements a **hard-stop safety guard** in [`packages/tui/src/tui.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/tui.ts) (lines 250–267). Before emitting any line, it checks `visibleWidth(line) > width`. If true, the system writes a crash log and throws an error. This prevents terminal corruption caused by overflow or misaligned escape sequences.

### How are overlays like pop-ups rendered without breaking the diff algorithm?

Overlays are composited **before** the differential comparison runs. The `compositeOverlays` method calculates each overlay's screen position and merges its lines into the base content using `compositeLineAt` (lines 71–90 in [`tui.ts`](https://github.com/badlogic/pi-mono/blob/main/tui.ts)). Because overlays become part of the `newLines` array before the diff pass, the renderer treats them as ordinary content, ensuring that closing an overlay automatically restores the underlying UI via the standard differential update.

### Why does pi-ai use a special APC sequence for cursor tracking?

The `CURSOR_MARKER` (`\x1b_pi:c\x07`) allows any `Focusable` component to request a specific hardware cursor position without knowing its absolute screen coordinates. During the render pass, `extractCursorPosition` (lines 282–289) scans the output for this marker, removes it from the visible text, and returns the row/column. After the differential buffer is flushed, `positionHardwareCursor` moves the physical cursor to this location, enabling IME candidate windows to appear exactly where text is being edited while keeping the rendering pipeline stateless and diff-friendly.