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.
🔵 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:
- setState() — A hook or parent updates the Box's props (e.g., borderColor changes).
- Reconciler.commitUpdate — React reconciler detects the prop diff and schedules a commit.
- Virtual DOM rebuild — The component subtree produces a new virtual output buffer.
- diffOutputs() — Optimizer compares new buffer against the previous frame line-by-line.
- minimizeEscapes() — Coalesces adjacent ANSI sequences to reduce byte count.
- buildANSI() — Converts minimal diff into raw ANSI escape strings with cursor moves.
- stdout.write() — Writes the ANSI payload to the terminal in a single syscall.
- FocusManager — If Tab was pressed, focus ring shifts and triggers next re-render cycle.
Key Classes & Functions
| Symbol | File | Purpose |
|---|---|---|
render() | render.ts | Entry point — creates Ink instance, mounts root component |
createReconciler | reconciler.ts | Custom React reconciler targeting terminal virtual DOM |
renderToScreen | render-to-screen.ts | Converts virtual DOM to ANSI and writes to stdout |
diffOutputs | optimizer.ts | Line-level diff between consecutive frames |
Box | components/Box.tsx | Flexbox-like container with borders, padding, margin |
Text | components/Text.tsx | Styled text with color, bold, italic, underline, dimming |
Ansi / RawAnsi | components/Ansi.tsx | Pass-through for pre-formatted ANSI strings |
parseKeypress | input/parse-keypress.ts | Converts raw stdin bytes into structured key events |
FocusManager | focus/FocusManager.ts | Tab-order tracking and focus ring state |
stringWidth | utils/stringWidth.ts | Terminal-aware string width (handles CJK, emoji) |
wrapText | utils/wrapText.ts | Word-wrapping respecting terminal column width |
colorize | utils/colorize.ts | Maps 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
| Option | Default | Description |
|---|---|---|
patchConsole | true | Intercept console.log to prevent raw output mixing |
exitOnCtrlC | true | Auto-exit process on Ctrl+C instead of event |
columns | process.stdout.columns | Override terminal width for layout |
colorDepth | auto | Force color depth: 1, 4, 8, or 24-bit |