75 KiB
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 inThemeProvider; returnsInstancecreateRoot(options?)— async; returns aRootwhose.render()method also wraps inThemeProviderRenderOptions,Instance,Root— type re-exports fromroot.tscolor— from the design-system color moduleBox,BoxProps— themed box (design-system ThemedBox)Text,TextProps— themed text (design-system ThemedText)ThemeProvider,usePreviewTheme,useTheme,useThemeSettingAnsi— ANSI-string rendering componentBaseBox,BaseBoxProps— raw ink Box without themeBaseText,BaseTextProps— raw ink Text without themeButton,ButtonProps,ButtonStateLink,LinkPropsNewline,NewlinePropsNoSelectRawAnsiSpacerDOMElement— the virtual DOM element typeClickEvent,EventEmitter,Event,Key,InputEventTerminalFocusEvent,TerminalFocusEventTypeFocusManagerFlickerReasonuseAnimationFrame,useApp,useInput,useAnimationTimer,useIntervaluseSelection,useStdin,useTabStatus,useTerminalFocususeTerminalTitle,useTerminalViewportmeasureElementsupportsTabStatuswrapText
/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 throttledscheduleRender.
/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:
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):
- Run
createRenderer()— walks DOM, runs yoga, fills back-buffer screen - Apply selection overlay (invert styled cells in
selection) - Apply search highlight (invert cells matching
searchHighlightQuery) - Apply positioned highlight (yellow/bold current-match via
searchPositions) - Diff back-frame vs. front-frame via
log.render()→Patch[] - Run
optimize(patches)to merge/deduplicate - Call
writeDiffToTerminal()to serialize patches and write ANSI to stdout - Emit
onFrameevent if wired - 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 repaintsresetFramesForAltScreen()— replaces both frames with blank screens, setsprevFrameContaminated = truereenterAltScreen()— re-asserts alt-screen state on SIGCONT
Resize handling (handleResize):
- Synchronous (no debounce) to keep
terminalColumns/terminalRowsand yoga in sync - For alt-screen: resets frame buffers and sets
needsEraseBeforePaint = trueso the erase happens atomically inside the next BSU/ESU block
Console patching:
patchConsole()— interceptsconsole.log/warn/errorso they write to a separate file descriptor (not stdout), preventing output mixingpatchStderr()— same for stderr
Key public methods:
render(node)— callsreconciler.updateContainer()unmount()— graceful teardown: restore console, disable mouse tracking, exit alt screen, write final frame, free yoga nodeswaitUntilExit()— returns a promise resolved onunmount()clearTextSelection()— clear selection state and force repaintsetSearchHighlight(query)— set live search termsetSearchPositions(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:
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.
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:
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:
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 aDOMElement; attaches a yoga measure function forink-textandink-raw-ansicreateTextNode(text)— allocates aTextNodeappendChildNode(node, childNode)— appends child, syncs yoga tree, marks dirtyinsertBeforeNode(node, newChild, beforeChild)— inserts before; yoga index computed separately from DOM index because some nodes lack yoga nodesremoveChildNode(node, removeNode)— removes child, collectspendingClears, marks dirtysetAttribute(node, key, value)— sets attribute only if changed; skipschildrensetStyle(node, style)— shallow-compares style objects; skips if unchangedsetTextStyles(node, textStyles)— shallow-compares; skips if unchangedsetTextNodeValue(node, text)— updates text, marks dirtymarkDirty(node?)— walks the ancestor chain settingdirty = true; marks yoga dirty onink-text/ink-raw-ansileaf nodesscheduleRenderFrom(node?)— walks to root and callsonRender()clearYogaNodeReferences(node)— recursively clearsyogaNodepointers (call beforefreeRecursive())findOwnerChainAtRow(root, y)— DFS to find the React component stack covering screen rowy(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:
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 timingmarkCommitStart()/getLastCommitMs()— React commit timingresetProfileCounters()dispatcher— theDispatcherinstance (event dispatch)isDebugRepaintsEnabled()— readsCLAUDE_CODE_DEBUG_REPAINTSenv 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:
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):
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 theLayoutMeasureFuncto translateMeasureModeenum valuescalculateLayout(width): callsthis.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:
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:
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: numbermargin | marginX | marginY | marginTop | marginBottom | marginLeft | marginRight: numberpadding | paddingX | paddingY | paddingTop | paddingBottom | paddingLeft | paddingRight: numberflexGrow | flexShrink | flexBasis: numberflexDirection:'row' | 'row-reverse' | 'column' | 'column-reverse'flexWrap:'wrap' | 'nowrap' | 'wrap-reverse'alignItems | alignSelf | justifyContentwidth | height | minWidth | minHeight | maxWidth | maxHeight: number or'${number}%'or'100%'display:'flex' | 'none'overflow | overflowX | overflowY:'visible' | 'hidden' | 'scroll'borderStyle:BorderStyleborderColor | borderTopColor | borderRightColor | borderBottomColor | borderLeftColor: ColorborderDimColor | ...: booleanborderTop | borderRight | borderBottom | borderLeft: booleancolor | 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 fastInt32Arraylookup; others use aMapget(id): retrieves the string- Pool index 0 = space, index 1 = empty (spacer cell)
HyperlinkPool:
intern(hyperlink?): returns 0 for no hyperlinkget(id): returns the URL string orundefined
StylePool:
intern(styles: AnsiCode[]): returns a tagged integer ID; bit 0 =VISIBLE_ON_SPACEflag (background/inverse/underline affect spaces)get(id): strips bit-0 flag and returnsAnsiCode[]transition(fromId, toId): returns the cached ANSI transition string (pre-serialized diff)withInverse(baseId): returns ID of style with SGR 7 (inverse) addedwithCurrentMatch(baseId): returns ID of style with inverse + bold + yellow-fg + underline (current search match)withSelectionBg(baseId): returns ID of style with selection background color appliedsetSelectionBg(bg): sets the selection backgroundAnsiCode(clears cache)
Screen type:
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 screenresetScreen(screen)— zeroes all cellssetCellAt(screen, x, y, char, styleId, width, hyperlink?)— writes a cellcellAt(screen, x, y)— reads a cellcellAtIndex(screen, idx)— reads cell at raw indexisEmptyCellAt(screen, x, y)— both packed words = 0blitRegion(dst, src, srcRect, dstX, dstY)— bulk-copy a rectangle from one screen to another (pureInt32Arraycopy, no decoding)shiftRows(screen, top, bottom, delta)— hardware scroll simulation: move rows up/down within boundsmarkNoSelectRegion(screen, x, y, w, h)— set the noSelect bit on a rectangular regiondiffEach(prev, next, callback)— iterate only changed cells between two screensmigrateScreenPools(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:
{
frontFrame: Frame
backFrame: Frame
isTTY: boolean
terminalWidth: number
terminalRows: number
altScreen: boolean
prevFrameContaminated: boolean
}
Algorithm:
- Check yoga computed dimensions; return empty frame if invalid
- For alt-screen: clamp
heighttoterminalRows(enforces the invariant) - Reuse or create
Outputwith the back-buffer screen - Reset
layoutShifted,scrollHint,scrollDrainNode - If
prevFrameContaminatedor an absolute node was removed: disable blit (passprevScreen = undefined) - Call
renderNodeToOutput(node, output, { prevScreen }) - After render: if a scroll-drain node remains, call
markDirty(drainNode)to schedule the next drain frame - Return
Framewith 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 inink.tsxgetScrollHint()/resetScrollHint()— DECSTBM hardware scroll hint forLogUpdategetScrollDrainNode()/resetScrollDrainNode()— identifies a ScrollBox with remainingpendingScrollDeltaconsumeFollowScroll()— 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):
- First pass: compute scrollHeight from yoga, apply
pendingScrollDelta(proportional drain), handlestickyScroll, detectscrollAnchor - Second pass (blit path): when content is unchanged and layout hasn't shifted, blit the scrollbox from
prevScreenand emit a hardwareshiftRowshint - Third pass (full render): render children with
clipandy-offset = -scrollTop; recordabsoluteRectsCurfor 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:
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:
- Tokenize the text (ANSI codes) via
@alcalzone/ansi-tokenize - Build a
ClusteredChar[]array (grapheme + width + styleId + hyperlink), cached per unique line viacharCache: Map<string, ClusteredChar[]> - Apply bidirectional reordering (
reorderBidi) on Windows/xterm.js - Call
setCellAtfor 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:
type Frame = {
readonly screen: Screen
readonly viewport: Size
readonly cursor: Cursor
readonly scrollHint?: ScrollHint | null
readonly scrollDrainPending?: boolean
}
Patch union type:
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:
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. IfshouldClearScreen()triggers, prepends aclearTerminalpatch. Otherwise runs the incremental diff algorithmrenderPreviousOutput_DEPRECATED(prevFrame)— used for final output on exit (writes the last frame to terminal)reset()— clearspreviousOutput(called after SIGCONT)
Incremental diff algorithm (renderDiff):
- Walk rows top-down, comparing
prevScreenandscreencell-by-cell viadiffEach - For unchanged rows: emit cursor moves to skip them
- For changed rows: emit
styleStrtransitions +stdoutcontent patches +hyperlinkpatches - Handle wide chars: skip
SpacerTailcells; emit a space forSpacerHead(end-of-line wrap guard) - Track hyperlink state across rows (emit
LINK_ENDwhen hyperlink changes) - 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) orscrollDown(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
stdoutpatches - Remove no-op
cursorMove(0,0)patches - Remove
clearpatches with count 0 - Merge consecutive
cursorMovepatches (add x/y) - Collapse consecutive
cursorTopatches (keep last) - Concat adjacent
styleStrpatches - Deduplicate consecutive
hyperlinkpatches with same URI - Cancel adjacent
cursorHide/cursorShowpairs
/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:
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()— returnstruefor iTerm2, WezTerm, Warp, ghostty, kitty, VS Code, alacritty, foot, etc. Returnsfalsefor tmux (parses but doesn't implement DEC 2026 properly)isProgressReportingAvailable()— returnstruefor 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 supportisXtermJs()— set by XTVERSION probe (survives SSH, unlike TERM_PROGRAM)setXtversionName(name)— called byApp.tsxafter 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 directlyclear:eraseLines(count)— moves cursor up and erasesclearTerminal:getClearTerminalSequence()— erase screen + scrollbackcursorHide/cursorShow: emit HIDE/SHOW_CURSOR sequencescursorMove: emitcursorMove(x, y)relative movecursorTo: emitcursorTo(col)absolute columncarriageReturn: emit\rhyperlink: emitlink(uri)orLINK_ENDstyleStr: 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:
Parserfromtermio/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:
// 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:
C0object — 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=0x5cisC0(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 rangesisCSIParam,isCSIIntermediate,isCSIFinalcsi(...args)— sequence generator:ESC [ params... finalCSIenum — 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:
DECenum — 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=2026decset(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 setDISABLE_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)— generatesESC ] parts BEL(or ST for Kitty)wrapForMultiplexer(sequence)— wraps in tmux DCS passthrough (ESC P tmux ; ... ESC \) or GNU screen DCS ifTMUX/STYenv vars setlink(url)/LINK_END— OSC 8 hyperlink start/endsetClipboard(text)— OSC 52 + optional pbcopy/tmux load-buffer; returnsClipboardPathgetClipboardPath()—'native' | 'tmux-buffer' | 'osc52'without side effectstmuxLoadBuffer(text)— async: runstmux load-buffer [-w] -tabStatus({indicator, status, statusColor})/CLEAR_TAB_STATUS— OSC 21337 tab chromesupportsTabStatus()— detects iTerm2 / Claude Code terminal from envCLEAR_ITERM2_PROGRESS— clears iTerm2 progress bar
Clipboard path decision:
native: macOS + no SSH_CONNECTION → usepbcopytmux-buffer: inside tmux → usetmux 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
5or truecolor via2) - 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:
{
feed(input: string): Token[]
flush(): Token[]
reset(): void
buffer(): string
}
createTokenizer(options?):
options.x10Mouse— enables X10 legacy mouse event parsing (consume 3 extra bytes afterCSI 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 (0x40–0x7e). 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:
class Parser {
feed(input: string): Action[]
flush(): Action[]
reset(): void
getStyle(): TextStyle
}
Internal structure:
- Holds a
Tokenizerwithx10Mouse: true - Maintains current
TextStyle(updated by SGR actions) - Calls
parseCSI,parseEsc,parseOSCfor sequence tokens - Calls
segmentGraphemesfor text tokens
Grapheme width detection:
isEmoji(codePoint)— ranges: 0x2600-0x26ff, 0x2700-0x27bf, 0x1F300-0x1F9FF, 0x1FA00-0x1FAFF, 0x1F1E0-0x1F1FFisEastAsianWide(codePoint)— standard CJK/Hangul rangesgraphemeWidth(grapheme)— returns 1 or 2segmentGraphemes(str)— usesIntl.Segmenterto 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().
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: booleantarget: EventTarget | null,currentTarget: EventTarget | nulleventPhase: '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 viacollectListeners+processDispatchQueue; runs asynchronously (viaunstable_batchedUpdatesor React's scheduler fordiscretevscontinuousevents)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:
{
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 coordinateslocalCol: number,localRow: number— coordinates relative to the current handler's Box (updated bydispatchClickbefore 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 | nullfocusStack: DOMElement[]— history for focus restoration (max 32 entries)enabled: boolean
Methods:
focus(node)— blur previous, push to stack, focus new node; dispatchesfocus/blureventsblur()— bluractiveElement, dispatchesblureventhandleNodeRemoved(node, root)— removes node from stack, restores focus from stack ifactiveElementwas in the removed subtreehandleClickFocus(node)— called bydispatchClick; focuses the nearest focusable ancestorgetNextFocusable(root, direction)— Tab/Shift+Tab cycling; collects all nodes withtabIndex >= 0, sorts by order, returns next/previousenable()/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:
{
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:
{
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 combosFN_KEY_RE: SS3/CSI function key sequencesCSI_U_RE: Kitty protocolESC [ codepoint [;modifier] uMODIFY_OTHER_KEYS_RE: xtermESC [ 27 ; modifier ; keycode ~DECRPM_RE,DA1_RE,DA2_RE,KITTY_FLAGS_RE,CURSOR_POSITION_RE— terminal response patternsOSC_RESPONSE_RE: OSC sequence responsesXTVERSION_RE: DCS>|terminal name/versionSGR_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?):
- Runs
hitTestto find the deepest hit node - Calls
focusManager.handleClickFocus()to click-to-focus the nearest focusable ancestor - Creates a
ClickEvent(col, row, cellIsBlank) - Bubbles up via
parentNodechain, callingonClickhandlers - Before each handler: sets
event.localCol/localRowrelative to the handler's bounding rect - Stops on
stopImmediatePropagation() - Returns
trueif 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:
{
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 statestartSelection(s, col, row, alt?)— initializes anchor and focusupdateSelection(s, col, row)— updates focus during dragfinishSelection(s)— clearsisDraggingclearSelection(s)— resets to emptyhasSelection(s)— returns true ifanchor !== null && focus !== nullgetSelectedText(s, screen)— extracts the selected text; handles soft-wrap (joins wrapped lines), wide chars (skipsSpacerTail),noSelectregions (excluded), andscrolledOffAbove/scrolledOffBelowaccumulatorsapplySelectionOverlay(s, screen, stylePool)— inverts cell styles in the selected regionselectWordAt(s, col, row, screen)— double-click: select the word under the cursorselectLineAt(s, row, screen)— triple-click: select the entire lineextendSelection(s, col, row, screen)— word/line-mode drag extension: extends to word/line boundariesshiftSelection(s, dRow, min, max, screenWidth)— keyboard scroll: shifts both anchor and focusshiftAnchor(s, dRow, min, max)— shift anchor only (keyboard selection extension)moveFocus(s, move, screen)— keyboard character/line focus extensioncaptureScrolledRows(s, firstRow, lastRow, side, screen)— saves rows about to scroll off intoscrolledOffAbove/scrolledOffBelowshiftSelectionForFollow(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 +
codeUnitToCellindex map (handles wide chars) - For each match: calls
setCellStyleId(screen, ..., stylePool.withInverse(cellStyleId))to invert the cell style - Skips
noSelectregions (gutters, line numbers) - Returns
trueif 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
parseMultipleKeypressesfor each chunk - Dispatches
KeyboardEventthrough the DOM viadispatcher.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_OUTsequences - Runs
TerminalQuerieron mount to probe XTVERSION and extended key support - Re-asserts terminal modes after
STDIN_RESUME_GAP_MS = 5000ms of stdin silence - Handles
Ctrl+C→onExitwhenexitOnCtrlC = 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 mountonClick?: (event: ClickEvent) => voidonFocus?, 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:
{
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) whenTERM_PROGRAM=vscodeand chalk detected level 2clampChalkLevelForTmux()— downgrades to level 2 (256-color) inside tmux to avoid truecolor passthrough bugs; skipped whenCLAUDE_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, notifiesuseSyncExternalStoresubscribersgetTerminalFocused()— returnsfocusState !== 'blurred'(unknown treated as focused)getTerminalFocusState()— returns the tristate valuesubscribeTerminalFocus(cb)— subscribe function foruseSyncExternalStoreresetTerminalFocusState()— 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:DecrpmResponseda1()— Primary Device Attributesda2()— Secondary Device AttributeskittyKeyboard()— Kitty keyboard flags querycursorPosition()— DECXCPRoscColor(code, index?)— OSC 10/11/12 color queriesxtversion()— DCS>|terminal name/version
TerminalQuerier class:
send<T>(query)— returns aPromise<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 notificationnotifyKitty({ message, title, id })— Kitty notification via OSC 99notifyGhostty({ message, title })— Ghostty notification via OSC 99 variantnotifyBell()— raw BEL characterprogress(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.screencontains the newly rendered content- It becomes the new
frontFrame - Old
frontFramebecomes the newbackFramefor 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:
- If a node's
dirty = falseANDnodeCachehas a valid rect ANDprevScreenis available AND no layout shift occurred AND no removed children: callblitRegion(backScreen, prevScreen, cachedRect)— pureInt32Array.copyWithin, O(cells). - 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:
setScrollRegion(top, bottom)— DECSTBM restricts scroll to the box's viewportSU(n)orSD(n)— hardware scroll by n rowsRESET_SCROLL_REGION— restore full-screen scroll- 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.