InkdownInkdown
Start writing

Claude-Code

62 files·4 subfolders

Shared Workspace

Claude-Code
codex

14-ink-internals

Shared from "Claude-Code" on Inkdown

Ink Internals & Terminal Rendering

Overview

Ink is a React renderer for terminals. Claude Code uses a customized Ink that handles layout (Yoga), ANSI escape codes, input handling, and screen updates. Understanding Ink is key to understanding the UI.

Plain text
┌─────────────────────────────────────────────────────────────────────────────┐
│                    INK RENDERING PIPELINE                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  REACT LAYER                                                                │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐           │
│  │ JSX/TSX │───►│  React   │───►│ Reconciler│───►│   Ink    │           │
│  │Components│    │ Virtual │    │ (diff)   │    │ Renderer │           │
│  │          │    │   DOM   │    │          │    │          │           │
│  └──────────┘    └──────────┘    └──────────┘    └────┬─────┘           │
│                                                      │                      │
│  LAYOUT LAYER                                        ▼                      │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐              │
│  │   Ink Nodes  │───►│  Yoga        │───►│  Geometry    │              │
│  │   (tree)     │    │  (flexbox)   │    │  (x,y,width, │              │
│  │              │    │              │    │   height)    │              │
│  └──────────────┘    └──────────────┘    └───────┬──────┘              │
│                                                  │                        │
│  OUTPUT LAYER                                    ▼                        │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐              │
│  │  ANSI Codes  │◄───│  Output      │◄───│  Screen      │              │
│  │  Generation  │    │  Buffer     │    │  Rendering   │              │
│  │              │    │              │    │              │              │
│  └──────┬───────┘    └──────────────┘    └──────────────┘              │
│         │                                                                   │
│         ▼                                                                   │
│  ┌──────────────┐                                                          │
│  │   Terminal   │                                                          │
│  │   stdout     │                                                          │
│  └──────────────┘                                                          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
0000_start_here_index_and_recommended_reading_order.md
0100_project_overview_tech_stack_runtime_modes_and_folder_map.md
0200_startup_flow_entry_points_and_cold_start_sequence.md
0300_codebase_modules_layers_state_models_and_schemas.md
0400_system_architecture_and_design_rationale.md
0500_interactive_repl_request_flow_end_to_end.md
0600_headless_sdk_and_print_mode_request_flow_end_to_end.md
0700_mcp_integration_connection_and_tool_call_flow.md
0800_external_services_sdks_storage_and_local_dependencies.md
0900_environment_variables_settings_feature_flags_and_failure_modes.md
1000_non_obvious_patterns_gotchas_and_debugging_traps.md
1100_full_codebase_file_inventory_grouped_by_directory.md
kimi
00-overview.md
01-entrypoints.md
02-state-management.md
03-query-system.md
04-tools-system.md
05-tasks-system.md
06-ui-components.md
07-bridge-remote.md
08-services.md
09-skills-plugins.md
10-commands.md
11-testing-architecture.md
12-permission-system.md
13-build-system.md
14-ink-internals.md
15-git-internals.md
16-context-compaction.md
17-vim-mode.md
18-mailbox-notifications.md
19-session-persistence.md
20-hooks-system.md
21-error-recovery.md
README.md
qwen
00-overview.md
01-entry-points.md
02-query-engine.md
03-tools-and-tasks.md
04-commands-and-skills.md
05-state-management.md
06-ink-rendering.md
07-bridge-remote.md
08-mcp-services.md
09-services-overview.md
10-multi-agent.md
11-system-prompt-constants.md
12-tool-interface.md
13-memory-system.md
14-buddy-companion.md
15-keybindings.md
16-stop-hooks.md
17-vim-mode.md
18-upstreamproxy.md
19-cost-tracking-history.md
20-contexts-styles-onboarding.md
21-hooks.md
22-screens.md
tweets-explain
claude-code-memory-analysis.md
compact
memory-system
agentic-architecture

Core Ink Files

FilePurpose
ink/renderer.tsMain rendering entry
ink/reconciler.tsReact reconciler implementation
ink/layout/yoga.tsYoga layout engine integration
ink/layout/engine.tsLayout calculation
ink/layout/geometry.tsBox geometry types
ink/render-to-screen.tsANSI output generation
ink/termio/Terminal I/O handling
ink/components/Built-in components (Box, Text)
ink/hooks/React hooks (useInput, etc.)

The Ink Node Tree

TypeScript
// ink/layout/node.ts
export type InkNode = {
  // React element info
  type: string | Function  // 'box', 'text', Component
  props: Record<string, any>
  children: InkNode[]

  // Yoga layout node
  yogaNode: YogaNode

  // Computed layout
  layout: {
    x: number
    y: number
    width: number
    height: number
  }

  // Output
  output?: string  // Rendered content
}
Tree Structure Example
JavaScript
<Box flexDirection="column">
  <Text>Header</Text>
  <Box flexDirection="row">
    <Text>Sidebar</Text>
    <Text>Main</Text>
  </Box>
</Box>
Plain text
Ink Node Tree:
├─ Box (column)
   ├─ layout: {x:0, y:0, width:80, height:24}
   ├─ children:
   │  ├─ Text "Header"
   │  │  ├─ layout: {x:0, y:0, width:6, height:1}
   │  │  └─ output: "Header"
   │  │
   │  └─ Box (row)
   │     ├─ layout: {x:0, y:1, width:80, height:23}
   │     ├─ children:
   │        ├─ Text "Sidebar"
   │        │  ├─ layout: {x:0, y:1, width:7, height:1}
   │        │  └─ output: "Sidebar"
   │        │
   │        └─ Text "Main"
   │           ├─ layout: {x:8, y:1, width:4, height:1}
   │           └─ output: "Main"

Yoga Layout Engine

Ink uses Yoga (Facebook's flexbox engine) for layout. Same concepts as CSS flexbox, but for terminal cells.

TypeScript
// ink/layout/yoga.ts
import Yoga, { Node as YogaNode } from 'yoga-layout'

export function createYogaNode(): YogaNode {
  return Yoga.Node.create()
}

export function applyStyles(node: YogaNode, props: BoxProps): void {
  // Flex direction
  node.setFlexDirection(
    props.flexDirection === 'row'
      ? Yoga.FLEX_DIRECTION_ROW
      : Yoga.FLEX_DIRECTION_COLUMN
  )

  // Justify content
  node.setJustifyContent(
    props.justifyContent === 'center'
      ? Yoga.JUSTIFY_CENTER
      : props.justifyContent === 'flex-end'
      ? Yoga.JUSTIFY_FLEX_END
      : Yoga.JUSTIFY_FLEX_START
  )

  // Dimensions
  if (props.width) {
    node.setWidth(props.width)
  }
  if (props.height) {
    node.setHeight(props.height)
  }

  // Padding
  node.setPadding(Yoga.EDGE_ALL, props.padding || 0)

  // Margin
  node.setMargin(Yoga.EDGE_ALL, props.margin || 0)
}
Layout Calculation
TypeScript
// ink/layout/engine.ts
export function calculateLayout(rootNode: InkNode, terminalWidth: number, terminalHeight: number): void {
  // 1. Set root constraints
  rootNode.yogaNode.setWidth(terminalWidth)
  rootNode.yogaNode.setHeight(terminalHeight)

  // 2. Apply styles to all nodes
  applyStylesRecursive(rootNode)

  // 3. Calculate layout
  rootNode.yogaNode.calculateLayout(
    terminalWidth,
    terminalHeight,
    Yoga.DIRECTION_LTR
  )

  // 4. Read computed positions
  readLayoutRecursive(rootNode)
}

function readLayoutRecursive(node: InkNode): void {
  node.layout = {
    x: node.yogaNode.getComputedLeft(),
    y: node.yogaNode.getComputedTop(),
    width: node.yogaNode.getComputedWidth(),
    height: node.yogaNode.getComputedHeight(),
  }

  for (const child of node.children) {
    readLayoutRecursive(child)
  }
}

The Reconciler

React's reconciler decides what needs to update. Ink provides a custom reconciler.

TypeScript
// ink/reconciler.ts
import Reconciler from 'react-reconciler'

const InkReconciler = Reconciler({
  // Create a new Ink node
  createInstance(type, props) {
    return {
      type,
      props,
      children: [],
      yogaNode: createYogaNode(),
      layout: { x: 0, y: 0, width: 0, height: 0 },
    }
  },

  // Append child to parent
  appendChildToContainer(container, child) {
    container.children.push(child)
    container.yogaNode.insertChild(child.yogaNode, container.children.length - 1)
  },

  // Remove child
  removeChildFromContainer(container, child) {
    const index = container.children.indexOf(child)
    container.children.splice(index, 1)
    container.yogaNode.removeChild(child.yogaNode)
  },

  // Update props
  commitUpdate(instance, updatePayload, type, oldProps, newProps) {
    instance.props = newProps
    // Mark for re-layout
    scheduleLayoutUpdate()
  },

  // ... other reconciler methods
})

Rendering to Screen

ANSI Escape Codes
TypeScript
// ink/render-to-screen.ts
const ANSI = {
  // Cursor
  CURSOR_HOME: '\x1b[H',
  CURSOR_UP: (n: number) => `\x1b[${n}A`,
  CURSOR_DOWN: (n: number) => `\x1b[${n}B`,
  CURSOR_FORWARD: (n: number) => `\x1b[${n}C`,
  CURSOR_BACK: (n: number) => `\x1b[${n}D`,
  CURSOR_POSITION: (row: number, col: number) => `\x1b[${row};${col}H`,

  // Clear
  CLEAR_SCREEN: '\x1b[2J',
  CLEAR_LINE: '\x1b[2K',
  CLEAR_LINE_RIGHT: '\x1b[0K',

  // Styles
  RESET: '\x1b[0m',
  BOLD: '\x1b[1m',
  DIM: '\x1b[2m',
  ITALIC: '\x1b[3m',
  UNDERLINE: '\x1b[4m',

  // Colors (16-color)
  FG_BLACK: '\x1b[30m',
  FG_RED: '\x1b[31m',
  FG_GREEN: '\x1b[32m',
  FG_YELLOW: '\x1b[33m',
  FG_BLUE: '\x1b[34m',
  FG_MAGENTA: '\x1b[35m',
  FG_CYAN: '\x1b[36m',
  FG_WHITE: '\x1b[37m',

  // 256 colors
  FG_COLOR_256: (n: number) => `\x1b[38;5;${n}m`,
  BG_COLOR_256: (n: number) => `\x1b[48;5;${n}m`,

  // True color (24-bit)
  FG_RGB: (r: number, g: number, b: number) => `\x1b[38;2;${r};${g};${b}m`,
  BG_RGB: (r: number, g: number, b: number) => `\x1b[48;2;${r};${g};${b}m`,
}
Screen Rendering
TypeScript
// ink/render-node-to-output.ts
export function renderNodeToOutput(node: InkNode): string {
  if (node.type === 'text') {
    return renderText(node)
  }

  if (node.type === 'box') {
    return renderBox(node)
  }

  // Custom components - render children
  return node.children.map(renderNodeToOutput).join('')
}

function renderText(node: InkNode): string {
  const { color, bold, dimColor } = node.props
  let output = node.props.children

  // Apply styles
  let prefix = ''
  if (bold) prefix += ANSI.BOLD
  if (dimColor) prefix += ANSI.DIM
  if (color) prefix += getColorCode(color)

  const suffix = prefix ? ANSI.RESET : ''

  return prefix + output + suffix
}

function renderBox(node: InkNode): string {
  const { borderStyle, borderColor } = node.props
  const { width, height } = node.layout

  let output = ''

  // Top border
  if (borderStyle) {
    const borderChars = getBorderChars(borderStyle)
    const borderLine = borderChars.topLeft +
      borderChars.horizontal.repeat(width - 2) +
      borderChars.topRight

    output += applyBorderStyle(borderLine, borderColor) + '\n'
  }

  // Content rows
  for (let y = 0; y < height; y++) {
    let row = ''

    // Left border
    if (borderStyle) row += getBorderChars(borderStyle).vertical

    // Content at this row
    const rowContent = getContentAtRow(node, y)
    row += rowContent.padEnd(width - (borderStyle ? 2 : 0))

    // Right border
    if (borderStyle) row += getBorderChars(borderStyle).vertical

    output += row + '\n'
  }

  // Bottom border
  if (borderStyle) {
    const borderChars = getBorderChars(borderStyle)
    const borderLine = borderChars.bottomLeft +
      borderChars.horizontal.repeat(width - 2) +
      borderChars.bottomRight

    output += applyBorderStyle(borderLine, borderColor)
  }

  return output
}

Efficient Screen Updates

Diff-Based Updates

Instead of redrawing everything, Ink only updates changed cells:

TypeScript
// ink/log-update.ts
let previousOutput: string = ''

export function updateScreen(newOutput: string): void {
  const diff = calculateDiff(previousOutput, newOutput)

  for (const { x, y, char, styles } of diff) {
    // Move cursor to position
    stdout.write(ANSI.CURSOR_POSITION(y + 1, x + 1))

    // Apply styles and write char
    if (styles) stdout.write(styles)
    stdout.write(char)
    if (styles) stdout.write(ANSI.RESET)
  }

  previousOutput = newOutput
}

function calculateDiff(old: string, new_: string): DiffItem[] {
  const oldLines = old.split('\n')
  const newLines = new_.split('\n')
  const diff: DiffItem[] = []

  for (let y = 0; y < newLines.length; y++) {
    const oldLine = oldLines[y] || ''
    const newLine = newLines[y]

    for (let x = 0; x < newLine.length; x++) {
      if (oldLine[x] !== newLine[x]) {
        diff.push({ x, y, char: newLine[x] })
      }
    }
  }

  return diff
}
Alternate Screen Buffer

For fullscreen apps, use alternate buffer (like vim):

TypeScript
// ink/components/AlternateScreen.tsx
export function AlternateScreen({ children }: { children: ReactNode }) {
  useEffect(() => {
    // Enter alternate screen
    stdout.write('\x1b[?1049h')

    // Hide cursor
    stdout.write('\x1b[?25l')

    // Enable mouse
    stdout.write('\x1b[?1000h')

    return () => {
      // Exit alternate screen
      stdout.write('\x1b[?1049l')

      // Show cursor
      stdout.write('\x1b[?25h')

      // Disable mouse
      stdout.write('\x1b[?1000l')
    }
  }, [])

  return <>{children}</>
}

Input Handling

Raw Mode
TypeScript
// ink/termio.ts
export function enableRawMode(): void {
  stdin.setRawMode(true)
  stdin.resume()
  stdin.setEncoding('utf8')
}

export function disableRawMode(): void {
  stdin.setRawMode(false)
  stdin.pause()
}
Key Parsing
TypeScript
// ink/parse-keypress.ts
export function parseKeypress(buffer: Buffer): KeypressEvent {
  const str = buffer.toString()

  // Escape sequences
  if (str.startsWith('\x1b[')) {
    const code = str.slice(2)

    // Arrow keys
    if (code === 'A') return { key: 'up' }
    if (code === 'B') return { key: 'down' }
    if (code === 'C') return { key: 'right' }
    if (code === 'D') return { key: 'left' }

    // Home/End
    if (code === 'H') return { key: 'home' }
    if (code === 'F') return { key: 'end' }

    // Function keys
    if (code.startsWith('1;')) return parseFunctionKey(code)
  }

  // Ctrl+<key>
  if (str.charCodeAt(0) < 32) {
    return {
      key: String.fromCharCode(str.charCodeAt(0) + 96),
      ctrl: true,
    }
  }

  // Regular character
  return { key: str }
}
useInput Hook
TypeScript
// ink/hooks/use-input.ts
export function useInput(handler: InputHandler): void {
  const { stdin } = useStdin()

  useEffect(() => {
    const onData = (data: Buffer) => {
      const keypress = parseKeypress(data)
      handler(keypress.key, {
        ctrl: keypress.ctrl,
        shift: keypress.shift,
        meta: keypress.meta,
      })
    }

    stdin.on('data', onData)
    return () => stdin.off('data', onData)
  }, [handler])
}

Terminal Size Handling

TypeScript
// ink/hooks/use-terminal-viewport.ts
export function useTerminalViewport() {
  const [size, setSize] = useState({
    width: process.stdout.columns || 80,
    height: process.stdout.rows || 24,
  })

  useEffect(() => {
    const onResize = () => {
      setSize({
        width: process.stdout.columns || 80,
        height: process.stdout.rows || 24,
      })
    }

    process.stdout.on('resize', onResize)
    return () => process.stdout.off('resize', onResize)
  }, [])

  return size
}

Performance Optimizations

1. Batching Updates
TypeScript
// ink/frame.ts
let scheduledUpdate = false

export function scheduleUpdate(): void {
  if (scheduledUpdate) return

  scheduledUpdate = true
  requestAnimationFrame(() => {
    scheduledUpdate = false
    performUpdate()
  })
}
2. Component Memoization
TypeScript
// ink/components/Text.tsx
import React from 'react'

export const Text = React.memo(function Text({ children, ...props }: TextProps) {
  // Memo prevents re-render if props unchanged
  return <ink-text {...props}>{children}</ink-text>
})
3. Lazy Text Measurement
TypeScript
// ink/stringWidth.ts
import { getEastAsianWidth } from 'get-east-asian-width'

export function stringWidth(str: string): number {
  let width = 0

  for (const char of str) {
    const code = char.codePointAt(0) || 0

    // Control characters
    if (code < 32 || (code >= 0x7f && code < 0xa0)) {
      continue
    }

    // Wide characters (CJK, emoji)
    if (getEastAsianWidth(code) === 'W' || getEastAsianWidth(code) === 'F') {
      width += 2
    } else {
      width += 1
    }
  }

  return width
}

Text Wrapping

TypeScript
// ink/wrap-text.ts
export function wrapText(text: string, maxWidth: number): string[] {
  const words = text.split(/\s+/)
  const lines: string[] = []
  let currentLine = ''

  for (const word of words) {
    const wordWidth = stringWidth(word)
    const lineWidth = stringWidth(currentLine)

    if (lineWidth + wordWidth + 1 <= maxWidth) {
      currentLine += (currentLine ? ' ' : '') + word
    } else {
      lines.push(currentLine)
      currentLine = word
    }
  }

  if (currentLine) {
    lines.push(currentLine)
  }

  return lines
}

Key Concepts

  1. Yoga Layout: Same as CSS flexbox, but for terminal
  2. Diff Rendering: Only changed cells update
  3. ANSI Codes: Control terminal cursor, colors, styles
  4. Raw Mode: Get individual keystrokes, not line input
  5. Reconciler: React decides what changed, Ink renders it

Debugging Ink

Bash
# Debug rendering
DEBUG=ink claude

# See ANSI codes
script -q /dev/null claude | cat -v

# Measure FPS
CLAUDE_CODE_DEBUG_FPS=1 claude