ink — Terminal Rendering Engine

Terminal Rendering Engine

Custom Ink fork providing React-based terminal rendering. Implements a reconciler, output optimizer, and screen writer alongside a rich component library for building interactive terminal UIs.

96
Files
20k
Lines
TS
TypeScript

🔵 React Reconciler 🟢 Terminal Output 🟡 Focus Management 🔴 Custom Fork

📚 What is this?

The ink module is a custom fork of the Ink framework that turns React components into terminal output. Instead of rendering to a browser DOM, this reconciler targets ANSI escape sequences, producing styled text that updates efficiently in-place on any terminal emulator.

💡 Analogy

Imagine a theatre stage manager (the reconciler) who takes a script (React component tree), figures out which set pieces changed since the last scene (the optimizer), and instructs stagehands to move only the changed props (the render-to-screen writer). The audience (terminal) sees smooth transitions without the whole stage being rebuilt each frame.

Architecture

flowchart LR
  subgraph Reconciler["React Reconciler"]
    R1["createInstance"]
    R2["commitUpdate"]
    R3["removeChild"]
  end
  subgraph Optimizer["Output Optimizer"]
    O1["diffOutputs"]
    O2["minimizeEscapes"]
  end
  subgraph Writer["render-to-screen"]
    W1["buildANSI"]
    W2["stdout.write"]
  end
  subgraph Components["Component Library"]
    C1["Box"]
    C2["Text"]
    C3["Button / Link"]
    C4["Ansi / RawAnsi"]
    C5["Newline / Spacer"]
  end
  subgraph Input["Input System"]
    I1["parse-keypress"]
    I2["InputEvent"]
    I3["click detection"]
  end
  Components --> Reconciler
  R2 --> O1
  O1 --> O2
  O2 --> W1
  W1 --> W2
  Input --> Components
  I1 --> I2

Data Flow

flowchart TD
  JSX["JSX Tree"] --> RECONCILER["Reconciler"]
  RECONCILER --> VDOM["Virtual DOM"]
  VDOM --> DIFF["diffOutputs()"]
  DIFF --> OPT["minimizeEscapes()"]
  OPT --> ANSI["buildANSI()"]
  ANSI --> STDOUT["stdout.write()"]
  STDIN["stdin (raw)"] --> PARSE["parse-keypress"]
  PARSE --> EVENT["InputEvent"]
  EVENT --> FOCUS["FocusManager"]
  FOCUS --> JSX

🚶 Step-by-Step Walkthrough: Rendering a <Box> update

Trace how a prop change on a <Box> component reaches the screen:

  1. setState() — A hook or parent updates the Box's props (e.g., borderColor changes).
  2. Reconciler.commitUpdate — React reconciler detects the prop diff and schedules a commit.
  3. Virtual DOM rebuild — The component subtree produces a new virtual output buffer.
  4. diffOutputs() — Optimizer compares new buffer against the previous frame line-by-line.
  5. minimizeEscapes() — Coalesces adjacent ANSI sequences to reduce byte count.
  6. buildANSI() — Converts minimal diff into raw ANSI escape strings with cursor moves.
  7. stdout.write() — Writes the ANSI payload to the terminal in a single syscall.
  8. FocusManager — If Tab was pressed, focus ring shifts and triggers next re-render cycle.

Key Classes & Functions

SymbolFilePurpose
render()render.tsEntry point — creates Ink instance, mounts root component
createReconcilerreconciler.tsCustom React reconciler targeting terminal virtual DOM
renderToScreenrender-to-screen.tsConverts virtual DOM to ANSI and writes to stdout
diffOutputsoptimizer.tsLine-level diff between consecutive frames
Boxcomponents/Box.tsxFlexbox-like container with borders, padding, margin
Textcomponents/Text.tsxStyled text with color, bold, italic, underline, dimming
Ansi / RawAnsicomponents/Ansi.tsxPass-through for pre-formatted ANSI strings
parseKeypressinput/parse-keypress.tsConverts raw stdin bytes into structured key events
FocusManagerfocus/FocusManager.tsTab-order tracking and focus ring state
stringWidthutils/stringWidth.tsTerminal-aware string width (handles CJK, emoji)
wrapTextutils/wrapText.tsWord-wrapping respecting terminal column width
colorizeutils/colorize.tsMaps color names/hex to ANSI 256/truecolor codes

Directory Map

Source tree for ink
ink/
├── render.ts                    ← Entry point, Ink instance creation
├── reconciler.ts                ← Custom React reconciler
├── render-to-screen.ts          ← ANSI writer, cursor management
├── optimizer.ts                 ← Frame diff & escape minimizer
├── components/
│   ├── Box.tsx                  ← Flexbox container
│   ├── Text.tsx                 ← Styled text primitive
│   ├── Button.tsx               ← Clickable button component
│   ├── Link.tsx                 ← Hyperlink with OSC 8
│   ├── Ansi.tsx                 ← Raw ANSI pass-through
│   ├── Newline.tsx              ← Line break helper
│   └── Spacer.tsx               ← Flex spacer
├── input/
│   ├── parse-keypress.ts        ← Raw stdin → key event parser
│   ├── InputEvent.ts            ← Key event type definitions
│   └── click-detection.ts       ← Mouse click handling
├── focus/
│   └── FocusManager.ts          ← Tab-order focus ring
├── utils/
│   ├── stringWidth.ts           ← CJK/emoji-aware width calc
│   ├── wrapText.ts              ← Word-wrap engine
│   └── colorize.ts              ← Color name → ANSI mapper
├── hooks/
│   ├── useAnimationFrame.ts     ← rAF loop for terminal
│   └── useInterval.ts           ← setInterval with cleanup
└── index.ts                     ← Barrel export

Configuration

OptionDefaultDescription
patchConsoletrueIntercept console.log to prevent raw output mixing
exitOnCtrlCtrueAuto-exit process on Ctrl+C instead of event
columnsprocess.stdout.columnsOverride terminal width for layout
colorDepthautoForce color depth: 1, 4, 8, or 24-bit