ππ Agent
Chapter 17 / 21

The pi-tui Engine

Differential rendering with zero dependencies

Synchronized output for atomic frames; paste markers survive as atomic graphemes
No simulation scenario available for this chapter

Deep Dive

~6 min read·6360 chars

Differential rendering, zero dependencies — synchronous output creates flicker-free frames

Learning Objectives

After this chapter you will be able to:

  • Explain how differential rendering + DECSET 2026 synchronized output achieves zero-flicker frames
  • Describe pi-tui's zero-dependency strategy and its significance for Bun compilation
  • Explain how the editor handles IME, paste markers, and KillRing
  • Understand the advantages of the Kitty keyboard protocol over traditional terminal input

Key Source Files

FileLinesResponsibility
packages/tui/src/tui.ts1719Core engine: render loop, diffing, component tree
packages/tui/src/components/editor.ts2351Multi-line editor: wrap, IME, autocomplete, UndoStack
packages/tui/src/keys.ts1401Kitty keyboard protocol parsing
packages/tui/src/terminal.ts531Terminal abstraction: size, capability detection
packages/tui/src/autocomplete.ts786Autocomplete: slash commands + file paths

Design Motivation: Why Not Ink/Blessed?

Problem: The Node.js ecosystem has Ink (React for CLI) and Blessed. But pi's requirements are special:

  • Extremely high update frequency (streaming tokens at 50+ per second)
  • Zero dependencies (Bun single-file compilation)
  • Precise cursor control (multi-line editor + IME)
  • Synchronous output (no flicker allowed)

Ink's issue: Built on the React reconciler — diffs the entire virtual DOM each frame. Too slow for 50fps streaming. Blessed's issue: Depends on ncurses native bindings — Bun does not support native addons.

pi's solution: Build pi-tui from scratch, depending only on marked (Markdown parsing) and get-east-asian-width (CJK character width).

Analogy: Like GPU VSync — the frame buffer swap is atomic; the user never sees a half-drawn frame.

Differential Rendering + Synchronized Output

// tui.ts render loop (conceptual)
function render(root: Component) {
  const newFrame = compose(root)  // component tree → line array

  // Decide rendering strategy
  if (isFirstFrame || widthChanged || root.invalidated) {
    drawFull(newFrame)            // full redraw
  } else {
    drawDiff(prevFrame, newFrame) // only changed lines
  }

  prevFrame = newFrame
}

function drawDiff(prev: string[], next: string[]) {
  // ① Begin synchronized region (DECSET 2026)
  write("\x1b[?2026h")

  // ② Emit only changed lines
  for (let i = 0; i < next.length; i++) {
    if (next[i] !== prev[i]) {
      write(`\x1b[${i + 1};1H`)  // cursor positioning
      write(next[i])              // write that line
    }
  }

  // ③ End synchronized region → terminal flushes all at once
  write("\x1b[?2026l")
}

What is DECSET 2026? A terminal "double buffer" — all output between \x1b[?2026h and \x1b[?2026l is buffered by the terminal, displayed atomically at the end marker. Users see a complete frame, not a line-by-line tearing refresh.

Editor: 2351 Lines of Complexity

editor.ts feature matrix:
  ├── Multi-line input + word-wrap
  ├── IME support (CJK input methods)
  ├── Autocomplete
  │   ├── /slash command completion
  │   └── File path completion (Tab trigger)
  ├── KillRing (Emacs-style Ctrl+K/Y)
  ├── UndoStack (Ctrl+Z / Ctrl+Shift+Z)
  └── Paste markers
      └── Large paste → [paste #1 +50 lines]
          Treated as atomic grapheme — never split

Paste markers: When a user pastes 50 lines, the editor does not insert character-by-character (too slow). Instead it generates a [paste #1 +50 lines] marker. Internally this marker is an atomic grapheme — backspace deletes the entire marker, cursor jumps over it as one unit.

Kitty Keyboard Protocol

// keys.ts parses the Kitty protocol
type Key =
  | { type: "enter"; shift: boolean; alt: boolean }
  | { type: "char"; char: string; ctrl: boolean; alt: boolean }
  | { type: "named"; name: "tab" | "escape" | ...; shift: boolean }

// Traditional terminal: Ctrl+Shift+C and Ctrl+C are indistinguishable
// Kitty protocol: every keypress carries full modifier information
function parseKittySequence(seq: string): Key {
  // CSI 57345;2;1u → char='c', ctrl=true, shift=true
}

Why Kitty protocol? Traditional terminal key encoding is ambiguous — Tab and Ctrl+I produce the same byte. The Kitty protocol uses CSI u sequences to disambiguate, letting pi distinguish Ctrl+Enter (steer) from Alt+Enter (followUp).

Engineering Insights

  1. tui.ts is 1719 lines but the core render loop is only ~200 lines. The rest is component layout (flex-like), scroll region management, and focus system. Far simpler than React — no hooks, no fiber, no scheduler.

  2. The cost of zero dependencies: Everything is hand-written — ANSI escape sequence parsing, Unicode width calculation, terminal capability detection. But the payoff is: Bun compilation with no native dependencies, startup <50ms.

  3. Component caching: Each component caches its render result and only redraws after invalidate(). During streaming, only the "current message" component invalidates — all others have zero overhead.

Cross-Chapter Links

DirectionChapterRelationship
PrerequisiteCh16 Interactive TUIinteractive-mode uses pi-tui for rendering
Follow-upCh18 Settings & Trustoutput-guard cooperates with the render cycle
Follow-upCh13 ExtensionsExtensions can register custom render components
RelatedCh09 StreamingStreaming tokens drive high-frequency invalidate

Check Your Understanding

  1. If DECSET 2026 synchronized output were removed, what would users see during fast streaming output?
  2. Why must paste be treated as an "atomic grapheme"? What would backspace behavior look like otherwise?
  3. What are the trade-offs of pi-tui's zero-dependency choice? In what scenarios might this choice be inappropriate?

Takeaways

  • Differential rendering + DECSET 2026 synchronized output = zero-flicker frames
  • Zero dependencies (only marked + east-asian-width) — Bun compilation friendly
  • Paste as atomic grapheme; IME/KillRing/Undo fully supported
  • Kitty keyboard protocol eliminates key ambiguity

This chapter is based on the main-branch source of earendil-works/pi.