claude-code/spec/08_ink_terminal.md
2026-04-01 01:20:27 +05:30

1608 lines
75 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Claude Code — Ink Terminal Rendering System
## Overview
The ink directory contains a complete, custom terminal UI framework built on top of React. It is a heavily modified and extended fork of the open-source Ink library, tuned for Claude Code's requirements: fullscreen alternate-screen rendering, hardware-accelerated scroll, text selection, search highlighting, mouse tracking, bidirectional text, and fine-grained performance instrumentation.
The system can be summarized as a pipeline:
```
React tree
→ React Reconciler (reconciler.ts)
→ Virtual DOM (dom.ts)
→ Yoga layout engine (layout/)
→ Output buffer (output.ts)
→ Screen cell buffer (screen.ts)
→ Diff engine (log-update.ts)
→ Patch optimizer (optimizer.ts)
→ Terminal write (terminal.ts / termio/)
```
The top-level entry point for consumers is `/x/Bigger-Projects/Claude-Code/src/ink.ts`, which wraps the internal `root.ts` render and `createRoot` APIs with a mandatory `ThemeProvider`.
---
## File-by-File Reference
### `/x/Bigger-Projects/Claude-Code/src/ink.ts` — Public API Module
**Purpose:** The package-level façade that re-exports all public APIs from the ink subsystem. Wraps every `render()` and `createRoot()` call in a `ThemeProvider` so that `ThemedBox`/`ThemedText` components work at every call site without requiring consumers to mount the provider manually.
**Exports:**
- `render(node, options?)` — async, mounts a React tree wrapped in `ThemeProvider`; returns `Instance`
- `createRoot(options?)` — async; returns a `Root` whose `.render()` method also wraps in `ThemeProvider`
- `RenderOptions`, `Instance`, `Root` — type re-exports from `root.ts`
- `color` — from the design-system color module
- `Box`, `BoxProps` — themed box (design-system ThemedBox)
- `Text`, `TextProps` — themed text (design-system ThemedText)
- `ThemeProvider`, `usePreviewTheme`, `useTheme`, `useThemeSetting`
- `Ansi` — ANSI-string rendering component
- `BaseBox`, `BaseBoxProps` — raw ink Box without theme
- `BaseText`, `BaseTextProps` — raw ink Text without theme
- `Button`, `ButtonProps`, `ButtonState`
- `Link`, `LinkProps`
- `Newline`, `NewlineProps`
- `NoSelect`
- `RawAnsi`
- `Spacer`
- `DOMElement` — the virtual DOM element type
- `ClickEvent`, `EventEmitter`, `Event`, `Key`, `InputEvent`
- `TerminalFocusEvent`, `TerminalFocusEventType`
- `FocusManager`
- `FlickerReason`
- `useAnimationFrame`, `useApp`, `useInput`, `useAnimationTimer`, `useInterval`
- `useSelection`, `useStdin`, `useTabStatus`, `useTerminalFocus`
- `useTerminalTitle`, `useTerminalViewport`
- `measureElement`
- `supportsTabStatus`
- `wrapText`
---
### `/x/Bigger-Projects/Claude-Code/src/ink/constants.ts`
**Purpose:** Shared timing constant.
**Exports:**
- `FRAME_INTERVAL_MS = 16` — target frame interval (~60 fps), used by the throttled `scheduleRender`.
---
### `/x/Bigger-Projects/Claude-Code/src/ink/ink.tsx` — Core Ink Class
**Purpose:** The central orchestrator. `class Ink` owns the React fiber root, the yoga layout tree, the double-buffered screen, the focus manager, stdin/stdout event handlers, selection state, and the main render loop.
**Class `Ink`**
Constructor receives `Options`:
```typescript
type Options = {
stdout: NodeJS.WriteStream
stdin: NodeJS.ReadStream
stderr: NodeJS.WriteStream
exitOnCtrlC: boolean
patchConsole: boolean
waitUntilExit?: () => Promise<void>
onFrame?: (event: FrameEvent) => void
}
```
Key private state:
| Field | Type | Description |
|---|---|---|
| `log` | `LogUpdate` | Diff engine that writes ANSI to stdout |
| `terminal` | `Terminal` | `{ stdout, stderr }` write streams |
| `scheduleRender` | throttled fn | Throttled at 16 ms, leading+trailing, deferred via `queueMicrotask` |
| `container` | `FiberRoot` | React-reconciler fiber root (ConcurrentRoot mode) |
| `rootNode` | `DOMElement` | The `ink-root` DOM node |
| `focusManager` | `FocusManager` | DOM-style focus state machine |
| `renderer` | `Renderer` | `createRenderer()` closure |
| `stylePool` | `StylePool` | Session-lived ANSI style interning pool |
| `charPool` | `CharPool` | Session-lived character interning pool |
| `hyperlinkPool` | `HyperlinkPool` | Session-lived hyperlink URL interning pool |
| `frontFrame` / `backFrame` | `Frame` | Double-buffered screen frames |
| `selection` | `SelectionState` | Text selection for alt-screen mode |
| `searchHighlightQuery` | `string` | Current /search term |
| `searchPositions` | object or null | Pre-scanned match positions for current-match highlight |
| `altScreenActive` | `boolean` | Set by `<AlternateScreen>` |
| `prevFrameContaminated` | `boolean` | Forces full repaint next frame |
**Render loop (`onRender`):**
1. Run `createRenderer()` — walks DOM, runs yoga, fills back-buffer screen
2. Apply selection overlay (invert styled cells in `selection`)
3. Apply search highlight (invert cells matching `searchHighlightQuery`)
4. Apply positioned highlight (yellow/bold current-match via `searchPositions`)
5. Diff back-frame vs. front-frame via `log.render()``Patch[]`
6. Run `optimize(patches)` to merge/deduplicate
7. Call `writeDiffToTerminal()` to serialize patches and write ANSI to stdout
8. Emit `onFrame` event if wired
9. Swap front/back frames
**Alt-screen handling:**
- `setAltScreenActive(active, mouseTracking)` — called by `<AlternateScreen>` during insertion effects; enables BSU/ESU synchronized output, DECSTBM hardware scroll hints, and selection-aware repaints
- `resetFramesForAltScreen()` — replaces both frames with blank screens, sets `prevFrameContaminated = true`
- `reenterAltScreen()` — re-asserts alt-screen state on SIGCONT
**Resize handling (`handleResize`):**
- Synchronous (no debounce) to keep `terminalColumns`/`terminalRows` and yoga in sync
- For alt-screen: resets frame buffers and sets `needsEraseBeforePaint = true` so the erase happens atomically inside the next BSU/ESU block
**Console patching:**
- `patchConsole()` — intercepts `console.log/warn/error` so they write to a separate file descriptor (not stdout), preventing output mixing
- `patchStderr()` — same for stderr
**Key public methods:**
- `render(node)` — calls `reconciler.updateContainer()`
- `unmount()` — graceful teardown: restore console, disable mouse tracking, exit alt screen, write final frame, free yoga nodes
- `waitUntilExit()` — returns a promise resolved on `unmount()`
- `clearTextSelection()` — clear selection state and force repaint
- `setSearchHighlight(query)` — set live search term
- `setSearchPositions(positions, rowOffset, currentIdx)` — set positioned highlights for search navigation
---
### `/x/Bigger-Projects/Claude-Code/src/ink/root.ts` — Public Entry Points
**Purpose:** Wraps `Ink` in the public `render()` and `createRoot()` APIs; manages the `instances` map so repeated calls to `render()` reuse the same `Ink` instance for the same stdout stream.
**Exports:**
```typescript
type RenderOptions = {
stdout?: NodeJS.WriteStream
stdin?: NodeJS.ReadStream
stderr?: NodeJS.WriteStream
exitOnCtrlC?: boolean
patchConsole?: boolean
onFrame?: (event: FrameEvent) => void
}
type Instance = {
rerender: Ink['render']
unmount: Ink['unmount']
waitUntilExit: Ink['waitUntilExit']
cleanup: () => void
}
type Root = {
render: (node: ReactNode) => void
unmount: () => void
waitUntilExit: () => Promise<void>
}
export const renderSync(node, options?): Instance // synchronous mount
export default async function render(node, options?): Promise<Instance>
export async function createRoot(options?): Promise<Root>
```
`renderSync` is used internally; the public-facing `render` and `createRoot` are the async versions exported through `ink.ts`.
---
### `/x/Bigger-Projects/Claude-Code/src/ink/instances.ts`
**Purpose:** Module-level singleton map keyed by `NodeJS.WriteStream`. Ensures one `Ink` instance per stdout stream.
```typescript
const instances = new Map<NodeJS.WriteStream, Ink>()
export default instances
```
---
## DOM Layer
### `/x/Bigger-Projects/Claude-Code/src/ink/dom.ts` — Virtual DOM
**Purpose:** Defines the virtual DOM node types and all mutation operations. Mirrors a minimal browser DOM API adapted for a terminal. Every mutation marks the affected node and all ancestors `dirty` so the render pass can skip clean subtrees.
**Element name types:**
```typescript
type ElementNames =
| 'ink-root' // Document root; owns FocusManager
| 'ink-box' // Flex container (yoga node)
| 'ink-text' // Leaf text container (yoga measure func)
| 'ink-virtual-text' // Inline text, no yoga node
| 'ink-link' // OSC 8 hyperlink wrapper, no yoga node
| 'ink-progress' // Terminal progress indicator, no yoga node
| 'ink-raw-ansi' // Pre-rendered ANSI, yoga node with rawWidth/rawHeight
type TextName = '#text'
type NodeNames = ElementNames | TextName
```
**`DOMElement` structure:**
```typescript
type DOMElement = {
nodeName: ElementNames
attributes: Record<string, DOMNodeAttribute>
childNodes: DOMNode[]
textStyles?: TextStyles
onComputeLayout?: () => void // Called by reconciler.resetAfterCommit
onRender?: () => void // Points to throttled scheduleRender
onImmediateRender?: () => void // Synchronous, for tests
hasRenderedContent?: boolean // React 19 test-mode guard
dirty: boolean
isHidden?: boolean
_eventHandlers?: Record<string, unknown> // Event handlers (separated from attrs)
// Scroll state (overflow: scroll boxes):
scrollTop?: number
pendingScrollDelta?: number
scrollClampMin?: number
scrollClampMax?: number
scrollHeight?: number
scrollViewportHeight?: number
scrollViewportTop?: number
stickyScroll?: boolean
scrollAnchor?: { el: DOMElement; offset: number }
focusManager?: FocusManager // Only on ink-root
debugOwnerChain?: string[] // CLAUDE_CODE_DEBUG_REPAINTS mode
yogaNode?: LayoutNode
style: Styles
parentNode: DOMElement | undefined
}
```
**Exported functions:**
- `createNode(nodeName)` — allocates a `DOMElement`; attaches a yoga measure function for `ink-text` and `ink-raw-ansi`
- `createTextNode(text)` — allocates a `TextNode`
- `appendChildNode(node, childNode)` — appends child, syncs yoga tree, marks dirty
- `insertBeforeNode(node, newChild, beforeChild)` — inserts before; yoga index computed separately from DOM index because some nodes lack yoga nodes
- `removeChildNode(node, removeNode)` — removes child, collects `pendingClears`, marks dirty
- `setAttribute(node, key, value)` — sets attribute only if changed; skips `children`
- `setStyle(node, style)` — shallow-compares style objects; skips if unchanged
- `setTextStyles(node, textStyles)` — shallow-compares; skips if unchanged
- `setTextNodeValue(node, text)` — updates text, marks dirty
- `markDirty(node?)` — walks the ancestor chain setting `dirty = true`; marks yoga dirty on `ink-text`/`ink-raw-ansi` leaf nodes
- `scheduleRenderFrom(node?)` — walks to root and calls `onRender()`
- `clearYogaNodeReferences(node)` — recursively clears `yogaNode` pointers (call before `freeRecursive()`)
- `findOwnerChainAtRow(root, y)` — DFS to find the React component stack covering screen row `y` (debug repaints mode)
**Dirty-checking optimization:** `stylesEqual` and `shallowEqual` prevent marking dirty when React creates a new style object with identical values on every render.
**Yoga index vs DOM index:** Nodes like `ink-virtual-text`, `ink-link`, and `ink-progress` have no yoga node. `insertBeforeNode` counts only yoga-equipped children to compute the correct yoga insertion index.
---
## Reconciler
### `/x/Bigger-Projects/Claude-Code/src/ink/reconciler.ts`
**Purpose:** Configures `react-reconciler` to use the ink virtual DOM as the host environment. This is the bridge between React's fiber tree and the ink DOM tree.
**Reconciler type parameters:**
```typescript
createReconciler<
ElementNames, // Type
Props, // Props
DOMElement, // Container
DOMElement, // Instance
TextNode, // TextInstance
DOMElement, // SuspenseInstance
unknown, // HydratableInstance
unknown, // PublicInstance
DOMElement, // HostContext (root)
HostContext, // ChildSet
null, // UpdatePayload (unused in React 19)
NodeJS.Timeout, // TimeoutHandle
-1, // NoTimeout
null // TransitionStatus
>
```
**Key reconciler methods:**
| Method | Behavior |
|---|---|
| `createInstance(type, props, rootContainer, context, fiber)` | Calls `createNode(type)`; applies all props via `applyProp`; optionally captures `debugOwnerChain` from fiber |
| `createTextInstance(text)` | Calls `createTextNode(text)` |
| `appendInitialChild` / `appendChild` | Calls `appendChildNode` |
| `insertBefore` | Calls `insertBeforeNode` |
| `removeChild` | Calls `removeChildNode`; notifies `focusManager.handleNodeRemoved` |
| `commitUpdate(instance, updatePayload, type, oldProps, newProps)` | Diffs old/new props and applies changes; uses `diff()` to find changed keys |
| `commitTextUpdate(textInstance, oldText, newText)` | Calls `setTextNodeValue` |
| `hideInstance(instance)` / `unhideInstance(instance)` | Sets `isHidden` and `LayoutDisplay.None` / restores display |
| `prepareForCommit` | Records timing start |
| `resetAfterCommit(rootNode)` | Records commit duration; calls `onComputeLayout()` (yoga layout); triggers `onImmediateRender` in test mode; calls `onRender()` in production |
| `commitMount(instance, type, props)` | Calls `focusManager.focus()` if `autoFocus` prop set |
**`applyProp(node, key, value)`:** Routes to `setStyle` (key=`style`), `setTextStyles` (key=`textStyles`), `setEventHandler` (key in `EVENT_HANDLER_PROPS`), or `setAttribute`.
**Event handler separation:** Event handler props are stored in `node._eventHandlers` rather than `node.attributes`. This prevents handler identity changes from marking the node dirty and defeating the blit optimization.
**`getOwnerChain(fiber)`:** Walks the React fiber's `_debugOwner` / `return` chain to collect component names. Used for `CLAUDE_CODE_DEBUG_REPAINTS` mode to attribute flicker to source components.
**Profiling exports:**
- `recordYogaMs(ms)` / `getLastYogaMs()` — yoga layout timing
- `markCommitStart()` / `getLastCommitMs()` — React commit timing
- `resetProfileCounters()`
- `dispatcher` — the `Dispatcher` instance (event dispatch)
- `isDebugRepaintsEnabled()` — reads `CLAUDE_CODE_DEBUG_REPAINTS` env var once
---
## Layout Engine
### `/x/Bigger-Projects/Claude-Code/src/ink/layout/node.ts` — Layout Node Interface
**Purpose:** Abstract interface for a layout node. Decouples the ink DOM from the concrete Yoga WASM implementation.
**Constants:**
```typescript
LayoutEdge: { All, Horizontal, Vertical, Left, Right, Top, Bottom, Start, End }
LayoutGutter: { All, Column, Row }
LayoutDisplay: { Flex, None }
LayoutFlexDirection: { Row, RowReverse, Column, ColumnReverse }
LayoutAlign: { Auto, Stretch, FlexStart, Center, FlexEnd }
LayoutJustify: { FlexStart, Center, FlexEnd, SpaceBetween, SpaceAround, SpaceEvenly }
LayoutWrap: { NoWrap, Wrap, WrapReverse }
LayoutPositionType: { Relative, Absolute }
LayoutOverflow: { Visible, Hidden, Scroll }
LayoutMeasureMode: { Undefined, Exactly, AtMost }
```
**`LayoutNode` interface** (abbreviated):
```typescript
interface LayoutNode {
// Tree operations
insertChild(child, index): void
removeChild(child): void
getChildCount(): number
getParent(): LayoutNode | null
// Layout computation
calculateLayout(width?, height?): void
setMeasureFunc(fn: LayoutMeasureFunc): void
unsetMeasureFunc(): void
markDirty(): void
// Layout reading (post-layout)
getComputedLeft(): number
getComputedTop(): number
getComputedWidth(): number
getComputedHeight(): number
getComputedBorder(edge): number
getComputedPadding(edge): number
// Style setters (width, height, min/max, flex*, align*, justify, display,
// position, overflow, margin, padding, border, gap)
// Lifecycle
free(): void
freeRecursive(): void
}
```
### `/x/Bigger-Projects/Claude-Code/src/ink/layout/yoga.ts` — Yoga Adapter
**Purpose:** Implements `LayoutNode` by wrapping the native Yoga WASM node (`src/native-ts/yoga-layout`). Maps `LayoutEdge`/`LayoutGutter`/etc. string enums to Yoga enum values.
**Class `YogaLayoutNode`:**
- Holds a `YogaNode` (`this.yoga`)
- `setMeasureFunc`: wraps the `LayoutMeasureFunc` to translate `MeasureMode` enum values
- `calculateLayout(width)`: calls `this.yoga.calculateLayout(width, undefined, Direction.LTR)` — height is always undefined (intrinsic)
Edge/gutter enum maps are static `EDGE_MAP` and `GUTTER_MAP` objects.
### `/x/Bigger-Projects/Claude-Code/src/ink/layout/engine.ts`
**Purpose:** Factory for layout nodes. A one-line indirection: `createLayoutNode()` calls `createYogaLayoutNode()`. Allows swapping the layout backend without changing the DOM layer.
### `/x/Bigger-Projects/Claude-Code/src/ink/layout/geometry.ts`
**Purpose:** 2D geometry primitives used throughout the rendering pipeline.
**Exports:**
```typescript
type Point = { x: number; y: number }
type Size = { width: number; height: number }
type Rectangle = Point & Size
type Edges = { top: number; right: number; bottom: number; left: number }
edges(all): Edges
edges(v, h): Edges
edges(t, r, b, l): Edges
addEdges(a, b): Edges
resolveEdges(partial?): Edges
ZERO_EDGES: Edges
unionRect(a, b): Rectangle // bounding union
clampRect(rect, size): Rectangle
withinBounds(size, point): boolean
clamp(value, min?, max?): number
```
---
## Styles
### `/x/Bigger-Projects/Claude-Code/src/ink/styles.ts`
**Purpose:** TypeScript types for box/text styles plus `applyStyles()` which translates style props onto a `LayoutNode`.
**Color types:**
```typescript
type RGBColor = `rgb(${number},${number},${number})`
type HexColor = `#${string}`
type Ansi256Color = `ansi256(${number})`
type AnsiColor = 'ansi:black' | 'ansi:red' | ... // 16 named colors
type Color = RGBColor | HexColor | Ansi256Color | AnsiColor
```
**`TextStyles`:** `{ color?, backgroundColor?, dim?, bold?, italic?, underline?, strikethrough?, inverse? }` — applied during rendering via chalk/colorize; not yoga properties.
**`Styles`:** The complete set of layout and text style props including:
- `textWrap`: `'wrap' | 'wrap-trim' | 'end' | 'middle' | 'truncate-end' | 'truncate' | 'truncate-middle' | 'truncate-start'`
- `position`: `'absolute' | 'relative'`
- `top | bottom | left | right`: `number | '${number}%'`
- `columnGap | rowGap | gap`: number
- `margin | marginX | marginY | marginTop | marginBottom | marginLeft | marginRight`: number
- `padding | paddingX | paddingY | paddingTop | paddingBottom | paddingLeft | paddingRight`: number
- `flexGrow | flexShrink | flexBasis`: number
- `flexDirection`: `'row' | 'row-reverse' | 'column' | 'column-reverse'`
- `flexWrap`: `'wrap' | 'nowrap' | 'wrap-reverse'`
- `alignItems | alignSelf | justifyContent`
- `width | height | minWidth | minHeight | maxWidth | maxHeight`: number or `'${number}%'` or `'100%'`
- `display`: `'flex' | 'none'`
- `overflow | overflowX | overflowY`: `'visible' | 'hidden' | 'scroll'`
- `borderStyle`: `BorderStyle`
- `borderColor | borderTopColor | borderRightColor | borderBottomColor | borderLeftColor`: Color
- `borderDimColor | ...`: boolean
- `borderTop | borderRight | borderBottom | borderLeft`: boolean
- `color | backgroundColor | dimColor | bold | italic | underline | strikethrough | inverse`
**`applyStyles(yogaNode, styles)`:** Translates each Styles property to `yogaNode.setXxx()` calls. Percentage values use `setWidthPercent`, etc. Position/overflow/display use enum mapping.
---
## Screen Buffer
### `/x/Bigger-Projects/Claude-Code/src/ink/screen.ts`
**Purpose:** The core cell-based screen buffer. Stores the rendered content as a 2D grid of cells. Each cell is packed into two 32-bit integers for memory efficiency.
**Cell encoding (packed as two `Int32Array` elements per cell):**
- Word 0 (low 32 bits): `charId` (high 22 bits) | `styleId` (low 10 bits)
- Word 1 (high 32 bits): `hyperlinkId` (high 16 bits) | `width` (2 bits) | flags
`CellWidth` enum: `Single = 0`, `Wide = 1`, `SpacerTail = 2`, `SpacerHead = 3`
**Pools (shared across all screens for zero-allocation diffing):**
`CharPool`:
- `intern(char)`: returns a stable integer ID; ASCII chars use a fast `Int32Array` lookup; others use a `Map`
- `get(id)`: retrieves the string
- Pool index 0 = space, index 1 = empty (spacer cell)
`HyperlinkPool`:
- `intern(hyperlink?)`: returns 0 for no hyperlink
- `get(id)`: returns the URL string or `undefined`
`StylePool`:
- `intern(styles: AnsiCode[])`: returns a tagged integer ID; bit 0 = `VISIBLE_ON_SPACE` flag (background/inverse/underline affect spaces)
- `get(id)`: strips bit-0 flag and returns `AnsiCode[]`
- `transition(fromId, toId)`: returns the cached ANSI transition string (pre-serialized diff)
- `withInverse(baseId)`: returns ID of style with SGR 7 (inverse) added
- `withCurrentMatch(baseId)`: returns ID of style with inverse + bold + yellow-fg + underline (current search match)
- `withSelectionBg(baseId)`: returns ID of style with selection background color applied
- `setSelectionBg(bg)`: sets the selection background `AnsiCode` (clears cache)
**`Screen` type:**
```typescript
type Screen = {
cells: Int32Array // packed cell data, width * height * 2 words per cell
width: number
height: number
charPool: CharPool
stylePool: StylePool
hyperlinkPool: HyperlinkPool
softWrap: Uint8Array // 1 bit per row; 1 = soft-wrapped continuation
noSelect: Uint8Array // 1 bit per cell; set on gutter/non-selectable regions
}
```
**Key functions:**
- `createScreen(width, height, stylePool, charPool, hyperlinkPool)` — allocates a new screen
- `resetScreen(screen)` — zeroes all cells
- `setCellAt(screen, x, y, char, styleId, width, hyperlink?)` — writes a cell
- `cellAt(screen, x, y)` — reads a cell
- `cellAtIndex(screen, idx)` — reads cell at raw index
- `isEmptyCellAt(screen, x, y)` — both packed words = 0
- `blitRegion(dst, src, srcRect, dstX, dstY)` — bulk-copy a rectangle from one screen to another (pure `Int32Array` copy, no decoding)
- `shiftRows(screen, top, bottom, delta)` — hardware scroll simulation: move rows up/down within bounds
- `markNoSelectRegion(screen, x, y, w, h)` — set the noSelect bit on a rectangular region
- `diffEach(prev, next, callback)` — iterate only changed cells between two screens
- `migrateScreenPools(screen, charPool, hyperlinkPool)` — re-intern all cells when pools are replaced (generational reset)
---
## Rendering Pipeline
### `/x/Bigger-Projects/Claude-Code/src/ink/renderer.ts`
**Purpose:** Creates a `Renderer` function (a closure over the root DOM node and `StylePool`) that converts the virtual DOM into a `Frame` object on each call.
**`Renderer` type:** `(options: RenderOptions) => Frame`
**`RenderOptions`:**
```typescript
{
frontFrame: Frame
backFrame: Frame
isTTY: boolean
terminalWidth: number
terminalRows: number
altScreen: boolean
prevFrameContaminated: boolean
}
```
**Algorithm:**
1. Check yoga computed dimensions; return empty frame if invalid
2. For alt-screen: clamp `height` to `terminalRows` (enforces the invariant)
3. Reuse or create `Output` with the back-buffer screen
4. Reset `layoutShifted`, `scrollHint`, `scrollDrainNode`
5. If `prevFrameContaminated` or an absolute node was removed: disable blit (pass `prevScreen = undefined`)
6. Call `renderNodeToOutput(node, output, { prevScreen })`
7. After render: if a scroll-drain node remains, call `markDirty(drainNode)` to schedule the next drain frame
8. Return `Frame` with the rendered screen, viewport, cursor position, and scroll hint
**Cursor position:**
- Alt-screen: `y = min(screen.height, terminalRows) - 1` — keeps cursor inside viewport to prevent LF-induced scroll
- Main-screen: `y = screen.height`
- `visible = !isTTY || screen.height === 0` — cursor is hidden during active rendering
The `Output` instance is reused across frames so its `charCache` (per-line grapheme cluster cache) persists between renders, making steady-state spinner/clock renders near zero-allocation.
### `/x/Bigger-Projects/Claude-Code/src/ink/render-node-to-output.ts`
**Purpose:** Recursively walks the DOM tree and emits write/blit/clear/clip/shift operations onto the `Output` buffer. This is the layout-to-pixels bridge.
**Key exported state:**
- `didLayoutShift()` / `resetLayoutShifted()` — set when any node moves; gates the full-damage path in `ink.tsx`
- `getScrollHint()` / `resetScrollHint()` — DECSTBM hardware scroll hint for `LogUpdate`
- `getScrollDrainNode()` / `resetScrollDrainNode()` — identifies a ScrollBox with remaining `pendingScrollDelta`
- `consumeFollowScroll()` — consumes the at-bottom follow-scroll event for selection adjustment
**Scroll drain parameters:**
```
SCROLL_MIN_PER_FRAME = 4 // minimum rows applied per frame
SCROLL_INSTANT_THRESHOLD = 5 // ≤ this: drain all at once (xterm.js wheel click)
SCROLL_HIGH_PENDING = 12 // threshold for high-speed drain
SCROLL_STEP_MED = 2 // medium pending drain step
SCROLL_STEP_HIGH = 3 // high pending drain step
```
**ScrollBox rendering (three-pass algorithm):**
1. **First pass:** compute scrollHeight from yoga, apply `pendingScrollDelta` (proportional drain), handle `stickyScroll`, detect `scrollAnchor`
2. **Second pass (blit path):** when content is unchanged and layout hasn't shifted, blit the scrollbox from `prevScreen` and emit a hardware `shiftRows` hint
3. **Third pass (full render):** render children with `clip` and `y-offset = -scrollTop`; record `absoluteRectsCur` for position:absolute descendants
**Blit optimization:** When a node's bounding box matches `nodeCache` and the node is not dirty, the renderer blits from `prevScreen` instead of re-rendering. The condition is: `!dirty && prevScreen && !hasRemovedChild && !layoutShifted`. This makes steady-state frames O(changed cells).
**`nodeCache` updates:** After rendering each node, `nodeCache.set(node, { x, y, width, height })` records the screen-space bounding box for hit-testing and blit reuse.
### `/x/Bigger-Projects/Claude-Code/src/ink/output.ts`
**Purpose:** Collects rendering operations (write, blit, clip, clear, no-select, shift) and applies them to a `Screen` buffer in `get()`.
**`Operation` union type:**
```typescript
type Operation =
| WriteOperation // { type: 'write', x, y, text, softWrap? }
| ClipOperation // { type: 'clip', clip: Clip }
| UnclipOperation // { type: 'unclip' }
| BlitOperation // { type: 'blit', srcScreen, srcRect, dstX, dstY }
| ClearOperation // { type: 'clear', x, y, width, height }
| NoSelectOperation // { type: 'noSelect', x, y, width, height }
| ShiftOperation // { type: 'shift', top, bottom, delta }
```
**`Clip` type:** `{ x1?, x2?, y1?, y2? }` — undefined on an axis means unbounded. Clips are intersected when nested.
**Write operation processing:**
The `WriteOperation` handler is the hot path. For each character:
1. Tokenize the text (ANSI codes) via `@alcalzone/ansi-tokenize`
2. Build a `ClusteredChar[]` array (grapheme + width + styleId + hyperlink), cached per unique line via `charCache: Map<string, ClusteredChar[]>`
3. Apply bidirectional reordering (`reorderBidi`) on Windows/xterm.js
4. Call `setCellAt` for each grapheme
**`charCache`:** Keyed by the raw ANSI string line. Cache miss: tokenize + cluster + intern styles. Cache hit: reuse the `ClusteredChar[]` array directly. The cache persists across frames (it lives on the `Output` instance). This is the dominant performance optimization for text-heavy content.
**Tab expansion:** Tabs in text are expanded to spaces at write time using screen x-position (not at measurement time), so the rendered width matches the measured width.
### `/x/Bigger-Projects/Claude-Code/src/ink/render-to-screen.ts`
**Purpose:** Off-screen renderer used for search scanning. Renders a React element to an isolated `Screen` buffer at a given width without writing to the terminal. Used to pre-scan message content for search match positions.
**`renderToScreen(el, width)`:** Returns `{ screen: Screen; height: number }`. Uses a shared persistent root/container/pools (LegacyRoot mode for synchronous rendering) so repeated calls reuse Yoga nodes. Unmounts between calls to free resources.
**`scanPositions(screen, query)`:** Scans a rendered screen for all occurrences of `query`, returning `MatchPosition[]` with `{ row, col, len }` in message-relative coordinates.
**`applyPositionedHighlight(screen, positions, rowOffset, currentIdx, stylePool)`:** Applies the current-match yellow/bold/underline style to the cell range at `positions[currentIdx]` and inverse style to all other positions.
### `/x/Bigger-Projects/Claude-Code/src/ink/frame.ts`
**Purpose:** Defines the `Frame` type and the diff/patch type hierarchy.
**`Frame` type:**
```typescript
type Frame = {
readonly screen: Screen
readonly viewport: Size
readonly cursor: Cursor
readonly scrollHint?: ScrollHint | null
readonly scrollDrainPending?: boolean
}
```
**`Patch` union type:**
```typescript
type Patch =
| { type: 'stdout'; content: string }
| { type: 'clear'; count: number }
| { type: 'clearTerminal'; reason: FlickerReason; debug?: {...} }
| { type: 'cursorHide' }
| { type: 'cursorShow' }
| { type: 'cursorMove'; x: number; y: number }
| { type: 'cursorTo'; col: number }
| { type: 'carriageReturn' }
| { type: 'hyperlink'; uri: string }
| { type: 'styleStr'; str: string }
type Diff = Patch[]
```
**`shouldClearScreen(prevFrame, frame)`:** Returns `'resize' | 'offscreen' | undefined`:
- `'resize'` — viewport dimensions changed
- `'offscreen'` — current or previous screen height exceeds viewport rows
**`FlickerReason`:** `'resize' | 'offscreen' | 'clear'`
**`FrameEvent`:** Timing breakdown emitted to `onFrame`:
```typescript
type FrameEvent = {
durationMs: number
phases?: {
renderer: number; diff: number; optimize: number; write: number
patches: number; yoga: number; commit: number
yogaVisited: number; yogaMeasured: number; yogaCacheHits: number; yogaLive: number
}
flickers: Array<{ desiredHeight, availableHeight, reason }>
}
```
### `/x/Bigger-Projects/Claude-Code/src/ink/log-update.ts` — Diff Engine
**Purpose:** Computes a `Diff` (list of `Patch` objects) by comparing the new `Frame` to the previous frame, or handles full-screen clears when needed.
**`LogUpdate` class:**
Constructor takes `{ isTTY, stylePool }`. Maintains `previousOutput: string` (deprecated legacy string tracking).
Key methods:
- `render(prevFrame, frame)` — main diff entry point. If `shouldClearScreen()` triggers, prepends a `clearTerminal` patch. Otherwise runs the incremental diff algorithm
- `renderPreviousOutput_DEPRECATED(prevFrame)` — used for final output on exit (writes the last frame to terminal)
- `reset()` — clears `previousOutput` (called after SIGCONT)
**Incremental diff algorithm (`renderDiff`):**
1. Walk rows top-down, comparing `prevScreen` and `screen` cell-by-cell via `diffEach`
2. For unchanged rows: emit cursor moves to skip them
3. For changed rows: emit `styleStr` transitions + `stdout` content patches + `hyperlink` patches
4. Handle wide chars: skip `SpacerTail` cells; emit a space for `SpacerHead` (end-of-line wrap guard)
5. Track hyperlink state across rows (emit `LINK_END` when hyperlink changes)
6. After last row: position cursor per `frame.cursor`
**DECSTBM hardware scroll (`scrollHint`):**
When the back frame has a `scrollHint` and no layout shift occurred and the viewport can accommodate the shift:
- Emit `setScrollRegion(top, bottom)` (DECSTBM)
- Emit `scrollUp(n)` (CSI S) or `scrollDown(n)` (CSI T)
- Emit `RESET_SCROLL_REGION`
- Only repaint the rows that changed due to the scroll (a narrow repair band)
This replaces O(viewport) cell writes with O(scrolled region) writes for smooth scroll in fullscreen mode.
### `/x/Bigger-Projects/Claude-Code/src/ink/optimizer.ts`
**Purpose:** Single-pass patch list optimizer.
**`optimize(diff)`:** Rules applied:
- Remove empty `stdout` patches
- Remove no-op `cursorMove(0,0)` patches
- Remove `clear` patches with count 0
- **Merge** consecutive `cursorMove` patches (add x/y)
- **Collapse** consecutive `cursorTo` patches (keep last)
- **Concat** adjacent `styleStr` patches
- **Deduplicate** consecutive `hyperlink` patches with same URI
- **Cancel** adjacent `cursorHide`/`cursorShow` pairs
### `/x/Bigger-Projects/Claude-Code/src/ink/node-cache.ts`
**Purpose:** Stores the rendered bounding box for each `DOMElement`, used for blit optimization and hit-testing.
**Exports:**
```typescript
type CachedLayout = { x: number; y: number; width: number; height: number; top?: number }
const nodeCache = new WeakMap<DOMElement, CachedLayout>()
const pendingClears = new WeakMap<DOMElement, Rectangle[]>()
addPendingClear(parent, rect, isAbsolute): void
consumeAbsoluteRemovedFlag(): boolean
```
`pendingClears` holds rectangles of removed children that must be painted over on the next render. `absoluteNodeRemoved` gates the global blit disable path for absolute-positioned removals.
---
## Terminal I/O
### `/x/Bigger-Projects/Claude-Code/src/ink/terminal.ts`
**Purpose:** Terminal capability detection and the `writeDiffToTerminal` serializer.
**`Terminal` type:** `{ stdout: NodeJS.WriteStream; stderr: NodeJS.WriteStream }`
**Capability detection:**
- `isSynchronizedOutputSupported()` — returns `true` for iTerm2, WezTerm, Warp, ghostty, kitty, VS Code, alacritty, foot, etc. Returns `false` for tmux (parses but doesn't implement DEC 2026 properly)
- `isProgressReportingAvailable()` — returns `true` for ConEmu, Ghostty 1.2.0+, iTerm2 3.6.6+; excludes Windows Terminal (interprets OSC 9;4 as notifications)
- `supportsExtendedKeys()` — detects Kitty keyboard protocol or modifyOtherKeys support
- `isXtermJs()` — set by XTVERSION probe (survives SSH, unlike TERM_PROGRAM)
- `setXtversionName(name)` — called by `App.tsx` after terminal query response
**`writeDiffToTerminal(terminal, diff, syncOutput)`:**
Serializes a `Diff` into ANSI sequences and writes to `terminal.stdout`. If `syncOutput === SYNC_OUTPUT_SUPPORTED`, wraps in BSU/ESU (DEC 2026 synchronized output). The serialization is a tight loop over the `Patch` array:
- `stdout`: write content directly
- `clear`: `eraseLines(count)` — moves cursor up and erases
- `clearTerminal`: `getClearTerminalSequence()` — erase screen + scrollback
- `cursorHide` / `cursorShow`: emit HIDE/SHOW_CURSOR sequences
- `cursorMove`: emit `cursorMove(x, y)` relative move
- `cursorTo`: emit `cursorTo(col)` absolute column
- `carriageReturn`: emit `\r`
- `hyperlink`: emit `link(uri)` or `LINK_END`
- `styleStr`: write pre-serialized ANSI transition string directly
`SYNC_OUTPUT_SUPPORTED` constant is computed once at module load.
---
## Termio Layer
### `/x/Bigger-Projects/Claude-Code/src/ink/termio.ts` — Termio Public API
Re-exports:
- `Parser` from `termio/parser.ts`
- All types: `Action`, `Color`, `CursorAction`, `CursorDirection`, `EraseAction`, `Grapheme`, `LinkAction`, `ModeAction`, `NamedColor`, `ScrollAction`, `TextSegment`, `TextStyle`, `TitleAction`, `UnderlineStyle`
- `colorsEqual`, `defaultStyle`, `stylesEqual`
### `/x/Bigger-Projects/Claude-Code/src/ink/termio/types.ts`
**Purpose:** Semantic type definitions for all ANSI parser output.
**Key types:**
```typescript
// 16-color palette
type NamedColor = 'black' | 'red' | ... | 'brightWhite'
// 3-way color union
type Color =
| { type: 'named'; name: NamedColor }
| { type: 'indexed'; index: number } // 0-255
| { type: 'rgb'; r: number; g: number; b: number }
| { type: 'default' }
type UnderlineStyle = 'none' | 'single' | 'double' | 'curly' | 'dotted' | 'dashed'
type TextStyle = {
bold: boolean; dim: boolean; italic: boolean; underline: UnderlineStyle
blink: boolean; inverse: boolean; hidden: boolean; strikethrough: boolean
overline: boolean; fg: Color; bg: Color; underlineColor: Color
}
// All parsed actions
type Action =
| { type: 'text'; graphemes: Grapheme[]; style: TextStyle }
| { type: 'cursor'; action: CursorAction }
| { type: 'erase'; action: EraseAction }
| { type: 'scroll'; action: ScrollAction }
| { type: 'mode'; action: ModeAction }
| { type: 'link'; action: LinkAction }
| { type: 'title'; action: TitleAction }
| { type: 'tabStatus'; action: TabStatusAction }
| { type: 'sgr'; params: string }
| { type: 'bell' }
| { type: 'reset' }
| { type: 'unknown'; sequence: string }
```
**`TabStatusAction`:** `{ indicator?: Color | null; status?: string | null; statusColor?: Color | null }` — for OSC 21337 tab chrome metadata.
Utility functions: `defaultStyle()`, `stylesEqual(a, b)`, `colorsEqual(a, b)`.
### `/x/Bigger-Projects/Claude-Code/src/ink/termio/ansi.ts`
**Purpose:** Base ANSI constants and C0 control character codes.
**Exports:**
- `C0` object — complete C0 control character table (NUL through DEL)
- `ESC = '\x1b'`, `BEL = '\x07'`, `SEP = ';'`
- `ESC_TYPE` — escape sequence introducers: `CSI=0x5b, OSC=0x5d, DCS=0x50, APC=0x5f, PM=0x5e, SOS=0x58, ST=0x5c`
- `isC0(byte)`, `isEscFinal(byte)`
### `/x/Bigger-Projects/Claude-Code/src/ink/termio/csi.ts`
**Purpose:** CSI (Control Sequence Introducer) sequence generation and constants.
**Exports:**
- `CSI_PREFIX = ESC + '['`
- `CSI_RANGE` — parameter/intermediate/final byte ranges
- `isCSIParam`, `isCSIIntermediate`, `isCSIFinal`
- `csi(...args)` — sequence generator: `ESC [ params... final`
- `CSI` enum — final byte codes: `CUU=0x41(A), CUD=0x42(B), CUF=0x43(C), CUB=0x44(D), CNL=0x45(E), CPL=0x46(F), CHA=0x47(G), CUP=0x48(H), ED=0x4a(J), EL=0x4b(K), SU=0x53(S), SD=0x54(T), SGR=0x6d(m), DECSTBM=0x72(r), ...`
- Pre-generated sequences: `CURSOR_HOME`, `ERASE_SCREEN`, `ERASE_SCROLLBACK`, `RESET_SCROLL_REGION`, `PASTE_START`, `PASTE_END`, `FOCUS_IN`, `FOCUS_OUT`, `ENABLE_KITTY_KEYBOARD`, `DISABLE_KITTY_KEYBOARD`, `ENABLE_MODIFY_OTHER_KEYS`, `DISABLE_MODIFY_OTHER_KEYS`
- Parameterized: `cursorMove(x,y)`, `cursorTo(col)`, `cursorPosition(row,col)`, `eraseLines(count)`, `setScrollRegion(top,bottom)`, `scrollUp(n)`, `scrollDown(n)`
### `/x/Bigger-Projects/Claude-Code/src/ink/termio/dec.ts`
**Purpose:** DEC private mode sequence generation.
**Exports:**
- `DEC` enum — mode numbers: `CURSOR_VISIBLE=25, ALT_SCREEN=47, ALT_SCREEN_CLEAR=1049, MOUSE_NORMAL=1000, MOUSE_BUTTON=1002, MOUSE_ANY=1003, MOUSE_SGR=1006, FOCUS_EVENTS=1004, BRACKETED_PASTE=2004, SYNCHRONIZED_UPDATE=2026`
- `decset(mode)` / `decreset(mode)``CSI ? N h` / `CSI ? N l`
- Pre-generated: `BSU` (begin synchronized update), `ESU`, `EBP/DBP` (bracketed paste), `EFE/DFE` (focus events), `SHOW_CURSOR/HIDE_CURSOR`, `ENTER_ALT_SCREEN/EXIT_ALT_SCREEN`
- `ENABLE_MOUSE_TRACKING` — combination of 1000+1002+1003+1006 set
- `DISABLE_MOUSE_TRACKING` — reverse order reset
### `/x/Bigger-Projects/Claude-Code/src/ink/termio/osc.ts`
**Purpose:** OSC (Operating System Command) sequence generation and clipboard/tab-status support.
**Exports:**
- `OSC_PREFIX = ESC + ']'`, `ST = ESC + '\\'`
- `osc(...parts)` — generates `ESC ] parts BEL` (or ST for Kitty)
- `wrapForMultiplexer(sequence)` — wraps in tmux DCS passthrough (`ESC P tmux ; ... ESC \`) or GNU screen DCS if `TMUX`/`STY` env vars set
- `link(url)` / `LINK_END` — OSC 8 hyperlink start/end
- `setClipboard(text)` — OSC 52 + optional pbcopy/tmux load-buffer; returns `ClipboardPath`
- `getClipboardPath()``'native' | 'tmux-buffer' | 'osc52'` without side effects
- `tmuxLoadBuffer(text)` — async: runs `tmux load-buffer [-w] -`
- `tabStatus({indicator, status, statusColor})` / `CLEAR_TAB_STATUS` — OSC 21337 tab chrome
- `supportsTabStatus()` — detects iTerm2 / Claude Code terminal from env
- `CLEAR_ITERM2_PROGRESS` — clears iTerm2 progress bar
**Clipboard path decision:**
- `native`: macOS + no SSH_CONNECTION → use `pbcopy`
- `tmux-buffer`: inside tmux → use `tmux load-buffer [-w]`
- `osc52`: fallback → write OSC 52 raw sequence
### `/x/Bigger-Projects/Claude-Code/src/ink/termio/sgr.ts`
**Purpose:** SGR (Select Graphic Rendition) parameter parser.
**`applySGR(paramStr, style)`:** Parses semicolon/colon separated SGR params and mutates a `TextStyle`. Handles:
- SGR 0: reset
- SGR 1/2/3/4/5/7/8/9/53: bold/dim/italic/underline/blink/inverse/hidden/strikethrough/overline
- SGR 21/22/23/24/25/27/28/29/55: attribute reset
- SGR 30-37/90-97: named fg colors
- SGR 40-47/100-107: named bg colors
- SGR 38/48: extended fg/bg (256-indexed via `5` or truecolor via `2`)
- SGR 39/49: default fg/bg
- SGR 58/59: underline color
- Kitty extended underline styles (SGR 4:1-4:5)
Colon-separated subparams take priority over semicolon-separated for extended colors.
### `/x/Bigger-Projects/Claude-Code/src/ink/termio/esc.ts`
**Purpose:** ESC (non-CSI, non-OSC) sequence parser. Handles `ESC c` (full reset), `ESC 7`/`ESC 8` (save/restore cursor), and other two-byte sequences.
**`parseEsc(sequence)`:** Returns an `Action | null`.
### `/x/Bigger-Projects/Claude-Code/src/ink/termio/tokenize.ts`
**Purpose:** Streaming tokenizer for terminal input — splits raw bytes into text chunks and escape sequences without interpreting semantics.
**States:** `ground`, `escape`, `escapeIntermediate`, `csi`, `ss3`, `osc`, `dcs`, `apc`
**`Token` type:** `{ type: 'text'; value: string } | { type: 'sequence'; value: string }`
**`Tokenizer` interface:**
```typescript
{
feed(input: string): Token[]
flush(): Token[]
reset(): void
buffer(): string
}
```
**`createTokenizer(options?)`:**
- `options.x10Mouse` — enables X10 legacy mouse event parsing (consume 3 extra bytes after `CSI M`)
- Maintains incremental state across `feed()` calls for streaming input
**Algorithm:** Character-by-character state machine. `ground` state: text passes through until `ESC` or C0 control chars. `csi` state: accumulates until CSI final byte (0x400x7e). `osc`/`dcs`/`apc` states: accumulate until BEL or ST.
### `/x/Bigger-Projects/Claude-Code/src/ink/termio/parser.ts`
**Purpose:** Semantic parser that wraps the tokenizer and interprets each sequence into a structured `Action`.
**`Parser` class:**
```typescript
class Parser {
feed(input: string): Action[]
flush(): Action[]
reset(): void
getStyle(): TextStyle
}
```
**Internal structure:**
- Holds a `Tokenizer` with `x10Mouse: true`
- Maintains current `TextStyle` (updated by SGR actions)
- Calls `parseCSI`, `parseEsc`, `parseOSC` for sequence tokens
- Calls `segmentGraphemes` for text tokens
**Grapheme width detection:**
- `isEmoji(codePoint)` — ranges: 0x2600-0x26ff, 0x2700-0x27bf, 0x1F300-0x1F9FF, 0x1FA00-0x1FAFF, 0x1F1E0-0x1F1FF
- `isEastAsianWide(codePoint)` — standard CJK/Hangul ranges
- `graphemeWidth(grapheme)` — returns 1 or 2
- `segmentGraphemes(str)` — uses `Intl.Segmenter` to split by grapheme cluster
**`parseCSI(rawSequence)`:** Dispatches by final byte:
- `m` (SGR): `{ type: 'sgr', params }`
- Cursor movement (A-H, d, f): `{ type: 'cursor', action }`
- Erase (J, K, X): `{ type: 'erase', action }`
- Scroll (S, T): `{ type: 'scroll', action }`
- DEC private modes (h/l with `?` prefix): `{ type: 'mode', action }`
- Mouse events (M/m with `<` prefix): decoded SGR mouse
---
## Event System
### `/x/Bigger-Projects/Claude-Code/src/ink/events/event.ts`
**Purpose:** Base `Event` class providing `stopImmediatePropagation()`.
```typescript
class Event {
stopImmediatePropagation(): void
didStopImmediatePropagation(): boolean
}
```
### `/x/Bigger-Projects/Claude-Code/src/ink/events/terminal-event.ts`
**Purpose:** `TerminalEvent` extends `Event` with DOM-style propagation properties.
**`EventTarget` type:** `{ parentNode: EventTarget | undefined; _eventHandlers?: Record<string, unknown> }`
**`TerminalEvent` properties:**
- `type: string`, `timeStamp: number`, `bubbles: boolean`, `cancelable: boolean`
- `target: EventTarget | null`, `currentTarget: EventTarget | null`
- `eventPhase: 'none' | 'capturing' | 'at_target' | 'bubbling'`
- `defaultPrevented: boolean`
**Methods:** `stopPropagation()`, `stopImmediatePropagation()` (overrides base), `preventDefault()`
Internal setters: `_setTarget`, `_setCurrentTarget`, `_setEventPhase`, `_isPropagationStopped()`, `_isImmediatePropagationStopped()`, `_prepareForTarget(target)` (hook for subclasses)
### `/x/Bigger-Projects/Claude-Code/src/ink/events/dispatcher.ts`
**Purpose:** Two-phase (capture + bubble) event dispatcher modeled after the browser DOM event model.
**`Dispatcher` class:**
- `dispatch(target, event)` — full capture+bubble cycle via `collectListeners` + `processDispatchQueue`; runs asynchronously (via `unstable_batchedUpdates` or React's scheduler for `discrete` vs `continuous` events)
- `dispatchDiscrete(target, event)` — discrete priority (keyboard, focus); triggers React's synchronous flush
- Internal `collectListeners(target, event)` — walks from target to root, prepending capture handlers (root-first) and appending bubble handlers (target-first); result: `[root-cap, ..., target-cap, target-bub, ..., root-bub]`
- `processDispatchQueue(listeners, event)` — calls each handler, checking `_isPropagationStopped()` and `_isImmediatePropagationStopped()` before each
**React event priority mapping:**
- Keyboard/focus → `DiscreteEventPriority`
- Mouse motion → `ContinuousEventPriority`
- Other → `DefaultEventPriority`
### `/x/Bigger-Projects/Claude-Code/src/ink/events/event-handlers.ts`
**Purpose:** Defines the complete set of event handler props and the reverse lookup table.
**`EventHandlerProps`:**
```typescript
{
onKeyDown?, onKeyDownCapture?: KeyboardEventHandler
onFocus?, onFocusCapture?, onBlur?, onBlurCapture?: FocusEventHandler
onPaste?, onPasteCapture?: PasteEventHandler
onResize?: ResizeEventHandler
onClick?: ClickEventHandler
onMouseEnter?, onMouseLeave?: HoverEventHandler
}
```
**`HANDLER_FOR_EVENT`:** Maps event type strings to `{ bubble?, capture? }` prop name pairs. Used by `Dispatcher` for O(1) handler lookup.
**`EVENT_HANDLER_PROPS`:** `Set<string>` of all handler prop names; used by the reconciler to route event props to `_eventHandlers` instead of `attributes`.
### `/x/Bigger-Projects/Claude-Code/src/ink/events/keyboard-event.ts`
**`KeyboardEvent extends TerminalEvent`:**
- Constructor takes a `ParsedKey`; type = `'keydown'`; `bubbles = true`; `cancelable = true`
- `key: string` — printable char for printable keys; multi-char name for specials (`'down'`, `'return'`, `'escape'`, `'f1'`, etc.)
- `ctrl, shift, meta, superKey, fn: boolean`
Key extraction: ctrl keys use the name (letter); single printable ASCII chars use the literal char; special keys use the parsed name.
### `/x/Bigger-Projects/Claude-Code/src/ink/events/click-event.ts`
**`ClickEvent extends Event`:**
- `col: number`, `row: number` — 0-indexed screen coordinates
- `localCol: number`, `localRow: number` — coordinates relative to the current handler's Box (updated by `dispatchClick` before each handler fires)
- `cellIsBlank: boolean` — true if the cell had no content (allows handlers to ignore clicks on empty space)
### Other event types:
**`InputEvent`** (events/input-event.ts): Legacy input event emitted on stdin data; carries `input: string` and `Key` object. `Key` type: `{ upArrow, downArrow, leftArrow, rightArrow, pageUp, pageDown, return, escape, ctrl, shift, tab, backspace, delete, meta, fn }`.
**`FocusEvent`** (events/focus-event.ts): `type = 'focus' | 'blur'`; `relatedTarget: DOMElement | null`
**`TerminalFocusEvent`** (events/terminal-focus-event.ts): `type: TerminalFocusEventType = 'terminal-focus-in' | 'terminal-focus-out'`; fired when DECSET 1004 focus events arrive.
**`EventEmitter`** (events/emitter.ts): Simple typed event emitter. `on(event, handler)`, `off(event, handler)`, `emit(event, ...args)`. Used by `App.tsx` for stdin data events.
---
## Focus Management
### `/x/Bigger-Projects/Claude-Code/src/ink/focus.ts`
**`FocusManager` class:**
Stored on `ink-root` node so any node can reach it by walking `parentNode`.
State:
- `activeElement: DOMElement | null`
- `focusStack: DOMElement[]` — history for focus restoration (max 32 entries)
- `enabled: boolean`
Methods:
- `focus(node)` — blur previous, push to stack, focus new node; dispatches `focus`/`blur` events
- `blur()` — blur `activeElement`, dispatches `blur` event
- `handleNodeRemoved(node, root)` — removes node from stack, restores focus from stack if `activeElement` was in the removed subtree
- `handleClickFocus(node)` — called by `dispatchClick`; focuses the nearest focusable ancestor
- `getNextFocusable(root, direction)` — Tab/Shift+Tab cycling; collects all nodes with `tabIndex >= 0`, sorts by order, returns next/previous
- `enable()` / `disable()` — gates all focus operations
**`getFocusManager(node)`** / **`getRootNode(node)`** — utility functions exported for the reconciler.
---
## Input Parsing
### `/x/Bigger-Projects/Claude-Code/src/ink/parse-keypress.ts`
**Purpose:** Parses raw terminal stdin bytes into structured `ParsedKey` objects. Handles standard ANSI sequences, Kitty keyboard protocol (CSI u), xterm modifyOtherKeys, SGR mouse events, and terminal response sequences.
**`ParsedKey` type:**
```typescript
{
kind: 'key' | 'mouse' | 'terminalResponse'
name: string // key name: 'up', 'down', 'return', 'escape', 'f1', 'a', ...
fn: boolean
ctrl: boolean; meta: boolean; shift: boolean; option: boolean; super: boolean
sequence: string // raw escape sequence
raw: string // original input bytes
isPasted?: boolean
}
```
**`ParsedMouse` type:**
```typescript
{
kind: 'mouse'
button: number // SGR button code
col: number; row: number // 1-indexed
press: boolean // true = press, false = release
isWheel: boolean
isDrag: boolean
modifiers: { ctrl, shift, meta, alt }
}
```
**`ParsedInput = ParsedKey | ParsedMouse | TerminalResponse`**
**Regex patterns:**
- `META_KEY_CODE_RE`: `ESC + [a-zA-Z0-9]` — meta key combos
- `FN_KEY_RE`: SS3/CSI function key sequences
- `CSI_U_RE`: Kitty protocol `ESC [ codepoint [;modifier] u`
- `MODIFY_OTHER_KEYS_RE`: xterm `ESC [ 27 ; modifier ; keycode ~`
- `DECRPM_RE`, `DA1_RE`, `DA2_RE`, `KITTY_FLAGS_RE`, `CURSOR_POSITION_RE` — terminal response patterns
- `OSC_RESPONSE_RE`: OSC sequence responses
- `XTVERSION_RE`: DCS `>|` terminal name/version
- `SGR_MOUSE_RE`: `ESC [ < btn ; col ; row M/m`
**`parseMultipleKeypresses(buffer, state)`:** Main entry point. Uses the tokenizer to split the input, then dispatches each token to the appropriate parser. Handles bracketed paste (accumulates until `PASTE_END`).
**`INITIAL_STATE`:** Initial parser state for `parseMultipleKeypresses`.
---
## Hit Testing
### `/x/Bigger-Projects/Claude-Code/src/ink/hit-test.ts`
**Purpose:** Mouse click hit-testing against the DOM tree.
**`hitTest(node, col, row)`:** DFS in reverse child order (last child = top paint layer wins). Uses `nodeCache` for bounding-box lookups. Returns the deepest `DOMElement` whose rendered rect contains `(col, row)`, or `null`.
**`dispatchClick(root, col, row, cellIsBlank?)`:**
1. Runs `hitTest` to find the deepest hit node
2. Calls `focusManager.handleClickFocus()` to click-to-focus the nearest focusable ancestor
3. Creates a `ClickEvent(col, row, cellIsBlank)`
4. Bubbles up via `parentNode` chain, calling `onClick` handlers
5. Before each handler: sets `event.localCol/localRow` relative to the handler's bounding rect
6. Stops on `stopImmediatePropagation()`
7. Returns `true` if any handler fired
**`dispatchHover(root, col, row, hoveredNodes)`:** Diff-based hover dispatch. Finds the set of nodes hit at `(col, row)`, fires `onMouseEnter` for newly-entered nodes and `onMouseLeave` for exited nodes. Mutates `hoveredNodes` in place (owned by the `Ink` instance).
---
## Text Selection
### `/x/Bigger-Projects/Claude-Code/src/ink/selection.ts`
**Purpose:** Linear text selection in screen-buffer coordinates for fullscreen mode.
**`SelectionState` type:**
```typescript
{
anchor: { col, row } | null
focus: { col, row } | null
isDragging: boolean
anchorSpan: { lo, hi, kind: 'word' | 'line' } | null // word/line mode
scrolledOffAbove: string[] // rows that scrolled off the top
scrolledOffBelow: string[]
scrolledOffAboveSW: boolean[] // soft-wrap flags parallel to scrolledOffAbove
scrolledOffBelowSW: boolean[]
virtualAnchorRow?: number // pre-clamp anchor row (for scroll restore)
virtualFocusRow?: number
lastPressHadAlt: boolean
}
```
**Exported functions:**
- `createSelectionState()` — returns zeroed state
- `startSelection(s, col, row, alt?)` — initializes anchor and focus
- `updateSelection(s, col, row)` — updates focus during drag
- `finishSelection(s)` — clears `isDragging`
- `clearSelection(s)` — resets to empty
- `hasSelection(s)` — returns true if `anchor !== null && focus !== null`
- `getSelectedText(s, screen)` — extracts the selected text; handles soft-wrap (joins wrapped lines), wide chars (skips `SpacerTail`), `noSelect` regions (excluded), and `scrolledOffAbove`/`scrolledOffBelow` accumulators
- `applySelectionOverlay(s, screen, stylePool)` — inverts cell styles in the selected region
- `selectWordAt(s, col, row, screen)` — double-click: select the word under the cursor
- `selectLineAt(s, row, screen)` — triple-click: select the entire line
- `extendSelection(s, col, row, screen)` — word/line-mode drag extension: extends to word/line boundaries
- `shiftSelection(s, dRow, min, max, screenWidth)` — keyboard scroll: shifts both anchor and focus
- `shiftAnchor(s, dRow, min, max)` — shift anchor only (keyboard selection extension)
- `moveFocus(s, move, screen)` — keyboard character/line focus extension
- `captureScrolledRows(s, firstRow, lastRow, side, screen)` — saves rows about to scroll off into `scrolledOffAbove`/`scrolledOffBelow`
- `shiftSelectionForFollow(s, delta, screen)` — called after follow-scroll to keep selection anchored to text
---
## Search Highlighting
### `/x/Bigger-Projects/Claude-Code/src/ink/searchHighlight.ts`
**`applySearchHighlight(screen, query, stylePool)`:**
- Case-insensitive scan of the screen buffer
- Builds per-row char text + `codeUnitToCell` index map (handles wide chars)
- For each match: calls `setCellStyleId(screen, ..., stylePool.withInverse(cellStyleId))` to invert the cell style
- Skips `noSelect` regions (gutters, line numbers)
- Returns `true` if any match was applied (triggers full-damage flag in caller)
The `applyPositionedHighlight` (in `render-to-screen.ts`) handles the "current match" in yellow, on top of `applySearchHighlight`'s inverse.
---
## Component Library
### `/x/Bigger-Projects/Claude-Code/src/ink/components/App.tsx`
**Purpose:** The root React component. Wires together stdin input, terminal size context, focus, clock, error boundaries, and all context providers.
**Props:** `children, stdin, stdout, stderr, exitOnCtrlC, onExit, terminalColumns, terminalRows, selection, onSelectionChange, onClickAt, onHoverAt, getHyperlinkAt, onOpenHyperlink, onMultiClick, onSelectionDrag, onStdinResume?, onCursorDeclaration`
**Responsibilities:**
- Provides `AppContext`, `StdinContext`, `TerminalSizeContext`, `ClockContext`, `TerminalFocusProvider`, `CursorDeclarationContext`, `TerminalWriteProvider`
- Listens to stdin data; calls `parseMultipleKeypresses` for each chunk
- Dispatches `KeyboardEvent` through the DOM via `dispatcher.dispatchDiscrete(rootNode, event)`
- Handles mouse events: left-button selection start/update/finish; wheel → `pendingScrollDelta`; hover → `onHoverAt`; click → `onClickAt`
- Handles pasted content via bracketed paste markers
- Detects terminal focus via DECSET 1004 `FOCUS_IN`/`FOCUS_OUT` sequences
- Runs `TerminalQuerier` on mount to probe XTVERSION and extended key support
- Re-asserts terminal modes after `STDIN_RESUME_GAP_MS = 5000` ms of stdin silence
- Handles `Ctrl+C``onExit` when `exitOnCtrlC = true`
- Handles `Ctrl+Z` (SIGTSTP) on non-Windows platforms
**Class component** (`PureComponent`) for stable reference identity. All mouse/keyboard state is imperative (refs), not React state, to avoid re-renders.
### `/x/Bigger-Projects/Claude-Code/src/ink/components/Box.tsx`
**Purpose:** The primary layout container. Analogous to `<div style="display: flex">`.
**`Props`:** All `Styles` properties (except `textWrap`) plus:
- `ref?: Ref<DOMElement>`
- `tabIndex?: number` — focus order (>= 0 participates in Tab cycling, -1 = programmatic only)
- `autoFocus?: boolean` — focus on mount
- `onClick?: (event: ClickEvent) => void`
- `onFocus?, onFocusCapture?, onBlur?, onBlurCapture?`
- `onKeyDown?, onKeyDownCapture?`
- `onMouseEnter?, onMouseLeave?`
Renders to `<ink-box>` host element. Compiled with React Compiler (memo cache `_c(42)`).
### `/x/Bigger-Projects/Claude-Code/src/ink/components/Text.tsx`
**Purpose:** Renders styled text. Wraps children in `ink-text` and `ink-virtual-text` elements.
**`Props`:**
- `color?, backgroundColor?`
- `bold?, dim?` (mutually exclusive via TypeScript union)
- `italic?, underline?, strikethrough?, inverse?`
- `wrap?: Styles['textWrap']`
- `children?: ReactNode`
Text styling props are mapped to `TextStyles` and passed as the `textStyles` prop on the host element. Layout props (flexDirection, flexGrow, etc.) are mapped to `Styles` on the `ink-text` node.
### `/x/Bigger-Projects/Claude-Code/src/ink/components/ScrollBox.tsx`
**Purpose:** A Box with imperative scroll API and viewport culling.
**`ScrollBoxHandle`:**
```typescript
{
scrollTo(y): void
scrollBy(dy): void
scrollToElement(el, offset?): void // defers position read to render time
scrollToBottom(): void
getScrollTop(): number
getPendingDelta(): number
getScrollHeight(): number
getFreshScrollHeight(): number // reads Yoga directly
getViewportHeight(): number
getViewportTop(): number
isSticky(): boolean
subscribe(listener): () => void
setClampBounds(min, max): void
}
```
**`ScrollBoxProps`:** All `Styles` except `overflow`/`overflowX`/`overflowY`, plus `stickyScroll?: boolean`.
Implementation: Sets `overflow: 'scroll'` on the underlying Box. Scroll mutations call `markDirty` + `scheduleRenderFrom` to trigger an Ink frame without going through React's reconciler. `scrollToElement` stores a `scrollAnchor` on the DOM node; `render-node-to-output` reads it at paint time (after Yoga has computed the element's position) and clears it.
### `/x/Bigger-Projects/Claude-Code/src/ink/components/AlternateScreen.tsx`
**Purpose:** Enters the terminal's alternate screen buffer for fullscreen rendering.
**Props:** `children, mouseTracking?: boolean` (default `true`)
Uses `useInsertionEffect` (not `useLayoutEffect`) to send `ENTER_ALT_SCREEN` before the first Ink render frame. On cleanup (unmount), sends `EXIT_ALT_SCREEN`. Calls `instances.get(process.stdout).setAltScreenActive(true/false)` to coordinate with the Ink instance. Renders children inside a `Box` constrained to `terminalRows` height.
### `/x/Bigger-Projects/Claude-Code/src/ink/components/Link.tsx`
**Purpose:** Renders OSC 8 terminal hyperlinks.
Wraps children in an `ink-link` host element with `href` attribute. During rendering, `squashTextNodesToSegments` propagates the hyperlink URL to contained text segments.
### `/x/Bigger-Projects/Claude-Code/src/ink/components/RawAnsi.tsx`
**Purpose:** Renders pre-formatted ANSI strings with known dimensions.
Props: `children: string, width: number, height: number`
Renders to `ink-raw-ansi` element with `rawWidth`/`rawHeight` attributes. The yoga measure function reads these dimensions directly (no string width measurement, no wrapping).
### `/x/Bigger-Projects/Claude-Code/src/ink/components/NoSelect.tsx`
**Purpose:** Marks a rectangular region as non-selectable (e.g., gutters, line numbers).
Sets the `noSelect` flag on cells in the screen buffer via `markNoSelectRegion`. Text in these cells is excluded from selection copy and search highlighting.
### `/x/Bigger-Projects/Claude-Code/src/ink/components/Newline.tsx`
**Purpose:** Renders `\n` characters (count configurable via `count` prop).
### `/x/Bigger-Projects/Claude-Code/src/ink/components/Spacer.tsx`
**Purpose:** Flexible spacer. Renders a `Box` with `flexGrow: 1`.
### `/x/Bigger-Projects/Claude-Code/src/ink/components/Button.tsx`
**Purpose:** Focusable, clickable button with keyboard activation.
**`ButtonState`:** `'idle' | 'active' | 'focus'`
**Props:** `children, onPress?, disabled?` plus styling.
Uses `useApp` for exit integration. Handles Return/Space keydown when focused.
### `/x/Bigger-Projects/Claude-Code/src/ink/components/ErrorOverview.tsx`
**Purpose:** Error boundary overlay shown when a React component throws. Renders the stack trace and error message with formatting.
### Context components:
**`AppContext.ts`** — provides `{ exit(error?): void }`. Consumed by `useApp`.
**`StdinContext.ts`** — provides `{ stdin, setRawMode, isRawModeSupported, internal_exitOnCtrlC, internal_eventEmitter }`. Consumed by `useStdin`, `useInput`.
**`TerminalSizeContext.tsx`** — provides `{ columns: number; rows: number } | null`. Consumed by `useTerminalViewport`, `AlternateScreen`.
**`ClockContext.tsx`** — provides a shared animation clock with `subscribe(cb, keepAlive?)` and `now()`. All `useAnimationFrame` instances share one clock; idle clock (no `keepAlive` subscribers) suspends to avoid waking the process.
**`CursorDeclarationContext.ts`** — provides a setter for declaring native cursor position. Consumed by `useDeclaredCursor`. The setter signature: `(decl: CursorDeclaration | null, node?: DOMElement | null) => void`.
**`TerminalFocusContext.tsx`** — provides `useTerminalFocus()` which subscribes to DECSET 1004 focus events via `useSyncExternalStore` on `terminal-focus-state.ts`.
---
## Hooks
### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-input.ts`
**`useInput(handler, options?)`:** Subscribes to stdin input events. Calls `setRawMode(true)` via `useLayoutEffect` (synchronous, before render returns). Subscribes to `internal_eventEmitter` `'input'` events. Options: `{ isActive?: boolean }`.
### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-app.ts`
**`default useApp()`:** Returns `{ exit(error?): void }` from `AppContext`. Throws if used outside the App tree.
### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-stdin.ts`
**`default useStdin()`:** Returns the full `StdinContext` value.
### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-animation-frame.ts`
**`useAnimationFrame(intervalMs?)`:** Returns `[ref, time]`. Subscribes to the shared clock and updates `time` every `intervalMs`. Pauses (unsubscribes) when `intervalMs = null` or the element is off-screen (via `useTerminalViewport`).
### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-interval.ts`
**`useInterval(callback, delay?)`:** Calls `callback` every `delay` ms. Uses the shared clock.
**`useAnimationTimer(intervalMs)`:** Returns `time` (elapsed ms). Similar to `useAnimationFrame` but without the viewport visibility check.
### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-terminal-viewport.ts`
**`useTerminalViewport()`:** Returns `[ref, { isVisible }]`. Computes visibility by walking the DOM ancestor chain (including `scrollTop` offsets) during `useLayoutEffect`. Does NOT cause re-renders on visibility change — callers read the current value naturally.
### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-terminal-focus.ts`
**`useTerminalFocus()`:** Returns `boolean` — whether the terminal window is focused. Uses `useSyncExternalStore` on `terminal-focus-state.ts` module-level signal.
### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-terminal-title.ts`
**`useTerminalTitle(title)`:** Sets the terminal window title via OSC 0/2 on mount and clears on unmount.
### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-selection.ts`
**`useSelection()`:** Returns an API object for text selection operations. Falls back to no-ops when not in fullscreen mode. The `Ink` instance is located via `instances.get(process.stdout)`.
Methods: `copySelection()`, `copySelectionNoClear()`, `clearSelection()`, `hasSelection()`, `getState()`, `subscribe(cb)`, `shiftAnchor(dRow, min, max)`, `shiftSelection(dRow, min, max)`, `moveFocus(move)`, `captureScrolledRows(first, last, side)`, `setSelectionBgColor(color)`.
### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-tab-status.ts`
**`useTabStatus(kind: TabStatusKind | null)`:** Emits OSC 21337 tab status sequences. `kind = 'idle' | 'busy' | 'waiting'`. Transitions to `null` emit `CLEAR_TAB_STATUS`. Wrapped for tmux passthrough. Uses `TerminalWriteContext`.
### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-declared-cursor.ts`
**`useDeclaredCursor({ line, column, active })`:** Returns a ref callback. When active, declares the cursor position to the `Ink` instance so the native cursor parks at the text caret. Uses `useLayoutEffect` (no dep array — re-declares every commit) for correct sibling handoff. Clears on unmount via a separate `useLayoutEffect` with empty deps.
### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-search-highlight.ts`
Internal hook for wiring search query to the Ink instance's `setSearchHighlight` / `setSearchPositions`.
---
## Utility Modules
### `/x/Bigger-Projects/Claude-Code/src/ink/Ansi.tsx`
**`Ansi` component:** Parses ANSI escape codes in a string and renders them using `Text` and `Link` components. Memoized. Accepts `children: string` and optional `dimColor: boolean`. Uses `termio.Parser` to extract spans and maps them to `Text` props + `Link` wrappers for hyperlinks.
### `/x/Bigger-Projects/Claude-Code/src/ink/bidi.ts`
**`reorderBidi(characters: ClusteredChar[])`:** Applies the Unicode Bidi Algorithm to a `ClusteredChar` array when running on Windows Terminal, WSL, or xterm.js (all lack native bidi). Uses `bidi-js` library. Detects need via `WT_SESSION` env var or `TERM_PROGRAM=vscode`. No-op on platforms with native bidi support.
### `/x/Bigger-Projects/Claude-Code/src/ink/clearTerminal.ts`
**`getClearTerminalSequence()`:** Returns `ERASE_SCREEN + ERASE_SCROLLBACK + CURSOR_HOME` on modern terminals. Windows: uses HVP (`ESC [ 0 f`) for cursor home on legacy console; includes scrollback clear for Windows Terminal, VS Code, and mintty.
**`clearTerminal`:** Pre-computed clear sequence (module load time).
### `/x/Bigger-Projects/Claude-Code/src/ink/colorize.ts`
**`colorize(text, styles)`:** Applies chalk-based color/style transforms to a text string. Detects the chalk level and adjusts for xterm.js and tmux environments.
**`applyTextStyles(text, textStyles)`:** Converts `TextStyles` to chalk method chain calls.
**Color level management:**
- `boostChalkLevelForXtermJs()` — upgrades chalk to level 3 (truecolor) when `TERM_PROGRAM=vscode` and chalk detected level 2
- `clampChalkLevelForTmux()` — downgrades to level 2 (256-color) inside tmux to avoid truecolor passthrough bugs; skipped when `CLAUDE_CODE_TMUX_TRUECOLOR=1`
### `/x/Bigger-Projects/Claude-Code/src/ink/get-max-width.ts`
**`getMaxWidth(node, offsetWidth?)`:** Computes the available render width for a node accounting for padding and border. Reads from `yogaNode.getComputedPadding`/`getComputedBorder` for each relevant edge.
### `/x/Bigger-Projects/Claude-Code/src/ink/line-width-cache.ts`
**`lineWidth(line: string)`:** Memoized string width for individual lines (no newlines). Cache is a `Map<string, number>`. Used by `measureText`.
### `/x/Bigger-Projects/Claude-Code/src/ink/measure-element.ts`
**`measureElement(node: DOMElement)`:** Returns `{ width, height }` by reading `yogaNode.getComputedWidth()/getComputedHeight()`. Throws if the node has no yoga node.
### `/x/Bigger-Projects/Claude-Code/src/ink/measure-text.ts`
**`measureText(text, maxWidth)`:** Single-pass computation of `{ width, height }` for a text string. Uses `lineWidth` per line. Height = sum of `ceil(lineWidth / maxWidth)` per line (or 1 when `noWrap`).
### `/x/Bigger-Projects/Claude-Code/src/ink/squash-text-nodes.ts`
**`squashTextNodes(node)`:** Concatenates all text content of a node tree into a plain string (no styles). Used by `measureTextNode` in `dom.ts`.
**`squashTextNodesToSegments(node, inheritedStyles?, inheritedHyperlink?, out?)`:** Walks the text node tree and produces `StyledSegment[]` — text with inherited styles and hyperlink URLs. Used by `output.ts` for structured rendering.
**`StyledSegment` type:** `{ text: string; styles: TextStyles; hyperlink?: string }`
### `/x/Bigger-Projects/Claude-Code/src/ink/stringWidth.ts`
**`stringWidth(str)`:** Terminal display width of a string (counts wide chars as 2, strips ANSI codes). Uses `@alcalzone/ansi-tokenize` + grapheme segmentation.
### `/x/Bigger-Projects/Claude-Code/src/ink/widest-line.ts`
**`widestLine(text)`:** Returns the display width of the widest line in a multi-line string.
### `/x/Bigger-Projects/Claude-Code/src/ink/wrap-text.ts`
**`wrapText(text, maxWidth, wrapType)`:** Applies the appropriate text wrap strategy:
- `'wrap'`: `wrapAnsi(text, maxWidth, { trim: false, hard: true })`
- `'wrap-trim'`: `wrapAnsi(text, maxWidth, { trim: true, hard: true })`
- `'truncate-end'` / `'truncate'`: append `…`
- `'truncate-middle'`: insert `…` in middle
- `'truncate-start'`: prepend `…`
- `'end'` / `'middle'`: same as corresponding truncate
Uses `sliceFit` to avoid wide-char boundary errors in slice operations.
### `/x/Bigger-Projects/Claude-Code/src/ink/wrapAnsi.ts`
Custom ANSI-aware word-wrap implementation. Handles wide chars, hyperlinks (OSC 8), and soft-wrap tracking. Returns wrapped text plus `softWrap[]` flags.
### `/x/Bigger-Projects/Claude-Code/src/ink/supports-hyperlinks.ts`
**`supportsHyperlinks()`:** Detects terminal support for OSC 8 hyperlinks. Returns `true` for iTerm2, VS Code, kitty, Ghostty, WezTerm, Warp, and others.
### `/x/Bigger-Projects/Claude-Code/src/ink/tabstops.ts`
**`expandTabs(text, startColumn?)`:** Expands tab characters to spaces based on 8-column tab stops. Used during text measurement (worst-case width). Actual tab expansion at render time uses the screen x-position.
### `/x/Bigger-Projects/Claude-Code/src/ink/render-border.ts`
**`renderBorder(node, output, x, y, width, height)`:** Draws box borders using `cli-boxes` glyphs (single, double, round, bold, classic, dashed). Supports `borderText` option to embed text into top/bottom border lines with start/end/center alignment. Colors applied via `chalk`.
### `/x/Bigger-Projects/Claude-Code/src/ink/terminal-focus-state.ts`
Module-level singleton signal for terminal focus state.
**`TerminalFocusState`:** `'focused' | 'blurred' | 'unknown'`
**Exports:**
- `setTerminalFocused(v)` — updates state, notifies `useSyncExternalStore` subscribers
- `getTerminalFocused()` — returns `focusState !== 'blurred'` (unknown treated as focused)
- `getTerminalFocusState()` — returns the tristate value
- `subscribeTerminalFocus(cb)` — subscribe function for `useSyncExternalStore`
- `resetTerminalFocusState()` — resets to `'unknown'`
### `/x/Bigger-Projects/Claude-Code/src/ink/terminal-querier.ts`
**Purpose:** Queries the terminal for capability information using DA1/DECRQM/XTVERSION sentinel protocol (no timeouts — DA1 is the universal sentinel).
**`TerminalQuery<T>` type:** `{ request: string; match: (r) => r is T }`
**Query builders:**
- `decrqm(mode)` — DECRQM query; response: `DecrpmResponse`
- `da1()` — Primary Device Attributes
- `da2()` — Secondary Device Attributes
- `kittyKeyboard()` — Kitty keyboard flags query
- `cursorPosition()` — DECXCPR
- `oscColor(code, index?)` — OSC 10/11/12 color queries
- `xtversion()` — DCS `>|` terminal name/version
**`TerminalQuerier` class:**
- `send<T>(query)` — returns a `Promise<T | undefined>`
- `flush()` — sends a DA1 sentinel; all pending queries resolve when DA1 response arrives (terminals answer in order)
- Internal: holds a queue of `{ query, resolve }` entries
**`xtversion`:** Stores the XTVERSION name (set by App.tsx after the query resolves; read by `isXtermJs()`).
### `/x/Bigger-Projects/Claude-Code/src/ink/useTerminalNotification.ts`
**`TerminalWriteContext`:** React context providing a `(data: string) => void` write function that bypasses the normal Ink render pipeline (direct stdout write).
**`TerminalWriteProvider`:** `= TerminalWriteContext.Provider`
**`useTerminalNotification()`:** Returns notification methods:
- `notifyITerm2({ message, title? })` — OSC 9 iTerm2 notification
- `notifyKitty({ message, title, id })` — Kitty notification via OSC 99
- `notifyGhostty({ message, title })` — Ghostty notification via OSC 99 variant
- `notifyBell()` — raw BEL character
- `progress(state, percentage?)` — OSC 9;4 progress bar (Ghostty 1.2+, iTerm2 3.6.6+, ConEmu)
### `/x/Bigger-Projects/Claude-Code/src/ink/warn.ts`
Centralized warning emitter (wraps `console.warn` with deduplication). Used by `Box.tsx` for invalid prop combinations.
---
## Architecture: The Complete Pipeline
```
[stdin bytes]
→ App.tsx handleInput()
→ parseMultipleKeypresses() (termio tokenizer + key parser)
→ KeyboardEvent / ParsedMouse / TerminalResponse
→ dispatcher.dispatchDiscrete(rootNode, event) // keyboard
→ React setState / component handlers
[React state change]
→ React reconciler commit
→ reconciler.resetAfterCommit(rootNode)
→ rootNode.onComputeLayout() // yoga calculateLayout()
→ rootNode.onRender() → queueMicrotask(ink.onRender)
[ink.onRender()]
→ createRenderer(rootNode, stylePool)(options)
→ renderNodeToOutput(rootNode, output, { prevScreen })
→ DOM walk with blit/clip/write/scroll ops
→ output.get() → Screen (cell buffer)
→ return Frame { screen, viewport, cursor, scrollHint }
→ applySelectionOverlay(selection, frame.screen, stylePool)
→ applySearchHighlight(frame.screen, query, stylePool)
→ applyPositionedHighlight(frame.screen, positions, ...)
→ log.render(prevFrame, frame) → Diff (Patch[])
→ optimize(diff) → compressed Patch[]
→ writeDiffToTerminal(terminal, diff, syncOutput)
→ BSU (if sync supported)
→ for each Patch: write ANSI to stdout
→ ESU (if sync supported)
→ emit onFrame(FrameEvent)
→ swap frontFrame ↔ backFrame
```
### Double Buffering
The `Ink` class maintains `frontFrame` (the last displayed frame) and `backFrame` (the rendering target). After each render:
- `backFrame.screen` contains the newly rendered content
- It becomes the new `frontFrame`
- Old `frontFrame` becomes the new `backFrame` for the next render
The renderer always reads `prevScreen = frontFrame.screen` for blit operations and writes into `backFrame.screen`.
### Blit Optimization
The blit path is the dominant fast path for steady-state rendering:
1. If a node's `dirty = false` AND `nodeCache` has a valid rect AND `prevScreen` is available AND no layout shift occurred AND no removed children: call `blitRegion(backScreen, prevScreen, cachedRect)` — pure `Int32Array.copyWithin`, O(cells).
2. This means spinner ticks and clock updates only re-render the changed cell ranges; the rest of the screen is copied in bulk.
### Synchronized Output (BSU/ESU)
When `SYNC_OUTPUT_SUPPORTED = true`, each frame is wrapped in DEC mode 2026 begin/end synchronized update sequences (`BSU` / `ESU`). This prevents terminals from rendering intermediate states during the diff write. Supported terminals: iTerm2, WezTerm, Warp, ghostty, kitty, VS Code, alacritty, foot, kitty.
### DECSTBM Hardware Scroll
When a `ScrollBox`'s `scrollTop` changes and layout is otherwise stable, `render-node-to-output` sets a `ScrollHint`. `LogUpdate.render()` checks for this hint and emits:
1. `setScrollRegion(top, bottom)` — DECSTBM restricts scroll to the box's viewport
2. `SU(n)` or `SD(n)` — hardware scroll by n rows
3. `RESET_SCROLL_REGION` — restore full-screen scroll
4. Then only re-renders the narrow band of newly exposed cells
This replaces O(viewport × width) cell writes with O(exposed_rows × width) for smooth scrolling.
### Pool Generational Reset
The `StylePool` and `CharPool` live for the entire session (never reset) to ensure stable IDs for the blit optimization (IDs are comparable as integers across frames). The `HyperlinkPool` is reset every 5 minutes (hyperlinks are ephemeral) via `migrateScreenPools()`, which re-interns all active cells into fresh pool instances.