Full Code of vadimdemedes/ink for AI

master 243e962f7e4f cached
225 files
746.9 KB
214.7k tokens
507 symbols
1 requests
Download .txt
Showing preview only (802K chars total). Download the full file or copy to clipboard to get everything.
Repository: vadimdemedes/ink
Branch: master
Commit: 243e962f7e4f
Files: 225
Total size: 746.9 KB

Directory structure:
gitextract_jq_9codb/

├── .editorconfig
├── .gitattributes
├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .npmrc
├── benchmark/
│   ├── simple/
│   │   ├── index.ts
│   │   └── simple.tsx
│   └── static/
│       ├── index.ts
│       └── static.tsx
├── examples/
│   ├── alternate-screen/
│   │   ├── alternate-screen.tsx
│   │   └── index.ts
│   ├── aria/
│   │   ├── aria.tsx
│   │   └── index.ts
│   ├── borders/
│   │   ├── borders.tsx
│   │   └── index.ts
│   ├── box-backgrounds/
│   │   ├── box-backgrounds.tsx
│   │   └── index.ts
│   ├── chat/
│   │   ├── chat.tsx
│   │   └── index.ts
│   ├── concurrent-suspense/
│   │   ├── concurrent-suspense.tsx
│   │   └── index.ts
│   ├── counter/
│   │   ├── counter.tsx
│   │   └── index.ts
│   ├── cursor-ime/
│   │   ├── cursor-ime.tsx
│   │   └── index.ts
│   ├── incremental-rendering/
│   │   ├── incremental-rendering.tsx
│   │   └── index.ts
│   ├── jest/
│   │   ├── index.ts
│   │   ├── jest.tsx
│   │   ├── summary.tsx
│   │   └── test.tsx
│   ├── justify-content/
│   │   ├── index.ts
│   │   └── justify-content.tsx
│   ├── render-throttle/
│   │   └── index.tsx
│   ├── router/
│   │   ├── index.ts
│   │   └── router.tsx
│   ├── select-input/
│   │   ├── index.ts
│   │   └── select-input.tsx
│   ├── static/
│   │   ├── index.ts
│   │   └── static.tsx
│   ├── subprocess-output/
│   │   ├── index.ts
│   │   └── subprocess-output.tsx
│   ├── suspense/
│   │   ├── index.ts
│   │   └── suspense.tsx
│   ├── table/
│   │   ├── index.ts
│   │   └── table.tsx
│   ├── terminal-resize/
│   │   ├── index.ts
│   │   └── terminal-resize.tsx
│   ├── use-focus/
│   │   ├── index.ts
│   │   └── use-focus.tsx
│   ├── use-focus-with-id/
│   │   ├── index.ts
│   │   └── use-focus-with-id.tsx
│   ├── use-input/
│   │   ├── index.ts
│   │   └── use-input.tsx
│   ├── use-stderr/
│   │   ├── index.ts
│   │   └── use-stderr.tsx
│   ├── use-stdout/
│   │   ├── index.ts
│   │   └── use-stdout.tsx
│   └── use-transition/
│       ├── index.ts
│       └── use-transition.tsx
├── license
├── media/
│   └── demo.js
├── package.json
├── readme.md
├── recipes/
│   └── routing.md
├── src/
│   ├── ansi-tokenizer.ts
│   ├── colorize.ts
│   ├── components/
│   │   ├── AccessibilityContext.ts
│   │   ├── App.tsx
│   │   ├── AppContext.ts
│   │   ├── BackgroundContext.ts
│   │   ├── Box.tsx
│   │   ├── CursorContext.ts
│   │   ├── ErrorBoundary.tsx
│   │   ├── ErrorOverview.tsx
│   │   ├── FocusContext.ts
│   │   ├── Newline.tsx
│   │   ├── Spacer.tsx
│   │   ├── Static.tsx
│   │   ├── StderrContext.ts
│   │   ├── StdinContext.ts
│   │   ├── StdoutContext.ts
│   │   ├── Text.tsx
│   │   └── Transform.tsx
│   ├── cursor-helpers.ts
│   ├── devtools-window-polyfill.ts
│   ├── devtools.ts
│   ├── dom.ts
│   ├── get-max-width.ts
│   ├── global.d.ts
│   ├── hooks/
│   │   ├── use-app.ts
│   │   ├── use-box-metrics.ts
│   │   ├── use-cursor.ts
│   │   ├── use-focus-manager.ts
│   │   ├── use-focus.ts
│   │   ├── use-input.ts
│   │   ├── use-is-screen-reader-enabled.ts
│   │   ├── use-paste.ts
│   │   ├── use-stderr.ts
│   │   ├── use-stdin.ts
│   │   ├── use-stdout.ts
│   │   └── use-window-size.ts
│   ├── index.ts
│   ├── ink.tsx
│   ├── input-parser.ts
│   ├── instances.ts
│   ├── kitty-keyboard.ts
│   ├── log-update.ts
│   ├── measure-element.ts
│   ├── measure-text.ts
│   ├── output.ts
│   ├── parse-keypress.ts
│   ├── reconciler.ts
│   ├── render-background.ts
│   ├── render-border.ts
│   ├── render-node-to-output.ts
│   ├── render-to-string.ts
│   ├── render.ts
│   ├── renderer.ts
│   ├── sanitize-ansi.ts
│   ├── squash-text-nodes.ts
│   ├── styles.ts
│   ├── utils.ts
│   ├── wrap-text.ts
│   └── write-synchronized.ts
├── test/
│   ├── alternate-screen-example.tsx
│   ├── ansi-tokenizer.ts
│   ├── background.tsx
│   ├── borders.tsx
│   ├── components.tsx
│   ├── cursor-helpers.tsx
│   ├── cursor.tsx
│   ├── display.tsx
│   ├── errors.tsx
│   ├── exit.tsx
│   ├── fixtures/
│   │   ├── alternate-screen-full-board-win.tsx
│   │   ├── ci-debug-after-exit.tsx
│   │   ├── ci-debug.tsx
│   │   ├── ci.tsx
│   │   ├── clear.tsx
│   │   ├── console.tsx
│   │   ├── erase-with-state-change.tsx
│   │   ├── erase-with-static.tsx
│   │   ├── erase.tsx
│   │   ├── exit-double-raw-mode.tsx
│   │   ├── exit-normally.tsx
│   │   ├── exit-on-exit-with-error-value-property.tsx
│   │   ├── exit-on-exit-with-error.tsx
│   │   ├── exit-on-exit-with-result.tsx
│   │   ├── exit-on-exit-with-value-object.tsx
│   │   ├── exit-on-exit.tsx
│   │   ├── exit-on-finish.tsx
│   │   ├── exit-on-unmount.tsx
│   │   ├── exit-raw-on-exit-with-error.tsx
│   │   ├── exit-raw-on-exit.tsx
│   │   ├── exit-raw-on-unmount.tsx
│   │   ├── exit-with-static.tsx
│   │   ├── exit-with-thrown-error.tsx
│   │   ├── fullscreen-no-extra-newline.tsx
│   │   ├── issue-442-full-height.tsx
│   │   ├── issue-450-fixture-helpers.tsx
│   │   ├── issue-450-full-height-rerender-with-marker.tsx
│   │   ├── issue-450-full-height-rerender.tsx
│   │   ├── issue-450-full-height-with-static-rerender.tsx
│   │   ├── issue-450-grow-to-fullscreen-rerender.tsx
│   │   ├── issue-450-grow-to-overflow-rerender.tsx
│   │   ├── issue-450-height-minus-one-rerender.tsx
│   │   ├── issue-450-initial-fullscreen.tsx
│   │   ├── issue-450-initial-overflow.tsx
│   │   ├── issue-450-shrink-from-fullscreen-rerender.tsx
│   │   ├── issue-450-shrink-from-overflow-rerender.tsx
│   │   ├── issue-450-static-shrink-from-fullscreen-rerender.tsx
│   │   ├── issue-725-child-process.tsx
│   │   ├── use-input-ctrl-c.tsx
│   │   ├── use-input-discrete-priority.tsx
│   │   ├── use-input-kitty.tsx
│   │   ├── use-input-many.tsx
│   │   ├── use-input-multiple.tsx
│   │   ├── use-input.tsx
│   │   ├── use-paste.tsx
│   │   └── use-stdout.tsx
│   ├── flex-align-content.tsx
│   ├── flex-align-items.tsx
│   ├── flex-align-self.tsx
│   ├── flex-direction.tsx
│   ├── flex-justify-content.tsx
│   ├── flex-wrap.tsx
│   ├── flex.tsx
│   ├── focus.tsx
│   ├── gap.tsx
│   ├── helpers/
│   │   ├── create-stdin.ts
│   │   ├── create-stdout.ts
│   │   ├── force-colors.ts
│   │   ├── render-to-string.ts
│   │   ├── run.ts
│   │   ├── term.ts
│   │   └── test-renderer.ts
│   ├── hooks-use-input-kitty.tsx
│   ├── hooks-use-input-navigation.tsx
│   ├── hooks-use-input.tsx
│   ├── hooks-use-paste.tsx
│   ├── hooks.tsx
│   ├── input-parser.ts
│   ├── kitty-keyboard.tsx
│   ├── log-update.tsx
│   ├── margin.tsx
│   ├── measure-element.tsx
│   ├── measure-text.tsx
│   ├── overflow.tsx
│   ├── padding.tsx
│   ├── position.tsx
│   ├── reconciler.tsx
│   ├── render-to-string.tsx
│   ├── render.tsx
│   ├── sanitize-ansi.ts
│   ├── screen-reader.tsx
│   ├── terminal-resize.tsx
│   ├── text-width.tsx
│   ├── text.tsx
│   ├── tsconfig.json
│   ├── use-box-metrics.tsx
│   ├── width-height.tsx
│   └── write-synchronized.tsx
├── tsconfig.json
└── xo.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.yml]
indent_style = space
indent_size = 2


================================================
FILE: .gitattributes
================================================
* text=auto eol=lf


================================================
FILE: .github/workflows/test.yml
================================================
name: test
on: [push, pull_request]
jobs:
  test:
    name: Node.js ${{ matrix.node_version }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node_version:
          - 24
          - 22
          - 20
    steps:
      - uses: actions/checkout@v6
      - name: Use Node.js ${{ matrix.node_version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node_version }}
      - run: npm install
      - run: npm test -- --serial
        env:
          FORCE_COLOR: true
          CI: false


================================================
FILE: .gitignore
================================================
node_modules
/build


================================================
FILE: .npmrc
================================================
package-lock=false


================================================
FILE: benchmark/simple/index.ts
================================================
import './simple.js';


================================================
FILE: benchmark/simple/simple.tsx
================================================
import React from 'react';
import {render, Box, Text} from '../../src/index.js';

function App() {
	return (
		<Box flexDirection="column" padding={1}>
			<Text underline bold color="red">
				{/* eslint-disable-next-line react/jsx-curly-brace-presence */}
				{'Hello World'}
			</Text>

			<Box marginTop={1} width={60}>
				<Text>
					Cupcake ipsum dolor sit amet candy candy. Sesame snaps cookie I love
					tootsie roll apple pie bonbon wafer. Caramels sesame snaps icing
					cotton candy I love cookie sweet roll. I love bonbon sweet.
				</Text>
			</Box>

			<Box marginTop={1} flexDirection="column">
				<Text backgroundColor="white" color="black">
					Colors:
				</Text>

				<Box flexDirection="column" paddingLeft={1}>
					<Text>
						- <Text color="red">Red</Text>
					</Text>
					<Text>
						- <Text color="blue">Blue</Text>
					</Text>
					<Text>
						- <Text color="green">Green</Text>
					</Text>
				</Box>
			</Box>
		</Box>
	);
}

const {rerender} = render(<App />);

for (let index = 0; index < 100_000; index++) {
	rerender(<App />);
}


================================================
FILE: benchmark/static/index.ts
================================================
import './static.js';


================================================
FILE: benchmark/static/static.tsx
================================================
import React from 'react';
import {render, Box, Text, Static} from '../../src/index.js';

function App() {
	const [items, setItems] = React.useState<
		Array<{
			id: number;
		}>
	>([]);
	const itemCountReference = React.useRef(0);

	React.useEffect(() => {
		let timer: NodeJS.Timeout | undefined;

		const run = () => {
			if (itemCountReference.current++ > 1000) {
				return;
			}

			setItems(previousItems => [
				...previousItems,
				{
					id: previousItems.length,
				},
			]);

			timer = setTimeout(run, 10);
		};

		run();

		return () => {
			clearTimeout(timer);
		};
	}, []);

	return (
		<Box flexDirection="column">
			<Static items={items}>
				{(item, index) => (
					<Box key={item.id} padding={1} flexDirection="column">
						<Text color="green">Item #{index}</Text>
						<Text>Item content</Text>
					</Box>
				)}
			</Static>

			<Box flexDirection="column" padding={1}>
				<Text underline bold color="red">
					{/* eslint-disable-next-line react/jsx-curly-brace-presence */}
					{'Hello World'}
				</Text>

				<Text>Rendered: {items.length}</Text>

				<Box marginTop={1} width={60}>
					<Text>
						Cupcake ipsum dolor sit amet candy candy. Sesame snaps cookie I love
						tootsie roll apple pie bonbon wafer. Caramels sesame snaps icing
						cotton candy I love cookie sweet roll. I love bonbon sweet.
					</Text>
				</Box>

				<Box marginTop={1} flexDirection="column">
					<Text backgroundColor="white" color="black">
						Colors:
					</Text>

					<Box flexDirection="column" paddingLeft={1}>
						<Text>
							- <Text color="red">Red</Text>
						</Text>
						<Text>
							- <Text color="blue">Blue</Text>
						</Text>
						<Text>
							- <Text color="green">Green</Text>
						</Text>
					</Box>
				</Box>
			</Box>
		</Box>
	);
}

render(<App />);


================================================
FILE: examples/alternate-screen/alternate-screen.tsx
================================================
import React, {useReducer, useEffect, useRef, useCallback} from 'react';
import {
	render,
	Text,
	Box,
	useInput,
	useApp,
	useWindowSize,
} from '../../src/index.js';

type Point = {
	x: number;
	y: number;
};

type Direction = 'up' | 'down' | 'left' | 'right';

type GameState = {
	snake: Point[];
	food: Point;
	score: number;
	gameOver: boolean;
	won: boolean;
	frame: number;
};

type Action = {type: 'tick'; direction: Direction} | {type: 'restart'};

const headCharacter = '🦄';
const bodyCharacter = '✨';
const foodCharacter = '🌈';
const emptyCell = '  ';
const tickMs = 150;

const boardWidth = 20;
const boardHeight = 15;

const opposites: Record<Direction, Direction> = {
	up: 'down',
	down: 'up',
	left: 'right',
	right: 'left',
};

const offsets: Record<Direction, Point> = {
	up: {x: 0, y: -1},
	down: {x: 0, y: 1},
	left: {x: -1, y: 0},
	right: {x: 1, y: 0},
};

const rainbowColors = [
	'red',
	'#FF7F00',
	'yellow',
	'green',
	'cyan',
	'blue',
	'magenta',
] as const;

const borderH = '─'.repeat(boardWidth * 2);
const borderTop = `┌${borderH}┐`;
const borderBottom = `└${borderH}┘`;
const boardWidthChars = boardWidth * 2 + 2;

const initialSnake: Point[] = [
	{x: 10, y: 7},
	{x: 9, y: 7},
	{x: 8, y: 7},
];

function randomPosition(exclude: Point[]): Point {
	let point = {
		x: 0,
		y: 0,
	};
	let isExcluded = true;

	while (isExcluded) {
		point = {
			x: Math.floor(Math.random() * boardWidth),
			y: Math.floor(Math.random() * boardHeight),
		};

		isExcluded = false;
		for (const segment of exclude) {
			if (segment.x === point.x && segment.y === point.y) {
				isExcluded = true;
				break;
			}
		}
	}

	return point;
}

function createInitialState(): GameState {
	return {
		snake: initialSnake,
		food: randomPosition(initialSnake),
		score: 0,
		gameOver: false,
		won: false,
		frame: 0,
	};
}

export function gameReducer(state: GameState, action: Action): GameState {
	if (action.type === 'restart') {
		return createInitialState();
	}

	if (state.gameOver) {
		return state;
	}

	const head = state.snake[0]!;
	const offset = offsets[action.direction];
	const newHead: Point = {x: head.x + offset.x, y: head.y + offset.y};

	// Wall collision
	if (
		newHead.x < 0 ||
		newHead.x >= boardWidth ||
		newHead.y < 0 ||
		newHead.y >= boardHeight
	) {
		return {...state, gameOver: true, won: false};
	}

	const ateFood = newHead.x === state.food.x && newHead.y === state.food.y;
	const collisionSegments = ateFood ? state.snake : state.snake.slice(0, -1);

	if (
		collisionSegments.some(
			segment => segment.x === newHead.x && segment.y === newHead.y,
		)
	) {
		return {...state, gameOver: true, won: false};
	}

	const newSnake = [newHead, ...state.snake];

	if (!ateFood) {
		newSnake.pop();
	}

	if (ateFood && newSnake.length === boardWidth * boardHeight) {
		return {
			snake: newSnake,
			food: state.food,
			score: state.score + 1,
			gameOver: true,
			won: true,
			frame: state.frame + 1,
		};
	}

	return {
		snake: newSnake,
		food: ateFood ? randomPosition(newSnake) : state.food,
		score: state.score + (ateFood ? 1 : 0),
		gameOver: false,
		won: false,
		frame: state.frame + 1,
	};
}

function buildBoard(snake: Point[], food: Point): string {
	const headKey = `${snake[0]!.x},${snake[0]!.y}`;
	const snakeSet = new Set(snake.map(segment => `${segment.x},${segment.y}`));

	const rows: string[] = [borderTop];
	for (let y = 0; y < boardHeight; y++) {
		let row = '│';
		for (let x = 0; x < boardWidth; x++) {
			const key = `${x},${y}`;
			if (key === headKey) {
				row += headCharacter;
			} else if (snakeSet.has(key)) {
				row += bodyCharacter;
			} else if (food.x === x && food.y === y) {
				row += foodCharacter;
			} else {
				row += emptyCell;
			}
		}

		row += '│';
		rows.push(row);
	}

	rows.push(borderBottom);
	return rows.join('\n');
}

function SnakeGame() {
	const {exit} = useApp();
	const {columns} = useWindowSize();
	const [game, dispatch] = useReducer(
		gameReducer,
		undefined,
		createInitialState,
	);
	const directionReference = useRef<Direction>('right');

	const tick = useCallback(() => {
		dispatch({type: 'tick', direction: directionReference.current});
	}, []);

	useEffect(() => {
		const timer = setInterval(tick, tickMs);
		return () => {
			clearInterval(timer);
		};
	}, [tick]);

	useInput((input, key) => {
		if (input === 'q') {
			exit();
		}

		if (game.gameOver && input === 'r') {
			directionReference.current = 'right';
			dispatch({type: 'restart'});
			return;
		}

		if (game.gameOver) {
			return;
		}

		const {current} = directionReference;
		if (key.upArrow && current !== 'down') {
			directionReference.current = 'up';
		} else if (key.downArrow && current !== 'up') {
			directionReference.current = 'down';
		} else if (key.leftArrow && current !== 'right') {
			directionReference.current = 'left';
		} else if (key.rightArrow && current !== 'left') {
			directionReference.current = 'right';
		}
	});

	const titleColor = rainbowColors[game.frame % rainbowColors.length]!;
	const board = buildBoard(game.snake, game.food);
	const marginLeft = Math.max(Math.floor((columns - boardWidthChars) / 2), 0);

	return (
		<Box flexDirection="column" paddingY={1}>
			<Box justifyContent="center">
				<Text bold color={titleColor}>
					🦄 Unicorn Snake 🦄
				</Text>
			</Box>

			<Box justifyContent="center" marginTop={1}>
				<Text bold color="yellow">
					Score: {game.score}
				</Text>
			</Box>

			<Box marginLeft={marginLeft} marginTop={1}>
				<Text>{board}</Text>
			</Box>

			{game.gameOver ? (
				<Box justifyContent="center" marginTop={1}>
					<Text bold color="red">
						{game.won ? 'You Win!' : 'Game Over!'}{' '}
					</Text>
					<Text dimColor>r: restart | q: quit</Text>
				</Box>
			) : (
				<Box justifyContent="center" marginTop={1}>
					<Text dimColor>
						Arrow keys: move | Eat {foodCharacter} to grow | q: quit
					</Text>
				</Box>
			)}
		</Box>
	);
}

export function runAlternateScreenExample() {
	render(<SnakeGame />, {alternateScreen: true});
}


================================================
FILE: examples/alternate-screen/index.ts
================================================
import {runAlternateScreenExample} from './alternate-screen.js';

runAlternateScreenExample();


================================================
FILE: examples/aria/aria.tsx
================================================
import React, {useState} from 'react';
import {render, Text, Box, useInput} from '../../src/index.js';

function AriaExample() {
	const [checked, setChecked] = useState(false);

	useInput(key => {
		if (key === ' ') {
			setChecked(!checked);
		}
	});

	return (
		<Box flexDirection="column">
			<Text>
				Press spacebar to toggle the checkbox. This example is best experienced
				with a screen reader.
			</Text>
			<Box marginTop={1}>
				<Box aria-role="checkbox" aria-state={{checked}}>
					<Text>{checked ? '[x]' : '[ ]'}</Text>
				</Box>
			</Box>
			<Box marginTop={1}>
				<Text aria-hidden="true">This text is hidden from screen readers.</Text>
			</Box>
		</Box>
	);
}

render(<AriaExample />);


================================================
FILE: examples/aria/index.ts
================================================
import './aria.js';


================================================
FILE: examples/borders/borders.tsx
================================================
import React from 'react';
import {render, Box, Text} from '../../src/index.js';

function Borders() {
	return (
		<Box flexDirection="column" padding={2}>
			<Box>
				<Box borderStyle="single" marginRight={2}>
					<Text>single</Text>
				</Box>

				<Box borderStyle="double" marginRight={2}>
					<Text>double</Text>
				</Box>

				<Box borderStyle="round" marginRight={2}>
					<Text>round</Text>
				</Box>

				<Box borderStyle="bold">
					<Text>bold</Text>
				</Box>
			</Box>

			<Box marginTop={1}>
				<Box borderStyle="singleDouble" marginRight={2}>
					<Text>singleDouble</Text>
				</Box>

				<Box borderStyle="doubleSingle" marginRight={2}>
					<Text>doubleSingle</Text>
				</Box>

				<Box borderStyle="classic">
					<Text>classic</Text>
				</Box>
			</Box>
		</Box>
	);
}

render(<Borders />);


================================================
FILE: examples/borders/index.ts
================================================
import './borders.js';


================================================
FILE: examples/box-backgrounds/box-backgrounds.tsx
================================================
import React from 'react';
import {Box, Text} from '../../src/index.js';

function BoxBackgrounds() {
	return (
		<Box flexDirection="column" gap={1}>
			<Text bold>Box Background Examples:</Text>

			<Box>
				<Text>1. Standard red background (10x3):</Text>
			</Box>
			<Box backgroundColor="red" width={10} height={3} alignSelf="flex-start">
				<Text>Hello</Text>
			</Box>

			<Box>
				<Text>2. Blue background with border (12x4):</Text>
			</Box>
			<Box
				backgroundColor="blue"
				borderStyle="round"
				width={12}
				height={4}
				alignSelf="flex-start"
			>
				<Text>Border</Text>
			</Box>

			<Box>
				<Text>3. Green background with padding (14x4):</Text>
			</Box>
			<Box
				backgroundColor="green"
				padding={1}
				width={14}
				height={4}
				alignSelf="flex-start"
			>
				<Text>Padding</Text>
			</Box>

			<Box>
				<Text>4. Yellow background with center alignment (16x3):</Text>
			</Box>
			<Box
				backgroundColor="yellow"
				width={16}
				height={3}
				justifyContent="center"
				alignSelf="flex-start"
			>
				<Text>Centered</Text>
			</Box>

			<Box>
				<Text>5. Magenta background, column layout (12x5):</Text>
			</Box>
			<Box
				backgroundColor="magenta"
				flexDirection="column"
				width={12}
				height={5}
				alignSelf="flex-start"
			>
				<Text>Line 1</Text>
				<Text>Line 2</Text>
			</Box>

			<Box>
				<Text>6. Hex color background #FF8800 (10x3):</Text>
			</Box>
			<Box
				backgroundColor="#FF8800"
				width={10}
				height={3}
				alignSelf="flex-start"
			>
				<Text>Hex</Text>
			</Box>

			<Box>
				<Text>7. RGB background rgb(0,255,0) (10x3):</Text>
			</Box>
			<Box
				backgroundColor="rgb(0,255,0)"
				width={10}
				height={3}
				alignSelf="flex-start"
			>
				<Text>RGB</Text>
			</Box>

			<Box>
				<Text>8. Text inheritance test:</Text>
			</Box>
			<Box backgroundColor="cyan" alignSelf="flex-start">
				<Text>Inherited </Text>
				<Text backgroundColor="red">Override </Text>
				<Text>Back to inherited</Text>
			</Box>

			<Box>
				<Text>9. Nested background inheritance:</Text>
			</Box>
			<Box backgroundColor="blue" alignSelf="flex-start">
				<Text>Outer: </Text>
				<Box backgroundColor="yellow">
					<Text>Inner: </Text>
					<Text backgroundColor="red">Deep</Text>
				</Box>
			</Box>

			<Box marginTop={1}>
				<Text>Press Ctrl+C to exit</Text>
			</Box>
		</Box>
	);
}

export default BoxBackgrounds;


================================================
FILE: examples/box-backgrounds/index.ts
================================================
#!/usr/bin/env node
import React from 'react';
import {render} from '../../src/index.js';
import BoxBackgrounds from './box-backgrounds.js';

render(React.createElement(BoxBackgrounds));


================================================
FILE: examples/chat/chat.tsx
================================================
import React, {useState} from 'react';
import {render, Text, Box, useInput} from '../../src/index.js';

let messageId = 0;

function ChatApp() {
	const [input, setInput] = useState('');

	const [messages, setMessages] = useState<
		Array<{
			id: number;
			text: string;
		}>
	>([]);

	useInput((character, key) => {
		if (key.return) {
			if (input) {
				setMessages(previousMessages => [
					...previousMessages,
					{
						id: messageId++,
						text: `User: ${input}`,
					},
				]);
				setInput('');
			}
		} else if (key.backspace || key.delete) {
			setInput(currentInput => currentInput.slice(0, -1));
		} else {
			setInput(currentInput => currentInput + character);
		}
	});

	return (
		<Box flexDirection="column" padding={1}>
			<Box flexDirection="column">
				{messages.map(message => (
					<Text key={message.id}>{message.text}</Text>
				))}
			</Box>

			<Box marginTop={1}>
				<Text>Enter your message: {input}</Text>
			</Box>
		</Box>
	);
}

render(<ChatApp />);


================================================
FILE: examples/chat/index.ts
================================================
import './chat.js';


================================================
FILE: examples/concurrent-suspense/concurrent-suspense.tsx
================================================
import React, {Suspense, useState} from 'react';
import {render, Box, Text} from '../../src/index.js';

// Simulated async data fetching with cache
const cache = new Map<
	string,
	{status: string; data?: string; promise?: Promise<void>}
>();

function fetchData(key: string, delay: number): string {
	const cached = cache.get(key);

	if (cached?.status === 'resolved') {
		return cached.data!;
	}

	if (cached?.status === 'pending') {
		// eslint-disable-next-line @typescript-eslint/only-throw-error
		throw cached.promise;
	}

	// Start fetching
	const promise = new Promise<void>(resolve => {
		setTimeout(() => {
			cache.set(key, {
				status: 'resolved',
				data: `Data for "${key}" (fetched in ${delay}ms)`,
			});
			resolve();
		}, delay);
	});

	cache.set(key, {status: 'pending', promise});
	// eslint-disable-next-line @typescript-eslint/only-throw-error
	throw promise;
}

// Component that suspends while fetching
function DataItem({
	name,
	delay,
}: {
	readonly name: string;
	readonly delay: number;
}) {
	const data = fetchData(name, delay);
	return (
		<Box marginLeft={2}>
			<Text color="green">{data}</Text>
		</Box>
	);
}

// Loading fallback
function Loading({message}: {readonly message: string}) {
	return (
		<Box marginLeft={2}>
			<Text color="yellow">{message}</Text>
		</Box>
	);
}

// Main app demonstrating concurrent suspense
function App() {
	const [showMore, setShowMore] = useState(false);

	// Auto-trigger "show more" after 2 seconds
	React.useEffect(() => {
		const timer = setTimeout(() => {
			setShowMore(true);
		}, 2000);
		return () => {
			clearTimeout(timer);
		};
	}, []);

	return (
		<Box flexDirection="column">
			<Text bold underline>
				Concurrent Suspense Demo
			</Text>
			<Text dimColor>
				(With concurrent: true, Suspense re-renders automatically)
			</Text>
			<Box marginTop={1} />

			<Text>Fast data (200ms):</Text>
			<Suspense fallback={<Loading message="Loading fast data..." />}>
				<DataItem name="fast" delay={200} />
			</Suspense>

			<Box marginTop={1} />

			<Text>Medium data (800ms):</Text>
			<Suspense fallback={<Loading message="Loading medium data..." />}>
				<DataItem name="medium" delay={800} />
			</Suspense>

			<Box marginTop={1} />

			<Text>Slow data (1500ms):</Text>
			<Suspense fallback={<Loading message="Loading slow data..." />}>
				<DataItem name="slow" delay={1500} />
			</Suspense>

			{showMore ? (
				<>
					<Box marginTop={1} />
					<Text>Dynamically added (500ms):</Text>
					<Suspense fallback={<Loading message="Loading dynamic data..." />}>
						<DataItem name="dynamic" delay={500} />
					</Suspense>
				</>
			) : null}
		</Box>
	);
}

// Render with concurrent mode enabled
render(<App />, {concurrent: true});


================================================
FILE: examples/concurrent-suspense/index.ts
================================================
import './concurrent-suspense.js';


================================================
FILE: examples/counter/counter.tsx
================================================
import React from 'react';
import {render, Text} from '../../src/index.js';

function Counter() {
	const [counter, setCounter] = React.useState(0);

	React.useEffect(() => {
		const timer = setInterval(() => {
			setCounter(prevCounter => prevCounter + 1); // eslint-disable-line unicorn/prevent-abbreviations
		}, 100);

		return () => {
			clearInterval(timer);
		};
	}, []);

	return <Text color="green">{counter} tests passed</Text>;
}

render(<Counter />);


================================================
FILE: examples/counter/index.ts
================================================
import './counter.js';


================================================
FILE: examples/cursor-ime/cursor-ime.tsx
================================================
import React, {useState} from 'react';
import stringWidth from 'string-width';
import {render, Box, Text, useInput, useCursor} from '../../src/index.js';

function App() {
	const [text, setText] = useState('');
	const {setCursorPosition} = useCursor();

	useInput((input, key) => {
		if (key.backspace || key.delete) {
			setText(previous => previous.slice(0, -1));
			return;
		}

		if (!key.ctrl && !key.meta && input) {
			setText(previous => previous + input);
		}
	});

	// Use stringWidth for correct cursor position with wide characters (Korean, CJK, emoji)
	const prompt = '> ';
	setCursorPosition({x: stringWidth(prompt + text), y: 1});

	return (
		<Box flexDirection="column">
			<Text>Type Korean (Ctrl+C to exit):</Text>
			<Text>
				{prompt}
				{text}
			</Text>
		</Box>
	);
}

render(<App />);


================================================
FILE: examples/cursor-ime/index.ts
================================================
import './cursor-ime.js';


================================================
FILE: examples/incremental-rendering/incremental-rendering.tsx
================================================
import React, {useState, useEffect} from 'react';
import {
	render,
	Text,
	Box,
	useInput,
	useWindowSize,
	useApp,
} from '../../src/index.js';

const rows = [
	'Server Authentication Module - Handles JWT token validation, OAuth2 flows, and session management across distributed systems',
	'Database Connection Pool - Maintains persistent connections to PostgreSQL cluster with automatic failover and load balancing',
	'API Gateway Service - Routes incoming HTTP requests to microservices with rate limiting and request transformation',
	'User Profile Manager - Caches user data in Redis with write-through policy and invalidation strategies',
	'Payment Processing Engine - Integrates with Stripe, PayPal, and Square APIs for transaction processing',
	'Email Notification Queue - Processes outbound emails through SendGrid with retry logic and delivery tracking',
	'File Storage Handler - Manages S3 bucket operations with multipart uploads and CDN integration',
	'Search Indexer Service - Maintains Elasticsearch indices with real-time document updates and reindexing',
	'Metrics Aggregation Pipeline - Collects and processes telemetry data for Prometheus and Grafana dashboards',
	'WebSocket Connection Manager - Handles real-time bidirectional communication for chat and notifications',
	'Cache Invalidation Service - Coordinates distributed cache updates across Redis cluster nodes',
	'Background Job Processor - Executes async tasks via RabbitMQ with dead letter queue handling',
	'Session Store Manager - Persists user sessions in DynamoDB with TTL and cross-region replication',
	'Rate Limiter Module - Enforces API quotas using token bucket algorithm with Redis backend',
	'Content Delivery Network - Serves static assets through Cloudflare with edge caching and GZIP compression',
	'Logging Aggregator - Streams application logs to ELK stack with structured JSON formatting',
	'Health Check Monitor - Performs periodic service health checks with circuit breaker pattern implementation',
	'Configuration Manager - Loads environment-specific settings from Consul with hot reload capability',
	'Security Scanner Service - Runs automated vulnerability scans and dependency checks on deployed applications',
	'Backup Orchestrator - Schedules and executes automated database backups with encryption and versioning',
	'Load Balancer Controller - Manages NGINX upstream servers with health-based traffic distribution',
	'Container Orchestration - Coordinates Docker container lifecycle via Kubernetes with auto-scaling policies',
	'Message Bus Coordinator - Routes events through Apache Kafka topics with guaranteed delivery semantics',
	'Analytics Data Warehouse - Aggregates business metrics in Snowflake with incremental ETL processes',
	'API Documentation Service - Generates and serves OpenAPI specs with interactive Swagger UI',
	'Feature Flag Manager - Controls feature rollouts using LaunchDarkly with user targeting and percentage rollouts',
	'Audit Trail Logger - Records all user actions and system events for compliance and security analysis',
	'Image Processing Pipeline - Resizes and optimizes uploaded images using Sharp with multiple format outputs',
	'Geolocation Service - Resolves IP addresses to geographic coordinates using MaxMind GeoIP2 database',
	'Recommendation Engine - Generates personalized content suggestions using collaborative filtering algorithms',
];

const generateLogLine = (index: number, value: number) => {
	const timestamp = new Date().toLocaleTimeString();
	const actions = [
		'PROCESSING',
		'COMPLETED',
		'UPDATING',
		'SYNCING',
		'VALIDATING',
		'EXECUTING',
	];
	const action = actions[Math.floor(Math.random() * actions.length)];
	return `[${timestamp}] Worker-${index} ${action}: Batch=${value} Throughput=${(Math.random() * 1000).toFixed(0)}req/s Memory=${(Math.random() * 512).toFixed(1)}MB CPU=${(Math.random() * 100).toFixed(1)}%`;
};

function IncrementalRendering() {
	const {exit} = useApp();
	const {rows: terminalHeight} = useWindowSize();

	// Calculate available space for dynamic content
	// Header box: ~9 lines (border + content)
	// Logs box: variable (border + title + log lines)
	// Services box: variable (border + title + services)
	// Footer box: ~3 lines
	// Margins: ~3 lines
	// Total fixed: ~15 lines, so available = terminalHeight - 15
	const availableLines = Math.max(terminalHeight - 15, 10);

	// Split available space: ~30% for logs, ~70% for services
	const logLineCount = Math.max(Math.floor(availableLines * 0.3), 3);
	const serviceCount = Math.min(
		Math.max(Math.floor(availableLines * 0.7), 5),
		rows.length,
	);

	const [selectedIndex, setSelectedIndex] = useState(0);
	const [timestamp, setTimestamp] = useState(new Date().toLocaleTimeString());
	const [counter, setCounter] = useState(0);
	const [fps, setFps] = useState(0);
	const [progress1, setProgress1] = useState(0);
	const [progress2, setProgress2] = useState(0);
	const [progress3, setProgress3] = useState(0);
	const [randomValue, setRandomValue] = useState(0);
	const [logLines, setLogLines] = useState(
		Array.from({length: logLineCount}, (_, i) => generateLogLine(i, 0)),
	);

	// Update timestamp and counter every second to show live updates
	useEffect(() => {
		const timer = setInterval(() => {
			setTimestamp(new Date().toLocaleTimeString());
			setCounter(previous => previous + 1);
		}, 1000);

		return () => {
			clearInterval(timer);
		};
	}, []);

	// Rapid updates to degrade performance - updates every 16ms (~60fps)
	useEffect(() => {
		let frameCount = 0;
		let lastTime = Date.now();

		const timer = setInterval(() => {
			setProgress1(previous => (previous + 1) % 101);
			setProgress2(previous => (previous + 2) % 101);
			setProgress3(previous => (previous + 3) % 101);
			setRandomValue(Math.floor(Math.random() * 1000));

			// Update only 1-2 log lines each frame (simulating real log updates)
			setLogLines(previous => {
				const newLines = [...previous];
				const updateIndex = Math.floor(Math.random() * newLines.length);
				newLines[updateIndex] = generateLogLine(
					updateIndex,
					Math.floor(Math.random() * 1000),
				);
				return newLines;
			});

			// Calculate FPS
			frameCount++;
			const now = Date.now();
			if (now - lastTime >= 1000) {
				setFps(frameCount);
				frameCount = 0;
				lastTime = now;
			}
		}, 16); // ~60 updates per second

		return () => {
			clearInterval(timer);
		};
	}, []);

	useInput((input, key) => {
		if (key.upArrow) {
			setSelectedIndex(previousIndex =>
				previousIndex === 0 ? serviceCount - 1 : previousIndex - 1,
			);
		}

		if (key.downArrow) {
			setSelectedIndex(previousIndex =>
				previousIndex === serviceCount - 1 ? 0 : previousIndex + 1,
			);
		}

		if (input === 'q') {
			exit();
		}
	});

	const progressBar = (value: number) => {
		const filled = Math.floor(value / 5);
		const empty = 20 - filled;
		return '█'.repeat(filled) + '░'.repeat(empty);
	};

	return (
		<Box flexDirection="column" height="100%">
			<Box borderStyle="round" borderColor="cyan" paddingX={2} paddingY={1}>
				<Box flexDirection="column">
					<Text bold color="cyan">
						Incremental Rendering Demo - incrementalRendering={String(true)}
					</Text>
					<Text dimColor>
						Use ↑/↓ arrows to navigate • Press q to quit • FPS: {fps}
					</Text>
					<Text>
						Time: <Text color="green">{timestamp}</Text> • Updates:{' '}
						<Text color="yellow">{counter}</Text> • Random:{' '}
						<Text color="cyan">{randomValue}</Text>
					</Text>
					<Text>
						Progress 1: <Text color="green">{progressBar(progress1)}</Text>{' '}
						{progress1}%
					</Text>
					<Text>
						Progress 2: <Text color="yellow">{progressBar(progress2)}</Text>{' '}
						{progress2}%
					</Text>
					<Text>
						Progress 3: <Text color="red">{progressBar(progress3)}</Text>{' '}
						{progress3}%
					</Text>
				</Box>
			</Box>

			<Box
				borderStyle="single"
				borderColor="yellow"
				paddingX={2}
				paddingY={1}
				marginTop={1}
			>
				<Box flexDirection="column">
					<Text bold color="yellow">
						Live Logs (only 1-2 lines update per frame):
					</Text>
					{logLines.map(line => (
						<Text key={line} color="green">
							{line}
						</Text>
					))}
				</Box>
			</Box>

			<Box
				borderStyle="single"
				borderColor="gray"
				paddingX={2}
				paddingY={1}
				marginTop={1}
				flexGrow={1}
				flexDirection="column"
			>
				<Text bold color="magenta">
					System Services Monitor ({serviceCount} of {rows.length} services):
				</Text>
				{rows.slice(0, serviceCount).map((row, index) => {
					const isSelected = index === selectedIndex;
					return (
						<Text key={row} color={isSelected ? 'blue' : 'white'}>
							{isSelected ? '> ' : '  '}
							{row}
						</Text>
					);
				})}
			</Box>

			<Box borderStyle="round" borderColor="magenta" paddingX={2} marginTop={1}>
				<Text>
					Selected:{' '}
					<Text bold color="magenta">
						{rows.slice(0, serviceCount)[selectedIndex]}
					</Text>
				</Text>
			</Box>
		</Box>
	);
}

render(<IncrementalRendering />, {incrementalRendering: true});


================================================
FILE: examples/incremental-rendering/index.ts
================================================
import './incremental-rendering.js';


================================================
FILE: examples/jest/index.ts
================================================
import './jest.js';


================================================
FILE: examples/jest/jest.tsx
================================================
import React from 'react';
import PQueue from 'p-queue';
import delay from 'delay';
import ms from 'ms';
import {Static, Box, render} from '../../src/index.js';
import Summary from './summary.jsx';
import Test from './test.js';

const paths = [
	'tests/login.js',
	'tests/signup.js',
	'tests/forgot-password.js',
	'tests/reset-password.js',
	'tests/view-profile.js',
	'tests/edit-profile.js',
	'tests/delete-profile.js',
	'tests/posts.js',
	'tests/post.js',
	'tests/comments.js',
];

type State = {
	startTime: number;
	completedTests: Array<{
		path: string;
		status: string;
	}>;
	runningTests: Array<{
		path: string;
		status: string;
	}>;
};

class Jest extends React.Component<Record<string, unknown>, State> {
	constructor(properties: Record<string, unknown>) {
		super(properties);

		this.state = {
			startTime: Date.now(),
			completedTests: [],
			runningTests: [],
		};
	}

	render() {
		const {startTime, completedTests, runningTests} = this.state;

		return (
			<Box flexDirection="column">
				<Static items={completedTests}>
					{test => (
						<Test key={test.path} status={test.status} path={test.path} />
					)}
				</Static>

				{runningTests.length > 0 && (
					<Box flexDirection="column" marginTop={1}>
						{runningTests.map(test => (
							<Test key={test.path} status={test.status} path={test.path} />
						))}
					</Box>
				)}

				<Summary
					isFinished={runningTests.length === 0}
					passed={completedTests.filter(test => test.status === 'pass').length}
					failed={completedTests.filter(test => test.status === 'fail').length}
					time={ms(Date.now() - startTime)}
				/>
			</Box>
		);
	}

	componentDidMount() {
		const queue = new PQueue({concurrency: 4});

		for (const path of paths) {
			void queue.add(this.runTest.bind(this, path));
		}
	}

	async runTest(path: string) {
		this.setState(previousState => ({
			runningTests: [
				...previousState.runningTests,
				{
					status: 'runs',
					path,
				},
			],
		}));

		await delay(1000 * Math.random());

		this.setState(previousState => ({
			runningTests: previousState.runningTests.filter(
				test => test.path !== path,
			),
			completedTests: [
				...previousState.completedTests,
				{
					status: Math.random() < 0.5 ? 'pass' : 'fail',
					path,
				},
			],
		}));
	}
}

render(<Jest />);


================================================
FILE: examples/jest/summary.tsx
================================================
import React from 'react';
import {Box, Text} from '../../src/index.js';

type Properties = {
	readonly isFinished: boolean;
	readonly passed: number;
	readonly failed: number;
	readonly time: string;
};

function Summary({isFinished, passed, failed, time}: Properties) {
	return (
		<Box flexDirection="column" marginTop={1}>
			<Box>
				<Box width={14}>
					<Text bold>Test Suites:</Text>
				</Box>
				{failed > 0 && (
					<Text bold color="red">
						{failed} failed,{' '}
					</Text>
				)}
				{passed > 0 && (
					<Text bold color="green">
						{passed} passed,{' '}
					</Text>
				)}
				<Text>{passed + failed} total</Text>
			</Box>

			<Box>
				<Box width={14}>
					<Text bold>Time:</Text>
				</Box>

				<Text>{time}</Text>
			</Box>

			{isFinished ? (
				<Box>
					<Text dimColor>Ran all test suites.</Text>
				</Box>
			) : null}
		</Box>
	);
}

export default Summary;


================================================
FILE: examples/jest/test.tsx
================================================
import React from 'react';
import {Box, Text} from '../../src/index.js';

const getBackgroundForStatus = (status: string): string | undefined => {
	switch (status) {
		case 'runs': {
			return 'yellow';
		}

		case 'pass': {
			return 'green';
		}

		case 'fail': {
			return 'red';
		}

		default: {
			return undefined;
		}
	}
};

type Properties = {
	readonly status: string;
	readonly path: string;
};

function Test({status, path}: Properties) {
	return (
		<Box>
			<Text color="black" backgroundColor={getBackgroundForStatus(status)}>
				{` ${status.toUpperCase()} `}
			</Text>

			<Box marginLeft={1}>
				<Text dimColor>{path.split('/')[0]}/</Text>

				<Text bold color="white">
					{path.split('/')[1]}
				</Text>
			</Box>
		</Box>
	);
}

export default Test;


================================================
FILE: examples/justify-content/index.ts
================================================
import './justify-content.js';


================================================
FILE: examples/justify-content/justify-content.tsx
================================================
import React from 'react';
import {render, Box, Text} from '../../src/index.js';

function JustifyContent() {
	return (
		<Box flexDirection="column">
			<Box>
				<Text>[</Text>
				<Box justifyContent="flex-start" width={20} height={1}>
					<Text>X</Text>
					<Text>Y</Text>
				</Box>
				<Text>] flex-start</Text>
			</Box>
			<Box>
				<Text>[</Text>
				<Box justifyContent="flex-end" width={20} height={1}>
					<Text>X</Text>
					<Text>Y</Text>
				</Box>
				<Text>] flex-end</Text>
			</Box>
			<Box>
				<Text>[</Text>
				<Box justifyContent="center" width={20} height={1}>
					<Text>X</Text>
					<Text>Y</Text>
				</Box>
				<Text>] center</Text>
			</Box>
			<Box>
				<Text>[</Text>
				<Box justifyContent="space-around" width={20} height={1}>
					<Text>X</Text>
					<Text>Y</Text>
				</Box>
				<Text>] space-around</Text>
			</Box>
			<Box>
				<Text>[</Text>
				<Box justifyContent="space-between" width={20} height={1}>
					<Text>X</Text>
					<Text>Y</Text>
				</Box>
				<Text>] space-between</Text>
			</Box>
			<Box>
				<Text>[</Text>
				<Box justifyContent="space-evenly" width={20} height={1}>
					<Text>X</Text>
					<Text>Y</Text>
				</Box>
				<Text>] space-evenly</Text>
			</Box>
		</Box>
	);
}

render(<JustifyContent />);


================================================
FILE: examples/render-throttle/index.tsx
================================================
import React, {useState, useEffect} from 'react';
import {render, Box, Text} from '../../src/index.js';

function App() {
	const [count, setCount] = useState(0);

	useEffect(() => {
		const interval = setInterval(() => {
			setCount(c => c + 1);
		}, 10); // Update every 10ms

		return () => {
			clearInterval(interval);
		};
	}, []);

	return (
		<Box flexDirection="column" padding={1}>
			<Text>Counter: {count}</Text>
			<Text>This updates every 10ms but renders are throttled</Text>
			<Text>Press Ctrl+C to exit</Text>
		</Box>
	);
}

// Example with custom maxFps
render(<App />, {
	maxFps: 10, // Only render at 10fps (every ~100ms) instead of default 30fps
});


================================================
FILE: examples/router/index.ts
================================================
import './router.js';


================================================
FILE: examples/router/router.tsx
================================================
import React from 'react';
import {MemoryRouter, Routes, Route, useNavigate} from 'react-router';
import {render, useInput, useApp, Box, Text} from '../../src/index.js';

function Home() {
	const {exit} = useApp();
	const navigate = useNavigate();

	useInput((input, key) => {
		if (input === 'q') {
			exit();
		}

		if (key.return) {
			void navigate('/about');
		}
	});

	return (
		<Box flexDirection="column">
			<Text bold color="green">
				Home
			</Text>
			<Text>Press Enter to go to About, or "q" to quit.</Text>
		</Box>
	);
}

function About() {
	const {exit} = useApp();
	const navigate = useNavigate();

	useInput((input, key) => {
		if (input === 'q') {
			exit();
		}

		if (key.return) {
			void navigate('/');
		}
	});

	return (
		<Box flexDirection="column">
			<Text bold color="blue">
				About
			</Text>
			<Text>Press Enter to go back Home, or "q" to quit.</Text>
		</Box>
	);
}

function App() {
	return (
		<MemoryRouter>
			<Routes>
				<Route path="/" element={<Home />} />
				<Route path="/about" element={<About />} />
			</Routes>
		</MemoryRouter>
	);
}

render(<App />);


================================================
FILE: examples/select-input/index.ts
================================================
import './select-input.js';


================================================
FILE: examples/select-input/select-input.tsx
================================================
import React, {useState} from 'react';
import {
	render,
	Text,
	Box,
	useInput,
	useIsScreenReaderEnabled,
} from '../../src/index.js';

const items = ['Red', 'Green', 'Blue', 'Yellow', 'Magenta', 'Cyan'];

function SelectInput() {
	const [selectedIndex, setSelectedIndex] = useState(0);
	const isScreenReaderEnabled = useIsScreenReaderEnabled();

	useInput((input, key) => {
		if (key.upArrow) {
			setSelectedIndex(previousIndex =>
				previousIndex === 0 ? items.length - 1 : previousIndex - 1,
			);
		}

		if (key.downArrow) {
			setSelectedIndex(previousIndex =>
				previousIndex === items.length - 1 ? 0 : previousIndex + 1,
			);
		}

		if (isScreenReaderEnabled) {
			const number = Number.parseInt(input, 10);
			if (!Number.isNaN(number) && number > 0 && number <= items.length) {
				setSelectedIndex(number - 1);
			}
		}
	});

	return (
		<Box flexDirection="column" aria-role="list">
			<Text>Select a color:</Text>
			{items.map((item, index) => {
				const isSelected = index === selectedIndex;
				const label = isSelected ? `> ${item}` : `  ${item}`;
				const screenReaderLabel = `${index + 1}. ${item}`;

				return (
					<Box
						key={item}
						aria-role="listitem"
						aria-state={{selected: isSelected}}
						aria-label={isScreenReaderEnabled ? screenReaderLabel : undefined}
					>
						<Text color={isSelected ? 'blue' : undefined}>{label}</Text>
					</Box>
				);
			})}
		</Box>
	);
}

render(<SelectInput />);


================================================
FILE: examples/static/index.ts
================================================
import './static.js';


================================================
FILE: examples/static/static.tsx
================================================
import React from 'react';
import {Box, Text, render, Static} from '../../src/index.js';

function Example() {
	const [tests, setTests] = React.useState<
		Array<{
			id: number;
			title: string;
		}>
	>([]);

	React.useEffect(() => {
		let completedTests = 0;
		let timer: NodeJS.Timeout | undefined;

		const run = () => {
			if (completedTests++ < 10) {
				setTests(previousTests => [
					...previousTests,
					{
						id: previousTests.length,
						title: `Test #${previousTests.length + 1}`,
					},
				]);

				timer = setTimeout(run, 100);
			}
		};

		run();

		return () => {
			clearTimeout(timer);
		};
	}, []);

	return (
		<>
			<Static items={tests}>
				{test => (
					<Box key={test.id}>
						<Text color="green">✔ {test.title}</Text>
					</Box>
				)}
			</Static>

			<Box marginTop={1}>
				<Text dimColor>Completed tests: {tests.length}</Text>
			</Box>
		</>
	);
}

render(<Example />);


================================================
FILE: examples/subprocess-output/index.ts
================================================
import './subprocess-output.js';


================================================
FILE: examples/subprocess-output/subprocess-output.tsx
================================================
import childProcess from 'node:child_process';
import type {Buffer} from 'node:buffer';
import React from 'react';
import stripAnsi from 'strip-ansi';
import {render, Text, Box} from '../../src/index.js';

function SubprocessOutput() {
	const [output, setOutput] = React.useState('');

	React.useEffect(() => {
		const subProcess = childProcess.spawn('npm', [
			'run',
			'example',
			'examples/jest',
		]);

		// eslint-disable-next-line @typescript-eslint/no-restricted-types
		subProcess.stdout.on('data', (newOutput: Buffer) => {
			const lines = stripAnsi(newOutput.toString('utf8')).split('\n');
			setOutput(lines.slice(-5).join('\n'));
		});
	}, [setOutput]);

	return (
		<Box flexDirection="column" padding={1}>
			<Text>Сommand output:</Text>
			<Box marginTop={1}>
				<Text>{output}</Text>
			</Box>
		</Box>
	);
}

render(<SubprocessOutput />);


================================================
FILE: examples/suspense/index.ts
================================================
import './suspense.js';


================================================
FILE: examples/suspense/suspense.tsx
================================================
import React from 'react';
import {render, Text} from '../../src/index.js';

let promise: Promise<void> | undefined;
let state: string | undefined;
let value: string | undefined;

const read = () => {
	if (!promise) {
		promise = new Promise(resolve => {
			setTimeout(resolve, 500);
		});

		state = 'pending';

		(async () => {
			await promise;
			state = 'done';
			value = 'Hello World';
		})();
	}

	if (state === 'pending') {
		// eslint-disable-next-line @typescript-eslint/only-throw-error
		throw promise;
	}

	if (state === 'done') {
		return value;
	}
};

function Example() {
	const message = read();
	return <Text>{message}</Text>;
}

function Fallback() {
	return <Text>Loading...</Text>;
}

render(
	<React.Suspense fallback={<Fallback />}>
		<Example />
	</React.Suspense>,
);


================================================
FILE: examples/table/index.ts
================================================
import './table.js';


================================================
FILE: examples/table/table.tsx
================================================
import React from 'react';
import {faker} from '@faker-js/faker';
import {Box, Text, render} from '../../src/index.js';

const users = Array.from({length: 10})
	.fill(true)
	.map((_, index) => ({
		id: index,
		name: faker.internet.username(),
		email: faker.internet.email(),
	}));

function Table() {
	return (
		<Box flexDirection="column" width={80}>
			<Box>
				<Box width="10%">
					<Text>ID</Text>
				</Box>

				<Box width="50%">
					<Text>Name</Text>
				</Box>

				<Box width="40%">
					<Text>Email</Text>
				</Box>
			</Box>

			{users.map(user => (
				<Box key={user.id}>
					<Box width="10%">
						<Text>{user.id}</Text>
					</Box>

					<Box width="50%">
						<Text>{user.name}</Text>
					</Box>

					<Box width="40%">
						<Text>{user.email}</Text>
					</Box>
				</Box>
			))}
		</Box>
	);
}

render(<Table />);


================================================
FILE: examples/terminal-resize/index.ts
================================================
import './terminal-resize.js';


================================================
FILE: examples/terminal-resize/terminal-resize.tsx
================================================
import React from 'react';
import {render, Box, Text, useWindowSize} from '../../src/index.js';

function TerminalResizeExample() {
	const {columns, rows} = useWindowSize();

	return (
		<Box flexDirection="column" padding={1}>
			<Text bold color="cyan">
				Terminal Size
			</Text>
			<Text>Columns: {columns}</Text>
			<Text>Rows: {rows}</Text>
			<Box marginTop={1}>
				<Text dimColor>
					Resize your terminal to see the values update. Press Ctrl+C to exit.
				</Text>
			</Box>
		</Box>
	);
}

render(<TerminalResizeExample />, {
	patchConsole: true,
	exitOnCtrlC: true,
});


================================================
FILE: examples/use-focus/index.ts
================================================
import './use-focus.js';


================================================
FILE: examples/use-focus/use-focus.tsx
================================================
import React from 'react';
import {Box, Text, render, useFocus} from '../../src/index.js';

function Focus() {
	return (
		<Box flexDirection="column" padding={1}>
			<Box marginBottom={1}>
				<Text>
					Press Tab to focus next element, Shift+Tab to focus previous element,
					Esc to reset focus.
				</Text>
			</Box>
			<Item label="First" />
			<Item label="Second" />
			<Item label="Third" />
		</Box>
	);
}

function Item({label}) {
	const {isFocused} = useFocus();
	return (
		<Text>
			{label} {isFocused ? <Text color="green">(focused)</Text> : null}
		</Text>
	);
}

render(<Focus />);


================================================
FILE: examples/use-focus-with-id/index.ts
================================================
import './use-focus-with-id.js';


================================================
FILE: examples/use-focus-with-id/use-focus-with-id.tsx
================================================
import React from 'react';
import {
	render,
	Box,
	Text,
	useFocus,
	useInput,
	useFocusManager,
} from '../../src/index.js';

function Focus() {
	const {focus} = useFocusManager();

	useInput(input => {
		if (input === '1') {
			focus('1');
		}

		if (input === '2') {
			focus('2');
		}

		if (input === '3') {
			focus('3');
		}
	});

	return (
		<Box flexDirection="column" padding={1}>
			<Box marginBottom={1}>
				<Text>
					Press Tab to focus next element, Shift+Tab to focus previous element,
					Esc to reset focus.
				</Text>
			</Box>
			<Item id="1" label="Press 1 to focus" />
			<Item id="2" label="Press 2 to focus" />
			<Item id="3" label="Press 3 to focus" />
		</Box>
	);
}

type ItemProperties = {
	readonly id: number;
	readonly label: string;
};

function Item({label, id}: ItemProperties) {
	const {isFocused} = useFocus({id});

	return (
		<Text>
			{label} {isFocused ? <Text color="green">(focused)</Text> : null}
		</Text>
	);
}

render(<Focus />);


================================================
FILE: examples/use-input/index.ts
================================================
import './use-input.js';


================================================
FILE: examples/use-input/use-input.tsx
================================================
import React from 'react';
import {render, useInput, useApp, Box, Text} from '../../src/index.js';

function Robot() {
	const {exit} = useApp();
	const [x, setX] = React.useState(1);
	const [y, setY] = React.useState(1);

	useInput((input, key) => {
		if (input === 'q') {
			exit();
		}

		if (key.leftArrow) {
			setX(Math.max(1, x - 1));
		}

		if (key.rightArrow) {
			setX(Math.min(20, x + 1));
		}

		if (key.upArrow) {
			setY(Math.max(1, y - 1));
		}

		if (key.downArrow) {
			setY(Math.min(10, y + 1));
		}
	});

	return (
		<Box flexDirection="column">
			<Text>Use arrow keys to move the face. Press “q” to exit.</Text>
			<Box height={12} paddingLeft={x} paddingTop={y}>
				<Text>^_^</Text>
			</Box>
		</Box>
	);
}

render(<Robot />);


================================================
FILE: examples/use-stderr/index.ts
================================================
import './use-stderr.js';


================================================
FILE: examples/use-stderr/use-stderr.tsx
================================================
import React from 'react';
import {render, Text, useStderr} from '../../src/index.js';

function Example() {
	const {write} = useStderr();

	React.useEffect(() => {
		const timer = setInterval(() => {
			write('Hello from Ink to stderr\n');
		}, 1000);

		return () => {
			clearInterval(timer);
		};
	}, []);

	return <Text>Hello World</Text>;
}

render(<Example />);


================================================
FILE: examples/use-stdout/index.ts
================================================
import './use-stdout.js';


================================================
FILE: examples/use-stdout/use-stdout.tsx
================================================
import React from 'react';
import {render, Box, Text, useStdout} from '../../src/index.js';

function Example() {
	const {stdout, write} = useStdout();

	React.useEffect(() => {
		const timer = setInterval(() => {
			write('Hello from Ink to stdout\n');
		}, 1000);

		return () => {
			clearInterval(timer);
		};
	}, []);

	return (
		<Box flexDirection="column" paddingX={2} paddingY={1}>
			<Text bold underline>
				Terminal dimensions:
			</Text>

			<Box marginTop={1}>
				<Text>
					Width: <Text bold>{stdout.columns}</Text>
				</Text>
			</Box>
			<Box>
				<Text>
					Height: <Text bold>{stdout.rows}</Text>
				</Text>
			</Box>
		</Box>
	);
}

render(<Example />);


================================================
FILE: examples/use-transition/index.ts
================================================
import './use-transition.js';


================================================
FILE: examples/use-transition/use-transition.tsx
================================================
import React, {useState, useMemo, useTransition} from 'react';
import {render, Box, Text, useInput} from '../../src/index.js';

// Generate a large list of items for demonstration
function generateItems(filter: string): string[] {
	const allItems: string[] = [];
	for (let i = 0; i < 200; i++) {
		allItems.push(
			`Item ${i + 1}: ${['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'][i % 5]}`,
		);
	}

	if (!filter) {
		return allItems.slice(0, 10);
	}

	// Simulate expensive filtering
	const start = Date.now();
	while (Date.now() - start < 100) {
		// Artificial delay to simulate expensive computation
	}

	return allItems
		.filter(item => item.toLowerCase().includes(filter.toLowerCase()))
		.slice(0, 10);
}

function SearchApp() {
	const [query, setQuery] = useState('');
	const [isPending, startTransition] = useTransition();

	// This is the "deferred" state that can lag behind
	const [deferredQuery, setDeferredQuery] = useState('');

	// Filtered items based on deferred query (expensive computation)
	const filteredItems = useMemo(
		() => generateItems(deferredQuery),
		[deferredQuery],
	);

	// Handle keyboard input
	useInput((input, key) => {
		if (key.backspace || key.delete) {
			setQuery(previousQuery => previousQuery.slice(0, -1));
			startTransition(() => {
				setDeferredQuery(previousQuery => previousQuery.slice(0, -1));
			});
		} else if (input && !key.ctrl && !key.meta) {
			setQuery(previousQuery => previousQuery + input);
			// Wrap the expensive update in a transition
			startTransition(() => {
				setDeferredQuery(previousQuery => previousQuery + input);
			});
		}
	});

	return (
		<Box flexDirection="column">
			<Text bold underline>
				useTransition Demo
			</Text>
			<Text dimColor>
				(Type to search - input stays responsive while list updates)
			</Text>
			<Box marginTop={1} />

			<Box>
				<Text>Search: </Text>
				<Text color="cyan">{query || '(type something)'}</Text>
				{isPending ? <Text color="yellow"> (updating...)</Text> : null}
			</Box>

			<Box marginTop={1} flexDirection="column">
				<Text bold>
					Results{' '}
					{deferredQuery ? `for "${deferredQuery}"` : '(showing first 10)'}:
				</Text>
				{filteredItems.length === 0 ? (
					<Text dimColor> No items found</Text>
				) : (
					filteredItems.map(item => (
						<Text key={item} dimColor={isPending}>
							{item}
						</Text>
					))
				)}
			</Box>

			<Box marginTop={1}>
				<Text dimColor>Press Ctrl+C to exit</Text>
			</Box>
		</Box>
	);
}

// Render with concurrent mode enabled (required for useTransition)
render(<SearchApp />, {concurrent: true});


================================================
FILE: license
================================================
MIT License

Copyright (c) Vadym Demedes <vadimdemedes@hey.com> (https://github.com/vadimdemedes)
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: media/demo.js
================================================
import process from 'node:process';
import React from 'react';
import {render, Box, Text} from 'ink';

class Counter extends React.PureComponent {
	constructor() {
		super();

		this.state = {
			i: 0,
		};
	}

	render() {
		return React.createElement(
			Box,
			{flexDirection: 'column'},
			React.createElement(
				Box,
				{},
				React.createElement(Text, {color: 'blue'}, '~/Projects/ink '),
			),
			React.createElement(
				Box,
				{},
				React.createElement(Text, {color: 'magenta'}, '❯ '),
				React.createElement(Text, {color: 'green'}, 'node '),
				React.createElement(Text, {}, 'media/example'),
			),
			React.createElement(
				Text,
				{color: 'green'},
				`${this.state.i} tests passed`,
			),
		);
	}

	componentDidMount() {
		this.timer = setInterval(() => {
			if (this.state.i === 50) {
				process.exit(0); // eslint-disable-line unicorn/no-process-exit
			}

			this.setState(previousState => ({
				i: previousState.i + 1,
			}));
		}, 100);
	}

	componentWillUnmount() {
		clearInterval(this.timer);
	}
}

render(React.createElement(Counter));


================================================
FILE: package.json
================================================
{
	"name": "ink",
	"version": "6.8.0",
	"description": "React for CLI",
	"license": "MIT",
	"repository": "vadimdemedes/ink",
	"author": {
		"name": "Vadim Demedes",
		"email": "vadimdemedes@hey.com",
		"url": "https://github.com/vadimdemedes"
	},
	"type": "module",
	"exports": {
		"types": "./build/index.d.ts",
		"default": "./build/index.js"
	},
	"engines": {
		"node": ">=20"
	},
	"scripts": {
		"dev": "tsc --watch",
		"build": "tsc",
		"prepare": "npm run build",
		"test": "npm run typecheck && npm run lint && FORCE_COLOR=true ava",
		"lint": "xo",
		"typecheck": "tsc --noEmit",
		"example": "NODE_NO_WARNINGS=1 node --import=tsx",
		"benchmark": "NODE_NO_WARNINGS=1 node --import=tsx",
		"inspect": "react-devtools"
	},
	"files": [
		"build"
	],
	"keywords": [
		"react",
		"cli",
		"jsx",
		"stdout",
		"components",
		"command-line",
		"preact",
		"redux",
		"print",
		"render",
		"colors",
		"text"
	],
	"dependencies": {
		"@alcalzone/ansi-tokenize": "^0.3.0",
		"ansi-escapes": "^7.3.0",
		"ansi-styles": "^6.2.1",
		"auto-bind": "^5.0.1",
		"chalk": "^5.6.0",
		"cli-boxes": "^3.0.0",
		"cli-cursor": "^4.0.0",
		"cli-truncate": "^5.1.1",
		"code-excerpt": "^4.0.0",
		"es-toolkit": "^1.39.10",
		"indent-string": "^5.0.0",
		"is-in-ci": "^2.0.0",
		"patch-console": "^2.0.0",
		"react-reconciler": "^0.33.0",
		"scheduler": "^0.27.0",
		"signal-exit": "^3.0.7",
		"slice-ansi": "^8.0.0",
		"stack-utils": "^2.0.6",
		"string-width": "^8.1.1",
		"terminal-size": "^4.0.1",
		"type-fest": "^5.4.1",
		"widest-line": "^6.0.0",
		"wrap-ansi": "^10.0.0",
		"ws": "^8.18.0",
		"yoga-layout": "~3.2.1"
	},
	"devDependencies": {
		"@faker-js/faker": "^10.3.0",
		"@sindresorhus/tsconfig": "^8.1.0",
		"@sinonjs/fake-timers": "^15.1.0",
		"@types/ms": "^2.1.0",
		"@types/node": "^25.0.10",
		"@types/react": "^19.2.13",
		"@types/react-reconciler": "^0.33.0",
		"@types/scheduler": "^0.26.0",
		"@types/signal-exit": "^3.0.0",
		"@types/sinon": "^21.0.0",
		"@types/stack-utils": "^2.0.2",
		"@types/ws": "^8.18.1",
		"@vdemedes/prettier-config": "^2.0.1",
		"ava": "^7.0.0",
		"boxen": "^8.0.1",
		"delay": "^7.0.0",
		"ms": "^2.1.3",
		"node-pty": "^1.2.0-beta.10",
		"p-queue": "^9.0.0",
		"prettier": "^3.8.1",
		"react": "^19.2.4",
		"react-devtools-core": "^7.0.1",
		"react-devtools": "^7.0.1",
		"react-router": "^7.13.0",
		"sinon": "^21.0.0",
		"strip-ansi": "^7.1.0",
		"tsx": "^4.21.0",
		"typescript": "^5.8.3",
		"xo": "^1.2.3"
	},
	"peerDependencies": {
		"@types/react": ">=19.0.0",
		"react": ">=19.0.0",
		"react-devtools-core": ">=6.1.2"
	},
	"peerDependenciesMeta": {
		"@types/react": {
			"optional": true
		},
		"react-devtools-core": {
			"optional": true
		}
	},
	"ava": {
		"workerThreads": false,
		"serial": true,
		"files": [
			"test/**/*",
			"!test/helpers/**/*",
			"!test/fixtures/**/*"
		],
		"extensions": {
			"ts": "module",
			"tsx": "module"
		},
		"nodeArguments": [
			"--import=tsx"
		]
	},
	"prettier": "@vdemedes/prettier-config"
}


================================================
FILE: readme.md
================================================
[![](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md)

---

<div align="center">
	<br>
	<br>
	<img width="240" alt="Ink" src="media/logo.png">
	<br>
	<br>
	<br>
</div>

> React for CLIs. Build and test your CLI output using components.

[![Build Status](https://github.com/vadimdemedes/ink/workflows/test/badge.svg)](https://github.com/vadimdemedes/ink/actions)
[![npm](https://img.shields.io/npm/dm/ink?logo=npm)](https://npmjs.com/package/ink)

Ink provides the same component-based UI building experience that React offers in the browser, but for command-line apps.
It uses [Yoga](https://github.com/facebook/yoga) to build Flexbox layouts in the terminal, so most CSS-like properties are available in Ink as well.
If you are already familiar with React, you already know Ink.

Since Ink is a React renderer, all features of React are supported.
Head over to the [React](https://reactjs.org) website for documentation on how to use it.
Only Ink's methods are documented in this readme.

---

<div align="center">
	<p>
		<p>
			<sup>
				<a href="https://opencollective.com/vadimdemedes">My open source work is supported by the community ❤️</a>
			</sup>
		</p>
	</p>
</div>

## Install

```sh
npm install ink react
```

> [!NOTE]
> This readme documents the upcoming version of Ink. For the latest stable release, see [Ink on npm](https://www.npmjs.com/package/ink).

## Usage

```jsx
import React, {useState, useEffect} from 'react';
import {render, Text} from 'ink';

const Counter = () => {
	const [counter, setCounter] = useState(0);

	useEffect(() => {
		const timer = setInterval(() => {
			setCounter(previousCounter => previousCounter + 1);
		}, 100);

		return () => {
			clearInterval(timer);
		};
	}, []);

	return <Text color="green">{counter} tests passed</Text>;
};

render(<Counter />);
```

<img src="media/demo.svg" width="600">

## Who's Using Ink?

- [Claude Code](https://github.com/anthropics/claude-code) - An agentic coding tool made by Anthropic.
- [Gemini CLI](https://github.com/google-gemini/gemini-cli) - An agentic coding tool made by Google.
- [GitHub Copilot CLI](https://github.com/features/copilot/cli) - Just say what you want the shell to do.
- [Canva CLI](https://www.canva.dev/docs/apps/canva-cli/) - CLI for creating and managing Canva Apps.
- [Cloudflare's Wrangler](https://github.com/cloudflare/wrangler2) - The CLI for Cloudflare Workers.
- [Linear](https://linear.app) - Linear built an internal CLI for managing deployments, configs, and other housekeeping tasks.
- [Gatsby](https://www.gatsbyjs.org) - Gatsby is a modern web framework for blazing-fast websites.
- [tap](https://node-tap.org) - A Test-Anything-Protocol library for JavaScript.
- [Terraform CDK](https://github.com/hashicorp/terraform-cdk) - Cloud Development Kit (CDK) for HashiCorp Terraform.
- [Specify CLI](https://specifyapp.com) - Automate the distribution of your design tokens.
- [Twilio's SIGNAL](https://github.com/twilio-labs/plugin-signal2020) - CLI for Twilio's SIGNAL conference. [Blog post](https://www.twilio.com/blog/building-conference-cli-in-react).
- [Typewriter](https://github.com/segmentio/typewriter) - Generates strongly-typed [Segment](https://segment.com) analytics clients from arbitrary JSON Schema.
- [Prisma](https://www.prisma.io) - The unified data layer for modern applications.
- [Blitz](https://blitzjs.com) - The Fullstack React Framework.
- [New York Times](https://github.com/nytimes/kyt) - NYT uses Ink's `kyt` - a toolkit that encapsulates and manages the configuration for web apps.
- [tink](https://github.com/npm/tink) - A next-generation runtime and package manager.
- [Inkle](https://github.com/jrr/inkle) - A Wordle game.
- [loki](https://github.com/oblador/loki) - Visual regression testing tool for Storybook.
- [Bit](https://github.com/teambit/bit) - Build, distribute, and collaborate on components.
- [Remirror](https://github.com/remirror/remirror) - Your friendly, world-class editor toolkit.
- [Prime](https://github.com/birkir/prime) - Open-source GraphQL CMS.
- [emoj](https://github.com/sindresorhus/emoj) - Find relevant emojis.
- [emma](https://github.com/maticzav/emma-cli) - Find and install npm packages easily.
- [npm-check-extras](https://github.com/akgondber/npm-check-extras) - Check for outdated and unused dependencies, and run update/delete actions on selected ones.
- [swiff](https://github.com/simple-integrated-marketing/swiff) - Multi-environment command-line tools for time-saving web developers.
- [share](https://github.com/marionebl/share-cli) - Share files quickly.
- [Kubelive](https://github.com/ameerthehacker/kubelive) - A CLI for Kubernetes that provides live data about the cluster and its resources.
- [changelog-view](https://github.com/jdeniau/changelog-view) - View changelogs.
- [cfpush](https://github.com/mamachanko/cfpush) - Interactive Cloud Foundry tutorial.
- [startd](https://github.com/mgrip/startd) - Turn your React component into a web app.
- [wiki-cli](https://github.com/hexrcs/wiki-cli) - Search Wikipedia and read article summaries.
- [garson](https://github.com/goliney/garson) - Build interactive, config-based command-line interfaces.
- [git-contrib-calendar](https://github.com/giannisp/git-contrib-calendar) - Display a contributions calendar for any Git repository.
- [gitgud](https://github.com/GitGud-org/GitGud) - Interactive command-line GUI for Git.
- [Autarky](https://github.com/pranshuchittora/autarky) - Find and delete old `node_modules` directories to free up disk space.
- [fast-cli](https://github.com/sindresorhus/fast-cli) - Test your download and upload speeds.
- [tasuku](https://github.com/privatenumber/tasuku) - Minimal task runner.
- [mnswpr](https://github.com/mordv/mnswpr) - A Minesweeper game.
- [lrn](https://github.com/krychu/lrn) - Learning by repetition.
- [turdle](https://github.com/mynameisankit/turdle) - A Wordle game.
- [Shopify CLI](https://github.com/Shopify/cli) - Build apps, themes, and storefronts for the Shopify platform.
- [ToDesktop CLI](https://www.todesktop.com/electron) - All-in-one platform for building Electron apps.
- [Walle](https://github.com/Pobepto/walle) - A full-featured crypto wallet for EVM networks.
- [Sudoku](https://github.com/mrozio13pl/sudoku-in-terminal) - A Sudoku game.
- [Sea Trader](https://github.com/zyishai/sea-trader) - A Taipan!-inspired trading simulator game.
- [srtd](https://github.com/t1mmen/srtd) - Live-reloading SQL templates for Supabase projects.
- [tweakcc](https://github.com/Piebald-AI/tweakcc) - Customize your Claude Code styling.
- [argonaut](https://github.com/darksworm/argonaut) - Manage Argo CD resources.
- [Qodo Command](https://github.com/qodo-ai/command) - Build, run, and manage AI agents.
- [Nanocoder](https://github.com/nano-collective/nanocoder) - A community-built, local-first AI coding agent with multi-provider support.
- [dev3000](https://github.com/vercel-labs/dev3000) - An AI agent MCP orchestrator and developer browser.
- [Neovate Code](https://github.com/neovateai/neovate-code) - An agentic coding tool made by AntGroup.
- [instagram-cli](https://github.com/supreme-gg-gg/instagram-cli) - Instagram client.
- [ElevenLabs CLI](https://github.com/elevenlabs/cli) - ElevenLabs agents client.
- [SSH AI Chat](https://github.com/miantiao-me/ssh-ai-chat) - Chat with AI over SSH.

*(PRs welcome. Append new entries at the end. Repos must have 100+ stars and showcase Ink beyond a basic list picker.)*

## Contents

- [Getting Started](#getting-started)
- [App Lifecycle](#app-lifecycle)
- [Components](#components)
  - [`<Text>`](#text)
  - [`<Box>`](#box)
  - [`<Newline>`](#newline)
  - [`<Spacer>`](#spacer)
  - [`<Static>`](#static)
  - [`<Transform>`](#transform)
- [Hooks](#hooks)
  - [`useInput`](#useinputinputhandler-options)
  - [`usePaste`](#usepastehandler-options)
  - [`useApp`](#useapp)
  - [`useStdin`](#usestdin)
  - [`useStdout`](#usestdout)
  - [`useBoxMetrics`](#useboxmetricsref)
  - [`useStderr`](#usestderr)
  - [`useWindowSize`](#usewindowsize)
  - [`useFocus`](#usefocusoptions)
  - [`useFocusManager`](#usefocusmanager)
  - [`useCursor`](#usecursor)
- [API](#api)
- [Testing](#testing)
- [Using React Devtools](#using-react-devtools)
- [Screen Reader Support](#screen-reader-support)
- [Useful Components](#useful-components)
- [Useful Hooks](#useful-hooks)
- [Recipes](#recipes)
- [Examples](#examples)
- [Continuous Integration](#continuous-integration)

## Getting Started

Use [create-ink-app](https://github.com/vadimdemedes/create-ink-app) to quickly scaffold a new Ink-based CLI.

```sh
npx create-ink-app my-ink-cli
```

Alternatively, create a TypeScript project:

```sh
npx create-ink-app --typescript my-ink-cli
```

<details><summary>Manual JavaScript setup</summary>
<p>
Ink requires the same Babel setup as you would do for regular React-based apps in the browser.

Set up Babel with a React preset to ensure all examples in this readme work as expected.
After [installing Babel](https://babeljs.io/docs/en/usage), install `@babel/preset-react` and insert the following configuration in `babel.config.json`:

```sh
npm install --save-dev @babel/preset-react
```

```json
{
	"presets": ["@babel/preset-react"]
}
```

Next, create a file `source.js`, where you'll type code that uses Ink:

```jsx
import React from 'react';
import {render, Text} from 'ink';

const Demo = () => <Text>Hello World</Text>;

render(<Demo />);
```

Then, transpile this file with Babel:

```sh
npx babel source.js -o cli.js
```

Now you can run `cli.js` with Node.js:

```sh
node cli
```

If you don't like transpiling files during development, you can use [import-jsx](https://github.com/vadimdemedes/import-jsx) or [@esbuild-kit/esm-loader](https://github.com/esbuild-kit/esm-loader) to `import` a JSX file and transpile it on the fly.

</p>
</details>

Ink uses [Yoga](https://github.com/facebook/yoga), a Flexbox layout engine, to build great user interfaces for your CLIs using familiar CSS-like properties you've used when building apps for the browser.
It's important to remember that each element is a Flexbox container.
Think of it as if every `<div>` in the browser had `display: flex`.
See [`<Box>`](#box) built-in component below for documentation on how to use Flexbox layouts in Ink.
Note that all text must be wrapped in a [`<Text>`](#text) component.

## App Lifecycle

An Ink app is a Node.js process, so it stays alive only while there is active work in the event loop (timers, pending promises, [`useInput`](#useinputinputhandler-options) listening on `stdin`, etc.). If your component tree has no async work, the app will render once and exit immediately.

To exit the app, press **Ctrl+C** (enabled by default via [`exitOnCtrlC`](#exitonctrlc)), call [`exit()`](#exiterrororresult) from [`useApp`](#useapp) inside a component, or call [`unmount()`](#unmount) on the object returned by [`render()`](#rendertree-options).

Use [`waitUntilExit()`](#waituntilexit) to run code after the app is unmounted:

```jsx
const {waitUntilExit} = render(<MyApp />);

await waitUntilExit();

console.log('App exited');
```

## Components

### `<Text>`

This component can display text and change its style to make it bold, underlined, italic, or strikethrough.

```jsx
import {render, Text} from 'ink';

const Example = () => (
	<>
		<Text color="green">I am green</Text>
		<Text color="black" backgroundColor="white">
			I am black on white
		</Text>
		<Text color="#ffffff">I am white</Text>
		<Text bold>I am bold</Text>
		<Text italic>I am italic</Text>
		<Text underline>I am underline</Text>
		<Text strikethrough>I am strikethrough</Text>
		<Text inverse>I am inversed</Text>
	</>
);

render(<Example />);
```

> [!NOTE]
> `<Text>` allows only text nodes and nested `<Text>` components inside of it. For example, `<Box>` component can't be used inside `<Text>`.

#### color

Type: `string`

Change text color.
Ink uses [chalk](https://github.com/chalk/chalk) under the hood, so all its functionality is supported.

```jsx
<Text color="green">Green</Text>
<Text color="#005cc5">Blue</Text>
<Text color="rgb(232, 131, 136)">Red</Text>
```

<img src="media/text-color.jpg" width="247">

#### backgroundColor

Type: `string`

Same as `color` above, but for background.

```jsx
<Text backgroundColor="green" color="white">Green</Text>
<Text backgroundColor="#005cc5" color="white">Blue</Text>
<Text backgroundColor="rgb(232, 131, 136)" color="white">Red</Text>
```

<img src="media/text-backgroundColor.jpg" width="226">

#### dimColor

Type: `boolean`\
Default: `false`

Dim the color (make it less bright).

```jsx
<Text color="red" dimColor>
	Dimmed Red
</Text>
```

<img src="media/text-dimColor.jpg" width="138">

#### bold

Type: `boolean`\
Default: `false`

Make the text bold.

#### italic

Type: `boolean`\
Default: `false`

Make the text italic.

#### underline

Type: `boolean`\
Default: `false`

Make the text underlined.

#### strikethrough

Type: `boolean`\
Default: `false`

Make the text crossed with a line.

#### inverse

Type: `boolean`\
Default: `false`

Invert background and foreground colors.

```jsx
<Text inverse color="yellow">
	Inversed Yellow
</Text>
```

<img src="media/text-inverse.jpg" width="138">

#### wrap

Type: `string`\
Allowed values: `wrap` `truncate` `truncate-start` `truncate-middle` `truncate-end`\
Default: `wrap`

This property tells Ink to wrap or truncate text if its width is larger than the container.
If `wrap` is passed (the default), Ink will wrap text and split it into multiple lines.
If `truncate-*` is passed, Ink will truncate text instead, resulting in one line of text with the rest cut off.

```jsx
<Box width={7}>
	<Text>Hello World</Text>
</Box>
//=> 'Hello\nWorld'

// `truncate` is an alias to `truncate-end`
<Box width={7}>
	<Text wrap="truncate">Hello World</Text>
</Box>
//=> 'Hello…'

<Box width={7}>
	<Text wrap="truncate-middle">Hello World</Text>
</Box>
//=> 'He…ld'

<Box width={7}>
	<Text wrap="truncate-start">Hello World</Text>
</Box>
//=> '…World'
```

### `<Box>`

`<Box>` is an essential Ink component to build your layout.
It's like `<div style="display: flex">` in the browser.

```jsx
import {render, Box, Text} from 'ink';

const Example = () => (
	<Box margin={2}>
		<Text>This is a box with margin</Text>
	</Box>
);

render(<Example />);
```

#### Dimensions

##### width

Type: `number` `string`

Width of the element in spaces.
You can also set it as a percentage, which will calculate the width based on the width of the parent element.

```jsx
<Box width={4}>
	<Text>X</Text>
</Box>
//=> 'X   '
```

```jsx
<Box width={10}>
	<Box width="50%">
		<Text>X</Text>
	</Box>
	<Text>Y</Text>
</Box>
//=> 'X    Y'
```

##### height

Type: `number` `string`

Height of the element in lines (rows).
You can also set it as a percentage, which will calculate the height based on the height of the parent element.

```jsx
<Box height={4}>
	<Text>X</Text>
</Box>
//=> 'X\n\n\n'
```

```jsx
<Box height={6} flexDirection="column">
	<Box height="50%">
		<Text>X</Text>
	</Box>
	<Text>Y</Text>
</Box>
//=> 'X\n\n\nY\n\n'
```

##### minWidth

Type: `number`

Sets a minimum width of the element.
Percentages aren't supported yet; see https://github.com/facebook/yoga/issues/872.

##### minHeight

Type: `number` `string`

Sets a minimum height of the element in lines (rows).
You can also set it as a percentage, which will calculate the minimum height based on the height of the parent element.

##### maxWidth

Type: `number`

Sets a maximum width of the element.
Percentages aren't supported yet; see https://github.com/facebook/yoga/issues/872.

##### maxHeight

Type: `number` `string`

Sets a maximum height of the element in lines (rows).
You can also set it as a percentage, which will calculate the maximum height based on the height of the parent element.

##### aspectRatio

Type: `number`

Defines the aspect ratio (width/height) for the element.

Use it with at least one size constraint (`width`, `height`, `minHeight`, or `maxHeight`) so Ink can derive the missing dimension.

#### Padding

##### paddingTop

Type: `number`\
Default: `0`

Top padding.

##### paddingBottom

Type: `number`\
Default: `0`

Bottom padding.

##### paddingLeft

Type: `number`\
Default: `0`

Left padding.

##### paddingRight

Type: `number`\
Default: `0`

Right padding.

##### paddingX

Type: `number`\
Default: `0`

Horizontal padding. Equivalent to setting `paddingLeft` and `paddingRight`.

##### paddingY

Type: `number`\
Default: `0`

Vertical padding. Equivalent to setting `paddingTop` and `paddingBottom`.

##### padding

Type: `number`\
Default: `0`

Padding on all sides. Equivalent to setting `paddingTop`, `paddingBottom`, `paddingLeft` and `paddingRight`.

```jsx
<Box paddingTop={2}><Text>Top</Text></Box>
<Box paddingBottom={2}><Text>Bottom</Text></Box>
<Box paddingLeft={2}><Text>Left</Text></Box>
<Box paddingRight={2}><Text>Right</Text></Box>
<Box paddingX={2}><Text>Left and right</Text></Box>
<Box paddingY={2}><Text>Top and bottom</Text></Box>
<Box padding={2}><Text>Top, bottom, left and right</Text></Box>
```

#### Margin

##### marginTop

Type: `number`\
Default: `0`

Top margin.

##### marginBottom

Type: `number`\
Default: `0`

Bottom margin.

##### marginLeft

Type: `number`\
Default: `0`

Left margin.

##### marginRight

Type: `number`\
Default: `0`

Right margin.

##### marginX

Type: `number`\
Default: `0`

Horizontal margin. Equivalent to setting `marginLeft` and `marginRight`.

##### marginY

Type: `number`\
Default: `0`

Vertical margin. Equivalent to setting `marginTop` and `marginBottom`.

##### margin

Type: `number`\
Default: `0`

Margin on all sides. Equivalent to setting `marginTop`, `marginBottom`, `marginLeft` and `marginRight`.

```jsx
<Box marginTop={2}><Text>Top</Text></Box>
<Box marginBottom={2}><Text>Bottom</Text></Box>
<Box marginLeft={2}><Text>Left</Text></Box>
<Box marginRight={2}><Text>Right</Text></Box>
<Box marginX={2}><Text>Left and right</Text></Box>
<Box marginY={2}><Text>Top and bottom</Text></Box>
<Box margin={2}><Text>Top, bottom, left and right</Text></Box>
```

#### Gap

#### gap

Type: `number`\
Default: `0`

Size of the gap between an element's columns and rows. A shorthand for `columnGap` and `rowGap`.

```jsx
<Box gap={1} width={3} flexWrap="wrap">
	<Text>A</Text>
	<Text>B</Text>
	<Text>C</Text>
</Box>
// A B
//
// C
```

#### columnGap

Type: `number`\
Default: `0`

Size of the gap between an element's columns.

```jsx
<Box columnGap={1}>
	<Text>A</Text>
	<Text>B</Text>
</Box>
// A B
```

#### rowGap

Type: `number`\
Default: `0`

Size of the gap between an element's rows.

```jsx
<Box flexDirection="column" rowGap={1}>
	<Text>A</Text>
	<Text>B</Text>
</Box>
// A
//
// B
```

#### Flex

##### flexGrow

Type: `number`\
Default: `0`

See [flex-grow](https://css-tricks.com/almanac/properties/f/flex-grow/).

```jsx
<Box>
	<Text>Label:</Text>
	<Box flexGrow={1}>
		<Text>Fills all remaining space</Text>
	</Box>
</Box>
```

##### flexShrink

Type: `number`\
Default: `1`

See [flex-shrink](https://css-tricks.com/almanac/properties/f/flex-shrink/).

```jsx
<Box width={20}>
	<Box flexShrink={2} width={10}>
		<Text>Will be 1/4</Text>
	</Box>
	<Box width={10}>
		<Text>Will be 3/4</Text>
	</Box>
</Box>
```

##### flexBasis

Type: `number` `string`

See [flex-basis](https://css-tricks.com/almanac/properties/f/flex-basis/).

```jsx
<Box width={6}>
	<Box flexBasis={3}>
		<Text>X</Text>
	</Box>
	<Text>Y</Text>
</Box>
//=> 'X  Y'
```

```jsx
<Box width={6}>
	<Box flexBasis="50%">
		<Text>X</Text>
	</Box>
	<Text>Y</Text>
</Box>
//=> 'X  Y'
```

##### flexDirection

Type: `string`\
Allowed values: `row` `row-reverse` `column` `column-reverse`

See [flex-direction](https://css-tricks.com/almanac/properties/f/flex-direction/).

```jsx
<Box>
	<Box marginRight={1}>
		<Text>X</Text>
	</Box>
	<Text>Y</Text>
</Box>
// X Y

<Box flexDirection="row-reverse">
	<Text>X</Text>
	<Box marginRight={1}>
		<Text>Y</Text>
	</Box>
</Box>
// Y X

<Box flexDirection="column">
	<Text>X</Text>
	<Text>Y</Text>
</Box>
// X
// Y

<Box flexDirection="column-reverse">
	<Text>X</Text>
	<Text>Y</Text>
</Box>
// Y
// X
```

##### flexWrap

Type: `string`\
Allowed values: `nowrap` `wrap` `wrap-reverse`

See [flex-wrap](https://css-tricks.com/almanac/properties/f/flex-wrap/).

```jsx
<Box width={2} flexWrap="wrap">
	<Text>A</Text>
	<Text>BC</Text>
</Box>
// A
// B C
```

```jsx
<Box flexDirection="column" height={2} flexWrap="wrap">
	<Text>A</Text>
	<Text>B</Text>
	<Text>C</Text>
</Box>
// A C
// B
```

##### alignItems

Type: `string`\
Allowed values: `flex-start` `center` `flex-end` `stretch` `baseline`

See [align-items](https://css-tricks.com/almanac/properties/a/align-items/).

```jsx
<Box alignItems="flex-start">
	<Box marginRight={1}>
		<Text>X</Text>
	</Box>
	<Text>
		A
		<Newline/>
		B
		<Newline/>
		C
	</Text>
</Box>
// X A
//   B
//   C

<Box alignItems="center">
	<Box marginRight={1}>
		<Text>X</Text>
	</Box>
	<Text>
		A
		<Newline/>
		B
		<Newline/>
		C
	</Text>
</Box>
//   A
// X B
//   C

<Box alignItems="flex-end">
	<Box marginRight={1}>
		<Text>X</Text>
	</Box>
	<Text>
		A
		<Newline/>
		B
		<Newline/>
		C
	</Text>
</Box>
//   A
//   B
// X C
```

##### alignSelf

Type: `string`\
Default: `auto`\
Allowed values: `auto` `flex-start` `center` `flex-end` `stretch` `baseline`

See [align-self](https://css-tricks.com/almanac/properties/a/align-self/).

```jsx
<Box height={3}>
	<Box alignSelf="flex-start">
		<Text>X</Text>
	</Box>
</Box>
// X
//
//

<Box height={3}>
	<Box alignSelf="center">
		<Text>X</Text>
	</Box>
</Box>
//
// X
//

<Box height={3}>
	<Box alignSelf="flex-end">
		<Text>X</Text>
	</Box>
</Box>
//
//
// X
```

##### alignContent

Type: `string`\
Default: `flex-start`\
Allowed values: `flex-start` `flex-end` `center` `stretch` `space-between` `space-around` `space-evenly`

Defines alignment between flex lines on the cross axis when `flexWrap` creates multiple lines.
See [align-content](https://css-tricks.com/almanac/properties/a/align-content/).
Unlike CSS (`stretch`), Ink defaults to `flex-start` so wrapped lines stay compact and fixed-height boxes don't gain unexpected empty rows unless you opt in to stretching.

##### justifyContent

Type: `string`\
Allowed values: `flex-start` `center` `flex-end` `space-between` `space-around` `space-evenly`

See [justify-content](https://css-tricks.com/almanac/properties/j/justify-content/).

```jsx
<Box justifyContent="flex-start">
	<Text>X</Text>
</Box>
// [X      ]

<Box justifyContent="center">
	<Text>X</Text>
</Box>
// [   X   ]

<Box justifyContent="flex-end">
	<Text>X</Text>
</Box>
// [      X]

<Box justifyContent="space-between">
	<Text>X</Text>
	<Text>Y</Text>
</Box>
// [X      Y]

<Box justifyContent="space-around">
	<Text>X</Text>
	<Text>Y</Text>
</Box>
// [  X   Y  ]

<Box justifyContent="space-evenly">
	<Text>X</Text>
	<Text>Y</Text>
</Box>
// [   X   Y   ]
```

#### Position

##### position

Type: `string`\
Allowed values: `relative` `absolute` `static`\
Default: `relative`

Controls how the element is positioned.

When `position` is `static`, `top`, `right`, `bottom`, and `left` are ignored.

##### top

Type: `number` `string`

Top offset for positioned elements.
You can also set it as a percentage of the parent size.

##### right

Type: `number` `string`

Right offset for positioned elements.
You can also set it as a percentage of the parent size.

##### bottom

Type: `number` `string`

Bottom offset for positioned elements.
You can also set it as a percentage of the parent size.

##### left

Type: `number` `string`

Left offset for positioned elements.
You can also set it as a percentage of the parent size.

#### Visibility

##### display

Type: `string`\
Allowed values: `flex` `none`\
Default: `flex`

Set this property to `none` to hide the element.

##### overflowX

Type: `string`\
Allowed values: `visible` `hidden`\
Default: `visible`

Behavior for an element's overflow in the horizontal direction.

##### overflowY

Type: `string`\
Allowed values: `visible` `hidden`\
Default: `visible`

Behavior for an element's overflow in the vertical direction.

##### overflow

Type: `string`\
Allowed values: `visible` `hidden`\
Default: `visible`

A shortcut for setting `overflowX` and `overflowY` at the same time.

#### Borders

##### borderStyle

Type: `string`\
Allowed values: `single` `double` `round` `bold` `singleDouble` `doubleSingle` `classic` | `BoxStyle`

Add a border with a specified style.
If `borderStyle` is `undefined` (the default), no border will be added.
Ink uses border styles from the [`cli-boxes`](https://github.com/sindresorhus/cli-boxes) module.

```jsx
<Box flexDirection="column">
	<Box>
		<Box borderStyle="single" marginRight={2}>
			<Text>single</Text>
		</Box>

		<Box borderStyle="double" marginRight={2}>
			<Text>double</Text>
		</Box>

		<Box borderStyle="round" marginRight={2}>
			<Text>round</Text>
		</Box>

		<Box borderStyle="bold">
			<Text>bold</Text>
		</Box>
	</Box>

	<Box marginTop={1}>
		<Box borderStyle="singleDouble" marginRight={2}>
			<Text>singleDouble</Text>
		</Box>

		<Box borderStyle="doubleSingle" marginRight={2}>
			<Text>doubleSingle</Text>
		</Box>

		<Box borderStyle="classic">
			<Text>classic</Text>
		</Box>
	</Box>
</Box>
```

<img src="media/box-borderStyle.jpg" width="521">

Alternatively, pass a custom border style like so:

```jsx
<Box
	borderStyle={{
		topLeft: '↘',
		top: '↓',
		topRight: '↙',
		left: '→',
		bottomLeft: '↗',
		bottom: '↑',
		bottomRight: '↖',
		right: '←'
	}}
>
	<Text>Custom</Text>
</Box>
```

See example in [examples/borders](examples/borders/borders.tsx).

##### borderColor

Type: `string`

Change border color.
A shorthand for setting `borderTopColor`, `borderRightColor`, `borderBottomColor`, and `borderLeftColor`.

```jsx
<Box borderStyle="round" borderColor="green">
	<Text>Green Rounded Box</Text>
</Box>
```

<img src="media/box-borderColor.jpg" width="228">

##### borderTopColor

Type: `string`

Change top border color.
Accepts the same values as [`color`](#color) in `<Text>` component.

```jsx
<Box borderStyle="round" borderTopColor="green">
	<Text>Hello world</Text>
</Box>
```

##### borderRightColor

Type: `string`

Change the right border color.
Accepts the same values as [`color`](#color) in `<Text>` component.

```jsx
<Box borderStyle="round" borderRightColor="green">
	<Text>Hello world</Text>
</Box>
```

##### borderBottomColor

Type: `string`

Change the bottom border color.
Accepts the same values as [`color`](#color) in `<Text>` component.

```jsx
<Box borderStyle="round" borderBottomColor="green">
	<Text>Hello world</Text>
</Box>
```

##### borderLeftColor

Type: `string`

Change the left border color.
Accepts the same values as [`color`](#color) in `<Text>` component.

```jsx
<Box borderStyle="round" borderLeftColor="green">
	<Text>Hello world</Text>
</Box>
```

##### borderDimColor

Type: `boolean`\
Default: `false`

Dim the border color.
A shorthand for setting `borderTopDimColor`, `borderBottomDimColor`, `borderLeftDimColor`, and `borderRightDimColor`.

```jsx
<Box borderStyle="round" borderDimColor>
	<Text>Hello world</Text>
</Box>
```

##### borderTopDimColor

Type: `boolean`\
Default: `false`

Dim the top border color.

```jsx
<Box borderStyle="round" borderTopDimColor>
	<Text>Hello world</Text>
</Box>
```

##### borderBottomDimColor

Type: `boolean`\
Default: `false`

Dim the bottom border color.

```jsx
<Box borderStyle="round" borderBottomDimColor>
	<Text>Hello world</Text>
</Box>
```

##### borderLeftDimColor

Type: `boolean`\
Default: `false`

Dim the left border color.

```jsx
<Box borderStyle="round" borderLeftDimColor>
	<Text>Hello world</Text>
</Box>
```

##### borderRightDimColor

Type: `boolean`\
Default: `false`

Dim the right border color.

```jsx
<Box borderStyle="round" borderRightDimColor>
	<Text>Hello world</Text>
</Box>
```

##### borderTop

Type: `boolean`\
Default: `true`

Determines whether the top border is visible.

##### borderRight

Type: `boolean`\
Default: `true`

Determines whether the right border is visible.

##### borderBottom

Type: `boolean`\
Default: `true`

Determines whether the bottom border is visible.

##### borderLeft

Type: `boolean`\
Default: `true`

Determines whether the left border is visible.

#### Background

##### backgroundColor

Type: `string`

Background color for the element.

Accepts the same values as [`color`](#color) in the `<Text>` component.

```jsx
<Box flexDirection="column">
	<Box backgroundColor="red" width={20} height={5} alignSelf="flex-start">
		<Text>Red background</Text>
	</Box>

	<Box backgroundColor="#FF8800" width={20} height={3} marginTop={1} alignSelf="flex-start">
		<Text>Orange background</Text>
	</Box>

	<Box backgroundColor="rgb(0, 255, 0)" width={20} height={3} marginTop={1} alignSelf="flex-start">
		<Text>Green background</Text>
	</Box>
</Box>
```

The background color fills the entire `<Box>` area and is inherited by child `<Text>` components unless they specify their own `backgroundColor`.

```jsx
<Box backgroundColor="blue" alignSelf="flex-start">
	<Text>Blue inherited </Text>
	<Text backgroundColor="yellow">Yellow override </Text>
	<Text>Blue inherited again</Text>
</Box>
```

Background colors work with borders and padding:

```jsx
<Box backgroundColor="cyan" borderStyle="round" padding={1} alignSelf="flex-start">
	<Text>Background with border and padding</Text>
</Box>
```

See example in [examples/box-backgrounds](examples/box-backgrounds/box-backgrounds.tsx).

### `<Newline>`

Adds one or more newline (`\n`) characters.
Must be used within `<Text>` components.

#### count

Type: `number`\
Default: `1`

Number of newlines to insert.

```jsx
import {render, Text, Newline} from 'ink';

const Example = () => (
	<Text>
		<Text color="green">Hello</Text>
		<Newline />
		<Text color="red">World</Text>
	</Text>
);

render(<Example />);
```

Output:

```
Hello
World
```

### `<Spacer>`

A flexible space that expands along the major axis of its containing layout.
It's useful as a shortcut for filling all the available space between elements.

For example, using `<Spacer>` in a `<Box>` with default flex direction (`row`) will position "Left" on the left side and will push "Right" to the right side.

```jsx
import {render, Box, Text, Spacer} from 'ink';

const Example = () => (
	<Box>
		<Text>Left</Text>
		<Spacer />
		<Text>Right</Text>
	</Box>
);

render(<Example />);
```

In a vertical flex direction (`column`), it will position "Top" at the top of the container and push "Bottom" to the bottom.
Note that the container needs to be tall enough to see this in effect.

```jsx
import {render, Box, Text, Spacer} from 'ink';

const Example = () => (
	<Box flexDirection="column" height={10}>
		<Text>Top</Text>
		<Spacer />
		<Text>Bottom</Text>
	</Box>
);

render(<Example />);
```

### `<Static>`

`<Static>` component permanently renders its output above everything else.
It's useful for displaying activity like completed tasks or logs - things that
don't change after they're rendered (hence the name "Static").

It's preferred to use `<Static>` for use cases like these when you can't know
or control the number of items that need to be rendered.

For example, [Tap](https://github.com/tapjs/node-tap) uses `<Static>` to display
a list of completed tests. [Gatsby](https://github.com/gatsbyjs/gatsby) uses it
to display a list of generated pages while still displaying a live progress bar.

```jsx
import React, {useState, useEffect} from 'react';
import {render, Static, Box, Text} from 'ink';

const Example = () => {
	const [tests, setTests] = useState([]);

	useEffect(() => {
		let completedTests = 0;
		let timer;

		const run = () => {
			// Fake 10 completed tests
			if (completedTests++ < 10) {
				setTests(previousTests => [
					...previousTests,
					{
						id: previousTests.length,
						title: `Test #${previousTests.length + 1}`
					}
				]);

				timer = setTimeout(run, 100);
			}
		};

		run();

		return () => {
			clearTimeout(timer);
		};
	}, []);

	return (
		<>
			{/* This part will be rendered once to the terminal */}
			<Static items={tests}>
				{test => (
					<Box key={test.id}>
						<Text color="green">✔ {test.title}</Text>
					</Box>
				)}
			</Static>

			{/* This part keeps updating as state changes */}
			<Box marginTop={1}>
				<Text dimColor>Completed tests: {tests.length}</Text>
			</Box>
		</>
	);
};

render(<Example />);
```

> [!NOTE]
> `<Static>` only renders new items in the `items` prop and ignores items
that were previously rendered. This means that when you add new items to the `items`
array, changes you make to previous items will not trigger a rerender.

See [examples/static](examples/static/static.tsx) for an example usage of `<Static>` component.

#### items

Type: `Array`

Array of items of any type to render using the function you pass as a component child.

#### style

Type: `object`

Styles to apply to a container of child elements.
See [`<Box>`](#box) for supported properties.

```jsx
<Static items={...} style={{padding: 1}}>
	{...}
</Static>
```

#### children(item)

Type: `Function`

Function that is called to render every item in the `items` array.
The first argument is the item itself, and the second argument is the index of that item in the
`items` array.

Note that a `key` must be assigned to the root component.

```jsx
<Static items={['a', 'b', 'c']}>
	{(item, index) => {
		// This function is called for every item in ['a', 'b', 'c']
		// `item` is 'a', 'b', 'c'
		// `index` is 0, 1, 2
		return (
			<Box key={index}>
				<Text>Item: {item}</Text>
			</Box>
		);
	}}
</Static>
```

### `<Transform>`

Transform a string representation of React components before they're written to output.
For example, you might want to apply a [gradient to text](https://github.com/sindresorhus/ink-gradient), [add a clickable link](https://github.com/sindresorhus/ink-link), or [create some text effects](https://github.com/sindresorhus/ink-big-text).
These use cases can't accept React nodes as input; they expect a string.
That's what the `<Transform>` component does: it gives you an output string of its child components and lets you transform it in any way.

> [!NOTE]
> `<Transform>` must be applied only to `<Text>` children components and shouldn't change the dimensions of the output; otherwise, the layout will be incorrect.

> [!IMPORTANT]
> When children use `<Text>` styling props (e.g. `color`, `bold`), the string passed to `transform` will contain [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). If your transform manipulates whitespace or does string operations like `.trim()`, you may need to use ANSI-aware methods (e.g. from [`slice-ansi`](https://github.com/chalk/slice-ansi) or [`strip-ansi`](https://github.com/chalk/strip-ansi)).

```jsx
import {render, Transform} from 'ink';

const Example = () => (
	<Transform transform={output => output.toUpperCase()}>
		<Text>Hello World</Text>
	</Transform>
);

render(<Example />);
```

Since the `transform` function converts all characters to uppercase, the final output rendered to the terminal will be "HELLO WORLD", not "Hello World".

When the output wraps to multiple lines, it can be helpful to know which line is being processed.

For example, to implement a hanging indent component, you can indent all the lines except for the first.

```jsx
import {render, Transform} from 'ink';

const HangingIndent = ({indent = 4, children}) => (
	<Transform
		transform={(line, index) =>
			index === 0 ? line : ' '.repeat(indent) + line
		}
	>
		{children}
	</Transform>
);

const text =
	'WHEN I WROTE the following pages, or rather the bulk of them, ' +
	'I lived alone, in the woods, a mile from any neighbor, in a ' +
	'house which I had built myself, on the shore of Walden Pond, ' +
	'in Concord, Massachusetts, and earned my living by the labor ' +
	'of my hands only. I lived there two years and two months. At ' +
	'present I am a sojourner in civilized life again.';

render(
	<HangingIndent indent={4}>
		{text}
	</HangingIndent>
);
```

#### transform(outputLine, index)

Type: `Function`

Function that transforms children output.
It accepts children and must return transformed children as well.

##### children

Type: `string`

Output of child components.

##### index

Type: `number`

The zero-indexed line number of the line that's currently being transformed.

## Hooks

### useInput(inputHandler, options?)

A React hook that returns `void` and handles user input.
It's a more convenient alternative to using `useStdin` and listening for `data` events.
The callback you pass to `useInput` is called for each character when the user enters any input.
However, if the user pastes text and it's more than one character, the callback will be called only once, and the whole string will be passed as `input`.
You can find a full example of using `useInput` at [examples/use-input](examples/use-input/use-input.tsx).

```jsx
import {useInput} from 'ink';

const UserInput = () => {
	useInput((input, key) => {
		if (input === 'q') {
			// Exit program
		}

		if (key.leftArrow) {
			// Left arrow key pressed
		}
	});

	return …
};
```

#### inputHandler(input, key)

Type: `Function`

The handler function that you pass to `useInput` receives two arguments:

##### input

Type: `string`

The input that the program received.

##### key

Type: `object`

Handy information about a key that was pressed.

###### key.leftArrow

###### key.rightArrow

###### key.upArrow

###### key.downArrow

Type: `boolean`\
Default: `false`

If an arrow key was pressed, the corresponding property will be `true`.
For example, if the user presses the left arrow key, `key.leftArrow` equals `true`.

###### key.return

Type: `boolean`\
Default: `false`

Return (Enter) key was pressed.

###### key.escape

Type: `boolean`\
Default: `false`

Escape key was pressed.

###### key.ctrl

Type: `boolean`\
Default: `false`

Ctrl key was pressed.

###### key.shift

Type: `boolean`\
Default: `false`

Shift key was pressed.

###### key.tab

Type: `boolean`\
Default: `false`

Tab key was pressed.

###### key.backspace

Type: `boolean`\
Default: `false`

Backspace key was pressed.

###### key.delete

Type: `boolean`\
Default: `false`

Delete key was pressed.

###### key.pageDown

###### key.pageUp

Type: `boolean`\
Default: `false`

If the Page Up or Page Down key was pressed, the corresponding property will be `true`.
For example, if the user presses Page Down, `key.pageDown` equals `true`.

###### key.home

###### key.end

Type: `boolean`\
Default: `false`

If the Home or End key was pressed, the corresponding property will be `true`.
For example, if the user presses End, `key.end` equals `true`.

###### key.meta

Type: `boolean`\
Default: `false`

[Meta key](https://en.wikipedia.org/wiki/Meta_key) was pressed.

###### key.super

Type: `boolean`\
Default: `false`

Super key (Cmd on macOS, Win on Windows) was pressed. Requires [kitty keyboard protocol](#kittykeyboard).

###### key.hyper

Type: `boolean`\
Default: `false`

Hyper key was pressed. Requires [kitty keyboard protocol](#kittykeyboard).

###### key.capsLock

Type: `boolean`\
Default: `false`

Caps Lock was active. Requires [kitty keyboard protocol](#kittykeyboard).

###### key.numLock

Type: `boolean`\
Default: `false`

Num Lock was active. Requires [kitty keyboard protocol](#kittykeyboard).

###### key.eventType

Type: `'press' | 'repeat' | 'release'`\
Default: `undefined`

The type of key event. Only available with [kitty keyboard protocol](#kittykeyboard). Without the protocol, this property is `undefined`.

#### options

Type: `object`

##### isActive

Type: `boolean`\
Default: `true`

Enable or disable capturing of user input.
Useful when there are multiple `useInput` hooks used at once to avoid handling the same input several times.

### usePaste(handler, options?)

A React hook that calls `handler` whenever the user pastes text. Bracketed paste mode (`\x1b[?2004h`) is automatically enabled while the hook is active, so pasted text arrives as a single string rather than being misinterpreted as individual key presses.

`usePaste` and `useInput` can be used together in the same component. They operate on separate event channels, so paste content is never forwarded to `useInput` handlers when `usePaste` is active.

```jsx
import {useInput, usePaste} from 'ink';

const MyInput = () => {
	useInput((input, key) => {
		// Only receives typed characters and key events, not pasted text.
		if (key.return) {
			// Submit
		}
	});

	usePaste((text) => {
		// Receives the full pasted string, including newlines.
		console.log('Pasted:', text);
	});

	return …
};
```

#### handler(text)

Type: `Function`

Called with the full pasted string whenever the user pastes text. The string is delivered verbatim — newlines, escape sequences, and other special characters are preserved exactly as pasted.

##### text

Type: `string`

The pasted text.

#### options

Type: `object`

##### isActive

Type: `boolean`\
Default: `true`

Enable or disable the paste handler. Useful when multiple components use `usePaste` and only one should be active at a time.

### useApp()

A React hook that returns app lifecycle methods.

#### exit(errorOrResult?)

Type: `Function`

Exit (unmount) the whole Ink app.

##### errorOrResult

Type: `Error | unknown`

Optional value that controls how [`waitUntilExit`](#waituntilexit) settles:
- `exit()` resolves with `undefined`.
- `exit(error)` rejects when `error` is an `Error`.
- `exit(value)` resolves with `value`.

```js
import {useEffect} from 'react';
import {useApp} from 'ink';

const Example = () => {
	const {exit} = useApp();

	// Exit the app after 5 seconds
	useEffect(() => {
		setTimeout(() => {
			exit();
		}, 5000);
	}, [exit]);

	return …
};
```

#### waitUntilRenderFlush()

Type: `Function`

Returns a promise that settles after pending render output is flushed to stdout.

```js
import {useEffect} from 'react';
import {useApp} from 'ink';

const Example = () => {
	const {waitUntilRenderFlush} = useApp();

	useEffect(() => {
		void (async () => {
			await waitUntilRenderFlush();
			runNextCommand();
		})();
	}, [waitUntilRenderFlush]);

	return …;
};
```

### useStdin()

A React hook that returns the stdin stream and stdin-related utilities.

#### stdin

Type: `stream.Readable`\
Default: `process.stdin`

The stdin stream passed to `render()` in `options.stdin`, or `process.stdin` by default.
Useful if your app needs to handle user input.

```js
import {useStdin} from 'ink';

const Example = () => {
	const {stdin} = useStdin();

	return …
};
```

#### isRawModeSupported

Type: `boolean`

A boolean flag determining if the current `stdin` supports `setRawMode`.
A component using `setRawMode` might want to use `isRawModeSupported` to nicely fall back in environments where raw mode is not supported.

```jsx
import {useStdin} from 'ink';

const Example = () => {
	const {isRawModeSupported} = useStdin();

	return isRawModeSupported ? (
		<MyInputComponent />
	) : (
		<MyComponentThatDoesntUseInput />
	);
};
```

#### setRawMode(isRawModeEnabled)

Type: `function`

##### isRawModeEnabled

Type: `boolean`

See [`setRawMode`](https://nodejs.org/api/tty.html#tty_readstream_setrawmode_mode).
Ink exposes this function to be able to handle <kbd>Ctrl</kbd>+<kbd>C</kbd>, that's why you should use Ink's `setRawMode` instead of `process.stdin.setRawMode`.

**Warning:** This function will throw unless the current `stdin` supports `setRawMode`. Use [`isRawModeSupported`](#israwmodesupported) to detect `setRawMode` support.

```js
import {useStdin} from 'ink';

const Example = () => {
	const {setRawMode} = useStdin();

	useEffect(() => {
		setRawMode(true);

		return () => {
			setRawMode(false);
		};
	});

	return …
};
```

### useStdout()

A React hook that returns the stdout stream where Ink renders your app and stdout-related utilities.

#### stdout

Type: `stream.Writable`\
Default: `process.stdout`

```js
import {useStdout} from 'ink';

const Example = () => {
	const {stdout} = useStdout();

	return …
};
```

#### write(data)

Write any string to stdout while preserving Ink's output.
It's useful when you want to display external information outside of Ink's rendering and ensure there's no conflict between the two.
It's similar to `<Static>`, except it can't accept components; it only works with strings.

##### data

Type: `string`

Data to write to stdout.

```js
import {useStdout} from 'ink';

const Example = () => {
	const {write} = useStdout();

	useEffect(() => {
		// Write a single message to stdout, above Ink's output
		write('Hello from Ink to stdout\n');
	}, []);

	return …
};
```

See additional usage example in [examples/use-stdout](examples/use-stdout/use-stdout.tsx).

### useBoxMetrics(ref)

A React hook that returns the current layout metrics for a tracked box element.
It updates when layout changes (for example terminal resize, sibling/content changes, or position changes).

Use `hasMeasured` to detect when the currently tracked element has been measured.

#### ref

Type: `React.RefObject<DOMElement>`

A ref to the `<Box>` element to track.

```jsx
import {useRef} from 'react';
import {Box, Text, useBoxMetrics} from 'ink';

const Example = () => {
	const ref = useRef(null);
	const {width, height, left, top, hasMeasured} = useBoxMetrics(ref);

	return (
		<Box ref={ref}>
			<Text>
				{hasMeasured ? `${width}x${height} at ${left},${top}` : 'Measuring...'}
			</Text>
		</Box>
	);
};
```

#### width

Type: `number`

Element width.

#### height

Type: `number`

Element height.

#### left

Type: `number`

Distance from the left edge of the parent.

#### top

Type: `number`

Distance from the top edge of the parent.

#### hasMeasured

Type: `boolean`

Whether the currently tracked element has been measured.

> [!NOTE]
> The hook returns `{width: 0, height: 0, left: 0, top: 0}` until the first layout pass completes. It also returns zeros when the tracked ref is detached.

### useStderr()

A React hook that returns the stderr stream and stderr-related utilities.

#### stderr

Type: `stream.Writable`\
Default: `process.stderr`

Stderr stream.

```js
import {useStderr} from 'ink';

const Example = () => {
	const {stderr} = useStderr();

	return …
};
```

#### write(data)

Write any string to stderr while preserving Ink's output.

It's useful when you want to display external information outside of Ink's rendering and ensure there's no conflict between the two.
It's similar to `<Static>`, except it can't accept components; it only works with strings.

##### data

Type: `string`

Data to write to stderr.

```js
import {useStderr} from 'ink';

const Example = () => {
	const {write} = useStderr();

	useEffect(() => {
		// Write a single message to stderr, above Ink's output
		write('Hello from Ink to stderr\n');
	}, []);

	return …
};
```

### useWindowSize()

A React hook that returns the current terminal dimensions and re-renders the component whenever the terminal is resized.

```js
import {Text, useWindowSize} from 'ink';

const Example = () => {
	const {columns, rows} = useWindowSize();

	return <Text>{columns}x{rows}</Text>;
};
```

#### columns

Type: `number`

Number of columns (horizontal character cells).

#### rows

Type: `number`

Number of rows (vertical character cells).

### useFocus(options?)

A React hook that returns focus state and focus controls for the current component.
A component that uses the `useFocus` hook becomes "focusable" to Ink, so when the user presses <kbd>Tab</kbd>, Ink will switch focus to this component.
If there are multiple components that execute the `useFocus` hook, focus will be given to them in the order in which these components are rendered.
This hook returns an object with an `isFocused` boolean property, which determines whether this component is focused.

#### options

##### autoFocus

Type: `boolean`\
Default: `false`

Auto-focus this component if there's no active (focused) component right now.

##### isActive

Type: `boolean`\
Default: `true`

Enable or disable this component's focus, while still maintaining its position in the list of focusable components.
This is useful for inputs that are temporarily disabled.

##### id

Type: `string`\
Required: `false`

Set a component's focus ID, which can be used to programmatically focus the component. This is useful for large interfaces with many focusable elements to avoid having to cycle through all of them.

```jsx
import {render, useFocus, Text} from 'ink';

const Example = () => {
	const {isFocused} = useFocus();

	return <Text>{isFocused ? 'I am focused' : 'I am not focused'}</Text>;
};

render(<Example />);
```

See example in [examples/use-focus](examples/use-focus/use-focus.tsx) and [examples/use-focus-with-id](examples/use-focus-with-id/use-focus-with-id.tsx).

### useFocusManager()

A React hook that returns methods to manage focus across focusable components.

#### enableFocus()

Enable focus management for all components.

> [!NOTE]
> You don't need to call this method manually unless you've disabled focus management. Focus management is enabled by default.

```js
import {useFocusManager} from 'ink';

const Example = () => {
	const {enableFocus} = useFocusManager();

	useEffect(() => {
		enableFocus();
	}, []);

	return …
};
```

#### disableFocus()

Disable focus management for all components.
The currently active component (if there's one) will lose its focus.

```js
import {useFocusManager} from 'ink';

const Example = () => {
	const {disableFocus} = useFocusManager();

	useEffect(() => {
		disableFocus();
	}, []);

	return …
};
```

#### focusNext()

Switch focus to the next focusable component.
If there's no active component right now, focus will be given to the first focusable component.
If the active component is the last in the list of focusable components, focus will be switched to the first focusable component.

> [!NOTE]
> Ink calls this method when user presses <kbd>Tab</kbd>.

```js
import {useFocusManager} from 'ink';

const Example = () => {
	const {focusNext} = useFocusManager();

	useEffect(() => {
		focusNext();
	}, []);

	return …
};
```

#### focusPrevious()

Switch focus to the previous focusable component.
If there's no active component right now, focus will be given to the first focusable component.
If the active component is the first in the list of focusable components, focus will be switched to the last focusable component.

> [!NOTE]
> Ink calls this method when user presses <kbd>Shift</kbd>+<kbd>Tab</kbd>.

```js
import {useFocusManager} from 'ink';

const Example = () => {
	const {focusPrevious} = useFocusManager();

	useEffect(() => {
		focusPrevious();
	}, []);

	return …
};
```

#### focus(id)

##### id

Type: `string`

Switch focus to the component with the given [`id`](#id).
If there's no component with that ID, focus is not changed.

```js
import {useFocusManager, useInput} from 'ink';

const Example = () => {
	const {focus} = useFocusManager();

	useInput(input => {
		if (input === 's') {
			// Focus the component with focus ID 'someId'
			focus('someId');
		}
	});

	return …
};
```

#### activeId

Type: `string | undefined`

The ID of the currently focused component, or `undefined` if no component is focused.

```js
import {Text, useFocusManager} from 'ink';

const Example = () => {
	const {activeId} = useFocusManager();

	return <Text>Focused: {activeId ?? 'none'}</Text>;
};
```

### useCursor()

A React hook that returns methods to control the terminal cursor position after each render.
This is essential for IME (Input Method Editor) support, where the composing character is displayed at the cursor location.

```jsx
import {useState} from 'react';
import {Box, Text, useCursor} from 'ink';
import stringWidth from 'string-width';

const TextInput = () => {
	const [text, setText] = useState('');
	const {setCursorPosition} = useCursor();

	const prompt = '> ';
	setCursorPosition({x: stringWidth(prompt + text), y: 1});

	return (
		<Box flexDirection="column">
			<Text>Type here:</Text>
			<Text>{prompt}{text}</Text>
		</Box>
	);
};
```

#### setCursorPosition(position)

Set the cursor position relative to the Ink output. Pass `undefined` to hide the cursor.

##### position

Type: `object | undefined`

Use [`string-width`](https://github.com/sindresorhus/string-width) to calculate `x` for strings containing wide characters (CJK, emoji).

See a full example at [examples/cursor-ime](examples/cursor-ime/cursor-ime.tsx).

###### x

Type: `number`

Column position (0-based).

###### y

Type: `number`

Row position from the top of the Ink output (0 = first line).

### useIsScreenReaderEnabled()

A React hook that returns whether a screen reader is enabled.
This is useful when you want to render different output for screen readers.

```jsx
import {useIsScreenReaderEnabled, Text} from 'ink';

const Example = () => {
	const isScreenReaderEnabled = useIsScreenReaderEnabled();

	return (
		<Text>
			{isScreenReaderEnabled
				? 'Screen reader is enabled'
				: 'Screen reader is disabled'}
		</Text>
	);
};
```

## API

#### render(tree, options?)

Returns: [`Instance`](#instance)

Mount a component and render the output.

##### tree

Type: `ReactNode`

##### options

Type: `object`

###### stdout

Type: `stream.Writable`\
Default: `process.stdout`

Output stream where the app will be rendered.

###### stdin

Type: `stream.Readable`\
Default: `process.stdin`

Input stream where app will listen for input.

###### stderr

Type: `stream.Writable`\
Default: `process.stderr`

Error stream.

###### exitOnCtrlC

Type: `boolean`\
Default: `true`

Configure whether Ink should listen for Ctrl+C keyboard input and exit the app.
This is needed in case `process.stdin` is in [raw mode](https://nodejs.org/api/tty.html#tty_readstream_setrawmode_mode), because then Ctrl+C is ignored by default and the process is expected to handle it manually.

###### patchConsole

Type: `boolean`\
Default: `true`

Patch console methods to ensure console output doesn't mix with Ink's output.
When any of the `console.*` methods are called (like `console.log()`), Ink intercepts their output, clears the main output, renders output from the console method, and then rerenders the main output again.
That way, both are visible and don't overlap each other.

Once unmount starts, Ink restores the native console before React cleanup runs. Teardown-time `console.*` output then follows the normal console behavior instead of being rerouted through Ink.

This functionality is powered by [patch-console](https://github.com/vadimdemedes/patch-console), so if you need to disable Ink's interception of output but want to build something custom, you can use that.

###### onRender

Type: `({renderTime: number}) => void`\
Default: `undefined`

Runs the given callback after each render and re-render with render metrics.
This callback runs after Ink commits a frame, but it does not wait for `stdout`/`stderr` stream callbacks.
To run code after output is flushed, use [`waitUntilRenderFlush()`](#waituntilrenderflush).

###### isScreenReaderEnabled

Type: `boolean`\
Default: `process.env['INK_SCREEN_READER'] === 'true'`

Enable screen reader support. See [Screen Reader Support](#screen-reader-support).

###### debug

Type: `boolean`\
Default: `false`

If `true`, each update will be rendered as separate output, without replacing the previous one.

###### maxFps

Type: `number`\
Default: `30`

Maximum frames per second for render updates.
This controls how frequently the UI can update to prevent excessive re-rendering.
Higher values allow more frequent updates but may impact performance.
Setting it to a lower value may be useful for components that update very frequently, to reduce CPU usage.

###### incrementalRendering

Type: `boolean`\
Default: `false`

Enable incremental rendering mode which only updates changed lines instead of redrawing the entire output.
This can reduce flickering and improve performance for frequently updating UIs.

###### concurrent

Type: `boolean`\
Default: `false`

Enable React Concurrent Rendering mode.

When enabled:
- Suspense boundaries work correctly with async data fetching
- `useTransition` and `useDeferredValue` hooks are fully functional
- Updates can be interrupted for higher priority work

```jsx
render(<MyApp />, {concurrent: true});
```

> [!NOTE]
> Concurrent mode changes the timing of renders. Some tests may need to use `act()` to properly await updates. Reusing the same stdout across multiple `render()` calls without unmounting is unsupported. Call `unmount()` first if you need to change the rendering mode or create a fresh instance.

###### interactive

Type: `boolean`\
Default: `true` (`false` if in CI (detected via [`is-in-ci`](https://github.com/sindresorhus/is-in-ci)) or `stdout.isTTY` is falsy)

Override automatic interactive mode detection.

By default, Ink detects whether the environment is interactive based on CI detection and `stdout.isTTY`. When non-interactive, Ink skips terminal-specific features like ANSI erase sequences, cursor manipulation, synchronized output, resize handling, and kitty keyboard auto-detection. Only the final frame of non-static output is written at unmount.

Most users should not need to set this option. Use it when you have your own "interactive" detection logic that differs from the built-in behavior.

> [!NOTE]
> Reusing the same stdout across multiple `render()` calls without unmounting is unsupported. Call `unmount()` first if you need to change this option or create a fresh instance.

```jsx
// Use your own detection logic
const isInteractive = myCustomDetection();
render(<MyApp />, {interactive: isInteractive});
```

###### alternateScreen

Type: `boolean`\
Default: `false`

Render the app in the terminal's alternate screen buffer. When enabled, the app renders on a separate screen, and the original terminal content is restored when the app exits. This is the same mechanism used by programs like vim, htop, and less.

Note: The terminal's scrollback buffer is not available while in the alternate screen. This is standard terminal behavior; programs like vim use the alternate screen specifically to avoid polluting the user's scrollback history.

Ink intentionally treats alternate-screen teardown output as disposable. It does not preserve or replay teardown-time frames, hook writes, or `console.*` output after restoring the primary screen.

Only works in interactive mode. Ignored when `interactive` is `false` or in a non-interactive environment (CI, piped stdout).

> [!NOTE]
> Reusing the same stdout across multiple `render()` calls without unmounting is unsupported. Call `unmount()` first if you need to change this option or create a fresh instance.

```jsx
render(<MyApp />, {alternateScreen: true});
```

###### kittyKeyboard

Type: `object`\
Default: `undefined`

Enable the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) for enhanced keyboard input handling. When enabled, terminals that support the protocol will report additional key information including `super`, `hyper`, `capsLock`, `numLock` modifiers and `eventType` (press/repeat/release).

```jsx
import {render} from 'ink';

render(<MyApp />, {kittyKeyboard: {mode: 'auto'}});
```

```jsx
import {render} from 'ink';

render(<MyApp />, {
	kittyKeyboard: {
		mode: 'enabled',
		flags: ['disambiguateEscapeCodes', 'reportEventTypes'],
	},
});
```

**kittyKeyboard.mode**

Type: `'auto' | 'enabled' | 'disabled'`\
Default: `'auto'`

- `'auto'`: Detect terminal support using a heuristic precheck (known terminals like kitty, WezTerm, Ghostty) followed by a protocol query confirmation (`CSI ? u`). The protocol is only enabled if the terminal responds to the query within a short timeout.
- `'enabled'`: Force enable the protocol. Both stdin and stdout must be TTYs.
- `'disabled'`: Never enable the protocol.

**kittyKeyboard.flags**

Type: `string[]`\
Default: `['disambiguateEscapeCodes']`

Protocol flags to request from the terminal. Pass an array of flag name strings.

Available flags:
- `'disambiguateEscapeCodes'` - Disambiguate escape codes
- `'reportEventTypes'` - Report key press, repeat, and release events
- `'reportAlternateKeys'` - Report alternate key encodings
- `'reportAllKeysAsEscapeCodes'` - Report all keys as escape codes
- `'reportAssociatedText'` - Report associated text with key events

**Behavior notes**

When the kitty keyboard protocol is enabled, input handling changes in several ways:

- **Non-printable keys produce empty input.** Keys like function keys (F1-F35), modifier-only keys (Shift, Control, Super), media keys, Caps Lock, Print Screen, and similar keys will not produce any text in the `input` parameter of `useInput`. They can still be detected via the `key` object properties.
- **Ctrl+letter shortcuts work as expected.** When the terminal sends `Ctrl+letter` as codepoint 1-26 (the kitty CSI-u alternate form), `input` is set to the letter name (e.g. `'c'` for `Ctrl+C`) and `key.ctrl` is `true`. This ensures `exitOnCtrlC` and custom `Ctrl+letter` handlers continue to work regardless of which codepoint form the terminal uses.
- **Key disambiguation.** The protocol allows the terminal to distinguish between keys that normally produce the same escape sequence. For example:
  - `Ctrl+I` vs `Tab` - without the protocol, both produce the same byte (`\x09`). With the protocol, they are reported as distinct keys.
  - `Shift+Enter` vs `Enter` - the shift modifier is correctly reported.
  - `Escape` key vs `Ctrl+[` - these are disambiguated.
- **Event types.** With the `reportEventTypes` flag, key press, repeat, and release events are distinguished via `key.eventType`.

#### renderToString(tree, options?)

Returns: `string`

Render a React element to a string synchronously. Unlike `render()`, this function does not write to stdout, does not set up any terminal event listeners, and returns the rendered output as a string.

Useful for generating documentation, writing output to files, testing, or any scenario where you need the rendered output as a string without starting a persistent terminal application.

```jsx
import {renderToString, Text, Box} from 'ink';

const output = renderToString(
	<Box padding={1}>
		<Text color="green">Hello World</Text>
	</Box>,
);

console.log(output);
```

**Notes:**

- Terminal-specific hooks (`useInput`, `useStdin`, `useStdout`, `useStderr`, `useWindowSize`, `useApp`, `useFocus`, `useFocusManager`) return default no-op values since there is no terminal session. They will not throw, but they will not function as in a live terminal.
- `useEffect` callbacks will execute during rendering (due to synchronous rendering mode), but state updates they trigger will not affect the returned output, which reflects the initial render.
- `useLayoutEffect` callbacks fire synchronously during commit, so state updates they trigger **will** be reflected in the output.
- The `<Static>` component is supported — its output is prepended to the dynamic output.
- If a component throws during rendering, the error is propagated to the caller after cleanup.

##### tree

Type: `ReactNode`

##### options

Type: `object`

###### columns

Type: `number`\
Default: `80`

Width of the virtual terminal in columns. Controls where text wrapping occurs.

```jsx
const output = renderToString(<Text>{'A'.repeat(100)}</Text>, {
	columns: 40,
});
// Text wraps at 40 columns
```

#### Instance

This is the object that `render()` returns.

##### rerender(tree)

Replace the previous root node with a new one or update the props of the current root node.

###### tree

Type: `ReactNode`

```jsx
// Update props of the root node
const {rerender} = render(<Counter count={1} />);
rerender(<Counter count={2} />);

// Replace root node
const {rerender} = render(<OldCounter />);
rerender(<NewCounter />);
```

##### unmount()

Manually unmount the whole Ink app.

```jsx
const {unmount} = render(<MyApp />);
unmount();
```

##### waitUntilExit()

Returns a promise that settles when the app is unmounted.

It resolves with the value passed to `exit(value)` and rejects with the error passed to `exit(error)`.
When `unmount()` is called manually, it settles after unmount-related stdout writes complete.

```jsx
const {unmount, waitUntilExit} = render(<MyApp />);

setTimeout(unmount, 1000);

await waitUntilExit(); // resolves after `unmount()` is called
```

##### waitUntilRenderFlush()

Returns a promise that settles after pending render output is flushed to stdout.

Useful when you need to run code only after a frame is written:

```jsx
const {rerender, waitUntilRenderFlush} = render(<MyApp step="loading" />);

rerender(<MyApp step="ready" />);
await waitUntilRenderFlush(); // output for "ready" is flushed

runNextCommand();
```

##### cleanup()

Unmount the current app and delete the internal Ink instance associated with the current `stdout`.
This is mostly useful for advanced cases (for example, tests) where you need `render()` to create a fresh instance for the same stream.
Unlike deleting the internal instance directly, this also tears down terminal state such as the alternate screen.

##### clear()

Clear output.

```jsx
const {clear} = render(<MyApp />);
clear();
```

#### measureElement(ref)

Measure the dimensions of a particular `<Box>` element.
Returns an object with `width` and `height` properties.
This function is useful when your component needs to know the amount of available space it has. You can use it when you need to change the layout based on the length of its content.

> [!NOTE]
> `measureElement()` returns `{width: 0, height: 0}` when called during render (before layout is calculated). Call it from post-render code, such as `useEffect`, `useLayoutEffect`, input handlers, or timer callbacks. When content changes, pass the relevant dependency to your effect so it re-measures after each update.

##### ref

Type: `MutableRef`

A reference to a `<Box>` element captured with the `ref` property.
See [Refs](https://reactjs.org/docs/refs-and-the-dom.html) for more information on how to capture references.

```jsx
import {render, measureElement, Box, Text} from 'ink';

const Example = () => {
	const ref = useRef();

	useEffect(() => {
		const {width, height} = measureElement(ref.current);
		// width = 100, height = 1
	}, []);

	return (
		<Box width={100}>
			<Box ref={ref}>
				<Text>This box will stretch to 100 width</Text>
			</Box>
		</Box>
	);
};

render(<Example />);
```

## Testing

Ink components are simple to test with [ink-testing-library](https://github.com/vadimdemedes/ink-testing-library).
Here's a simple example that checks how the component is rendered:

```jsx
import React from 'react';
import {Text} from 'ink';
import {render} from 'ink-testing-library';

const Test = () => <Text>Hello World</Text>;
const {lastFrame} = render(<Test />);

lastFrame() === 'Hello World'; //=> true
```

Check out [ink-testing-library](https://github.com/vadimdemedes/ink-testing-library) for more examples and full documentation.

## Using React Devtools

![](media/devtools.jpg)

Ink supports [React Devtools](https://github.com/facebook/react/tree/master/packages/react-devtools) out of the box. To enable integration with React Devtools in your Ink-based CLI, first ensure you have installed the optional `react-devtools-core` dependency, and then run your app with the `DEV=true` environment variable:

```sh
DEV=true my-cli
```

Then, start React Devtools itself:

```sh
npx react-devtools
```

After it starts, you should see the component tree of your CLI.
You can even inspect and change the props of components, and see the results immediately in the CLI, without restarting it.

> [!NOTE]
> You must manually quit your CLI via <kbd>Ctrl</kbd>+<kbd>C</kbd> after you're done testing.

## Screen Reader Support

Ink has basic support for screen readers.

To enable it, you can either pass the `isScreenReaderEnabled` option to the `render` function or set the `INK_SCREEN_READER` environment variable to `true`.

Ink implements a small subset of functionality from the [ARIA specification](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA).

```jsx
render(<MyApp />, {isScreenReaderEnabled: true});
```

When screen reader support is enabled, Ink will try its best to generate a screen-reader-friendly output.

For example, for this code:

```jsx
<Box aria-role="checkbox" aria-state={{checked: true}}>
	<Text>Accept terms and conditions</Text>
</Box>
```

Ink will generate the following output for screen readers:

```
(checked) checkbox: Accept terms and conditions
```

You can also provide a custom label for screen readers if you want to render something different for them.

For example, if you are building a progress bar, you can use `aria-label` to provide a more descriptive label for screen readers.

```jsx
<Box>
	<Box width="50%" height={1} backgroundColor="green" />
	<Text aria-label="Progress: 50%">50%</Text>
</Box>
```

In the example above, the screen reader will read "Progress: 50%" instead of "50%".

### `aria-label`

Type: `string`

A label for the element for screen readers.

### `aria-hidden`

Type: `boolean`\
Default: `false`

Hide the element from screen readers.

##### aria-role

Type: `string`

The role of the element.

Supported values:
- `button`
- `checkbox`
- `radio`
- `radiogroup`
- `list`
- `listitem`
- `menu`
- `menuitem`
- `progressbar`
- `tab`
- `tablist`
- `timer`
- `toolbar`
- `table`

##### aria-state

Type: `object`

The state of the element.

Supported values:
- `checked` (boolean)
- `disabled` (boolean)
- `expanded` (boolean)
- `selected` (boolean)

## Creating Components

When building custom components, it's important to keep accessibility in mind. While Ink provides the building blocks, ensuring your components are accessible will make your CLIs usable by a wider audience.

### General Principles

- **Provide screen reader-friendly output:** Use the `useIsScreenReaderEnabled` hook to detect if a screen reader is active. You can then render more descriptive output for screen reader users.
- **Leverage ARIA props:** For components that have a specific role (e.g., a checkbox or button), use the `aria-role`, `aria-state`, and `aria-label` props on `<Box>` and `<Text>` to provide semantic meaning to screen readers.

For a practical example of building an accessible component, see the [ARIA example](/examples/aria/aria.tsx).

## Useful Components

- [ink-text-input](https://github.com/vadimdemedes/ink-text-input) - Text input.
- [ink-spinner](https://github.com/vadimdemedes/ink-spinner) - Spinner.
- [ink-select-input](https://github.com/vadimdemedes/ink-select-input) - Select (dropdown) input.
- [ink-link](https://github.com/sindresorhus/ink-link) - Link.
- [ink-gradient](https://github.com/sindresorhus/ink-gradient) - Gradient color.
- [ink-big-text](https://github.com/sindresorhus/ink-big-text) - Awesome text.
- [ink-picture](https://github.com/endernoke/ink-picture) - Display images.
- [ink-tab](https://github.com/jdeniau/ink-tab) - Tab.
- [ink-color-pipe](https://github.com/LitoMore/ink-color-pipe) - Create color text with simpler style strings.
- [ink-multi-select](https://github.com/karaggeorge/ink-multi-select) - Select one or more values from a list
- [ink-divider](https://github.com/JureSotosek/ink-divider) - A divider.
- [ink-progress-bar](https://github.com/brigand/ink-progress-bar) - Progress bar.
- [ink-table](https://github.com/maticzav/ink-table) - Table.
- [ink-ascii](https://github.com/hexrcs/ink-ascii) - Awesome text component with more font choices, based on Figlet.
- [ink-markdown](https://github.com/cameronhunter/ink-markdown) - Render syntax highlighted Markdown.
- [ink-quicksearch-input](https://github.com/Eximchain/ink-quicksearch-input) - Select component with fast, quicksearch-like navigation.
- [ink-confirm-input](https://github.com/kevva/ink-confirm-input) - Yes/No confirmation input.
- [ink-syntax-highlight](https://github.com/vsashyn/ink-syntax-highlight) - Code syntax highlighting.
- [ink-form](https://github.com/lukasbach/ink-form) - Form.
- [ink-task-list](https://github.com/privatenumber/ink-task-list) - Task list.
- [ink-spawn](https://github.com/kraenhansen/ink-spawn) - Spawn child processes.
- [ink-titled-box](https://github.com/mishieck/ink-titled-box) - Box with a title.
- [ink-chart](https://github.com/pppp606/ink-chart) - Sparkline and bar chart.
- [ink-scroll-view](https://github.com/ByteLandTechnology/ink-scroll-view) - Scroll container.
- [ink-scroll-list](https://github.com/ByteLandTechnology/ink-scroll-list) - Scrollable list.
- [ink-stepper](https://github.com/archcorsair/ink-stepper) - Step-by-step wizard.
- [ink-virtual-list](https://github.com/archcorsair/ink-virtual-list) - Virtualized list that renders only visible items for performance.
- [ink-color-picker](https://github.com/sina-byn/ink-color-picker) - Color picker.

## Useful Hooks

- [ink-use-stdout-dimensions](https://github.com/cameronhunter/ink-monorepo/tree/master/packages/ink-use-stdout-dimensions) - Subscribe to stdout dimensions.

## Recipes

- [Routing with React Router](recipes/routing.md) - Navigate between routes using `MemoryRouter`.

## Examples

The [`examples`](/examples) directory contains a set of real examples. You can run them with:

```bash
npm run example examples/[example name]
# e.g. npm run example examples/borders
```

- [Jest](examples/jest/jest.tsx) - Implementation of basic Jest UI.
- [Counter](examples/counter/counter.tsx) - A simple counter that increments every 100ms.
- [Form with validation](https://github.com/final-form/rff-cli-example) - Manage form state using [Final Form](https://github.com/final-form/final-form#-final-form).
- [Borders](examples/borders/borders.tsx) - Add borders to the `<Box>` component.
- [Suspense](examples/suspense/suspense.tsx) - Use React Suspense.
- [Table](examples/table/table.tsx) - Renders a table with multiple columns and rows.
- [Focus management](examples/use-focus/use-focus.tsx) - Use the `useFocus` hook to manage focus between components.
- [User input](examples/use-input/use-input.tsx) - Listen for user input.
- [Write to stdout](examples/use-stdout/use-stdout.tsx) - Write to stdout, bypassing main Ink output.
- [Write to stderr](examples/use-stderr/use-stderr.tsx) - Write to stderr, bypassing main Ink output.
- [Static](examples/static/static.tsx) - Use the `<Static>` component to render permanent output.
- [Child process](examples/subprocess-output) - Renders output from a child process.
- [Router](examples/router/router.tsx) - Navigate between routes using React Router's `MemoryRouter`.

## Continuous Integration

When running on CI (detected via the `CI` environment variable), Ink adapts its rendering:

- Only the last frame is rendered on exit, instead of continuously updating the terminal. This is because most CI environments don't support the ANSI escape sequences used to overwrite previous output.
- Terminal resize events are not listened to.

If your CI environment supports full terminal rendering and you want to opt out of this behavior, set `CI=false`:

```sh
CI=false node my-cli.js
```

## Maintainers

- [Vadim Demedes](https://github.com/vadimdemedes)
- [Sindre Sorhus](https://github.com/sindresorhus)


================================================
FILE: recipes/routing.md
================================================
# Routing with React Router

[React Router](https://reactrouter.com) can be used for routing in Ink apps via its [`MemoryRouter`](https://reactrouter.com/api/declarative-routers/MemoryRouter). Unlike `BrowserRouter`, `MemoryRouter` doesn't rely on the browser's history API, storing the navigation stack in memory instead — which is exactly what a terminal app needs.

```tsx
import React from 'react';
import {MemoryRouter, Routes, Route, useNavigate} from 'react-router';
import {render, useInput, Text} from 'ink';

function Home() {
	const navigate = useNavigate();

	useInput((input, key) => {
		if (key.return) {
			navigate('/about');
		}
	});

	return <Text>Home. Press Enter to go to About.</Text>;
}

function About() {
	const navigate = useNavigate();

	useInput((input, key) => {
		if (key.return) {
			navigate('/');
		}
	});

	return <Text>About. Press Enter to go back Home.</Text>;
}

function App() {
	return (
		<MemoryRouter>
			<Routes>
				<Route path="/" element={<Home />} />
				<Route path="/about" element={<About />} />
			</Routes>
		</MemoryRouter>
	);
}

render(<App />);
```

Things to keep in mind:

- `<Link>` can't be used in Ink since it renders an `<a>` tag. Use the `useNavigate` hook for all navigation instead.
- `MemoryRouter` starts at `"/"` by default. Set the `initialEntries` prop to start at a different route.
- Terminal routing is an abstraction for conditional rendering — routes aren't URLs, they're just screen states.

See [`examples/router`](/examples/router) for a working example.


================================================
FILE: src/ansi-tokenizer.ts
================================================
const bellCharacter = '\u0007';
const escapeCharacter = '\u001B';
const stringTerminatorCharacter = '\u009C';
const csiCharacter = '\u009B';
const oscCharacter = '\u009D';
const dcsCharacter = '\u0090';
const pmCharacter = '\u009E';
const apcCharacter = '\u009F';
const sosCharacter = '\u0098';

type ControlStringType = 'osc' | 'dcs' | 'pm' | 'apc' | 'sos';

type CsiToken = {
	readonly type: 'csi';
	readonly value: string;
	readonly parameterString: string;
	readonly intermediateString: string;
	readonly finalCharacter: string;
};

type EscToken = {
	readonly type: 'esc';
	readonly value: string;
	readonly intermediateString: string;
	readonly finalCharacter: string;
};

type ControlStringToken = {
	readonly type: ControlStringType;
	readonly value: string;
};

type TextToken = {
	readonly type: 'text';
	readonly value: string;
};

type StToken = {
	readonly type: 'st';
	readonly value: string;
};

type C1Token = {
	readonly type: 'c1';
	readonly value: string;
};

type InvalidToken = {
	readonly type: 'invalid';
	readonly value: string;
};

export type AnsiToken =
	| TextToken
	| CsiToken
	| EscToken
	| ControlStringToken
	| StToken
	| C1Token
	| InvalidToken;

const isCsiParameterCharacter = (character: string): boolean => {
	const codePoint = character.codePointAt(0);

	return codePoint !== undefined && codePoint >= 0x30 && codePoint <= 0x3f;
};

const isCsiIntermediateCharacter = (character: string): boolean => {
	const codePoint = character.codePointAt(0);

	return codePoint !== undefined && codePoint >= 0x20 && codePoint <= 0x2f;
};

const isCsiFinalCharacter = (character: string): boolean => {
	const codePoint = character.codePointAt(0);

	return codePoint !== undefined && codePoint >= 0x40 && codePoint <= 0x7e;
};

const isEscapeIntermediateCharacter = (character: string): boolean => {
	const codePoint = character.codePointAt(0);

	return codePoint !== undefined && codePoint >= 0x20 && codePoint <= 0x2f;
};

const isEscapeFinalCharacter = (character: string): boolean => {
	const codePoint = character.codePointAt(0);

	return codePoint !== undefined && codePoint >= 0x30 && codePoint <= 0x7e;
};

const isC1ControlCharacter = (character: string): boolean => {
	const codePoint = character.codePointAt(0);

	return codePoint !== undefined && codePoint >= 0x80 && codePoint <= 0x9f;
};

// Standards references:
// ECMA-48 control functions and CSI byte classes: https://ecma-international.org/publications-and-standards/standards/ecma-48/
// xterm CSI parameter/intermediate/final format notes: https://invisible-island.net/xterm/ecma-48-parameter-format.html
// xterm/OSC BEL termination behavior: https://davidrg.github.io/ckwin/dev/ctlseqs.html
const readCsiSequence = (
	text: string,
	fromIndex: number,
):
	| {
			readonly endIndex: number;
			readonly parameterString: string;
			readonly intermediateString: string;
			readonly finalCharacter: string;
	  }
	| undefined => {
	let index = fromIndex;

	while (index < text.length) {
		const character = text[index]!;

		if (!isCsiParameterCharacter(character)) {
			break;
		}

		index++;
	}

	const parameterString = text.slice(fromIndex, index);
	const intermediateStartIndex = index;

	while (index < text.length) {
		const character = text[index]!;

		if (!isCsiIntermediateCharacter(character)) {
			break;
		}

		index++;
	}

	const intermediateString = text.slice(intermediateStartIndex, index);
	const finalCharacter = text[index];

	if (finalCharacter === undefined || !isCsiFinalCharacter(finalCharacter)) {
		return undefined;
	}

	return {
		endIndex: index + 1,
		parameterString,
		intermediateString,
		finalCharacter,
	};
};

const findControlStringTerminatorIndex = (
	text: string,
	fromIndex: number,
	allowBellTerminator: boolean,
): number | undefined => {
	for (let index = fromIndex; index < text.length; index++) {
		const character = text[index];

		if (allowBellTerminator && character === bellCharacter) {
			return index + 1;
		}

		if (character === stringTerminatorCharacter) {
			return index + 1;
		}

		if (character === escapeCharacter) {
			const followingCharacter = text[index + 1];

			// Tmux escapes ESC bytes in payload as ESC ESC.
			if (followingCharacter === escapeCharacter) {
				index++;
				continue;
			}

			if (followingCharacter === '\\') {
				return index + 2;
			}
		}
	}

	return undefined;
};

const readEscapeSequence = (
	text: string,
	fromIndex: number,
):
	| {
			readonly endIndex: number;
			readonly intermediateString: string;
			readonly finalCharacter: string;
	  }
	| undefined => {
	let index = fromIndex;

	while (index < text.length) {
		const character = text[index]!;

		if (!isEscapeIntermediateCharacter(character)) {
			break;
		}

		index++;
	}

	const intermediateString = text.slice(fromIndex, index);
	const finalCharacter = text[index];

	if (finalCharacter === undefined || !isEscapeFinalCharacter(finalCharacter)) {
		return undefined;
	}

	return {
		endIndex: index + 1,
		intermediateString,
		finalCharacter,
	};
};

// Centralize control-string rules so ESC and C1 paths do not diverge.
const getControlStringFromEscapeIntroducer = (
	character: string,
):
	| {
			readonly type: ControlStringType;
			readonly allowBellTerminator: boolean;
	  }
	| undefined => {
	switch (character) {
		case ']': {
			return {type: 'osc', allowBellTerminator: true};
		}

		case 'P': {
			return {type: 'dcs', allowBellTerminator: false};
		}

		case '^': {
			return {type: 'pm', allowBellTerminator: false};
		}

		case '_': {
			return {type: 'apc', allowBellTerminator: false};
		}

		case 'X': {
			return {type: 'sos', allowBellTerminator: false};
		}

		default: {
			return undefined;
		}
	}
};

const getControlStringFromC1Introducer = (
	character: string,
):
	| {
			readonly type: ControlStringType;
			readonly allowBellTerminator: boolean;
	  }
	| undefined => {
	switch (character) {
		case oscCharacter: {
			return {type: 'osc', allowBellTerminator: true};
		}

		case dcsCharacter: {
			return {type: 'dcs', allowBellTerminator: false};
		}

		case pmCharacter: {
			return {type: 'pm', allowBellTerminator: false};
		}

		case apcCharacter: {
			return {type: 'apc', allowBellTerminator: false};
		}

		case sosCharacter: {
			return {type: 'sos', allowBellTerminator: false};
		}

		default: {
			return undefined;
		}
	}
};

export const hasAnsiControlCharacters = (text: string): boolean => {
	if (text.includes(escapeCharacter)) {
		return true;
	}

	for (const character of text) {
		if (isC1ControlCharacter(character)) {
			return true;
		}
	}

	return false;
};

const malformedFromIndex = (
	tokens: AnsiToken[],
	text: string,
	textStartIndex: number,
	fromIndex: number,
): AnsiToken[] => {
	if (fromIndex > textStartIndex) {
		tokens.push({type: 'text', value: text.slice(textStartIndex, fromIndex)});
	}

	// Treat the remainder as invalid so callers can drop it as one unsafe unit.
	tokens.push({type: 'invalid', value: text.slice(fromIndex)});

	return tokens;
};

export const tokenizeAnsi = (text: string): AnsiToken[] => {
	if (!hasAnsiControlCharacters(text)) {
		return [{type: 'text', value: text}];
	}

	const tokens: AnsiToken[] = [];
	let textStartIndex = 0;

	for (let index = 0; index < text.length; ) {
		const character = text[index];

		if (character === undefined) {
			break;
		}

		if (character === escapeCharacter) {
			const followingCharacter = text[index + 1];

			if (followingCharacter === undefined) {
				return malformedFromIndex(tokens, text, textStartIndex, index);
			}

			if (followingCharacter === '[') {
				const csiSequence = readCsiSequence(text, index + 2);

				if (csiSequence === undefined) {
					return malformedFromIndex(tokens, text, textStartIndex, index);
				}

				if (index > textStartIndex) {
					tokens.push({type: 'text', value: text.slice(textStartIndex, index)});
				}

				tokens.push({
					type: 'csi',
					value: text.slice(index, csiSequence.endIndex),
					parameterString: csiSequence.parameterString,
					intermediateString: csiSequence.intermediateString,
					finalCharacter: csiSequence.finalCharacter,
				});
				index = csiSequence.endIndex;
				textStartIndex = index;
				continue;
			}

			const escapeControlString =
				getControlStringFromEscapeIntroducer(followingCharacter);

			if (escapeControlString !== undefined) {
				const controlStringTerminatorIndex = findControlStringTerminatorIndex(
					text,
					index + 2,
					escapeControlString.allowBellTerminator,
				);

				if (controlStringTerminatorIndex === undefined) {
					return malformedFromIndex(tokens, text, textStartIndex, index);
				}

				if (index > textStartIndex) {
					tokens.push({type: 'text', value: text.slice(textStartIndex, index)});
				}

				tokens.push({
					type: escapeControlString.type,
					value: text.slice(index, controlStringTerminatorIndex),
				});
				index = controlStringTerminatorIndex;
				textStartIndex = index;
				continue;
			}

			const escapeSequence = readEscapeSequence(text, index + 1);

			if (escapeSequence === undefined) {
				// Incomplete escape sequences with intermediates are malformed control strings.
				if (isEscapeIntermediateCharacter(followingCharacter)) {
					return malformedFromIndex(tokens, text, textStartIndex, index);
				}

				if (index > textStartIndex) {
					tokens.push({type: 'text', value: text.slice(textStartIndex, index)});
				}

				// Ignore lone ESC and continue tokenizing the rest.
				index++;
				textStartIndex = index;
				continue;
			}

			if (index > textStartIndex) {
				tokens.push({type: 'text', value: text.slice(textStartIndex, index)});
			}

			tokens.push({
				type: 'esc',
				value: text.slice(index, escapeSequence.endIndex),
				intermediateString: escapeSequence.intermediateString,
				finalCharacter: escapeSequence.finalCharacter,
			});
			index = escapeSequence.endIndex;
			textStartIndex = index;
			continue;
		}

		if (character === csiCharacter) {
			const csiSequence = readCsiSequence(text, index + 1);

			if (csiSequence === undefined) {
				return malformedFromIndex(tokens, text, textStartIndex, index);
			}

			if (index > textStartIndex) {
				tokens.push({type: 'text', value: text.slice(textStartIndex, index)});
			}

			tokens.push({
				type: 'csi',
				value: text.slice(index, csiSequence.endIndex),
				parameterString: csiSequence.parameterString,
				intermediateString: csiSequence.intermediateString,
				finalCharacter: csiSequence.finalCharacter,
			});
			index = csiSequence.endIndex;
			textStartIndex = index;
			continue;
		}

		const c1ControlString = getControlStringFromC1Introducer(character);

		if (c1ControlString !== undefined) {
			const controlStringTerminatorIndex = findControlStringTerminatorIndex(
				text,
				index + 1,
				c1ControlString.allowBellTerminator,
			);

			if (controlStringTerminatorIndex === undefined) {
				return malformedFromIndex(tokens, text, textStartIndex, index);
			}

			if (index > textStartIndex) {
				tokens.push({type: 'text', value: text.slice(textStartIndex, index)});
			}

			tokens.push({
				type: c1ControlString.type,
				value: text.slice(index, controlStringTerminatorIndex),
			});
			index = controlStringTerminatorIndex;
			textStartIndex = index;
			continue;
		}

		if (character === stringTerminatorCharacter) {
			if (index > textStartIndex) {
				tokens.push({type: 'text', value: text.slice(textStartIndex, index)});
			}

			tokens.push({type: 'st', value: character});
			index++;
			textStartIndex = index;
			continue;
		}

		// Strip remaining C1 controls as standalone functions.
		if (isC1ControlCharacter(character)) {
			if (index > textStartIndex) {
				tokens.push({type: 'text', value: text.slice(textStartIndex, index)});
			}

			tokens.push({type: 'c1', value: character});
			index++;
			textStartIndex = index;
			continue;
		}

		index++;
	}

	if (textStartIndex < text.length) {
		tokens.push({type: 'text', value: text.slice(textStartIndex)});
	}

	return tokens;
};


================================================
FILE: src/colorize.ts
================================================
import chalk, {type ForegroundColorName, type BackgroundColorName} from 'chalk';

type ColorType = 'foreground' | 'background';

const rgbRegex = /^rgb\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)$/;
const ansiRegex = /^ansi256\(\s?(\d+)\s?\)$/;

const isNamedColor = (color: string): color is ForegroundColorName => {
	return color in chalk;
};

const colorize = (
	str: string,
	color: string | undefined,
	type: ColorType,
): string => {
	if (!color) {
		return str;
	}

	if (isNamedColor(color)) {
		if (type === 'foreground') {
			return chalk[color](str);
		}

		const methodName = `bg${
			color[0]!.toUpperCase() + color.slice(1)
		}` as BackgroundColorName;

		return chalk[methodName](str);
	}

	if (color.startsWith('#')) {
		return type === 'foreground'
			? chalk.hex(color)(str)
			: chalk.bgHex(color)(str);
	}

	if (color.startsWith('ansi256')) {
		const matches = ansiRegex.exec(color);

		if (!matches) {
			return str;
		}

		const value = Number(matches[1]);

		return type === 'foreground'
			? chalk.ansi256(value)(str)
			: chalk.bgAnsi256(value)(str);
	}

	if (color.startsWith('rgb')) {
		const matches = rgbRegex.exec(color);

		if (!matches) {
			return str;
		}

		const firstValue = Number(matches[1]);
		const secondValue = Number(matches[2]);
		const thirdValue = Number(matches[3]);

		return type === 'foreground'
			? chalk.rgb(firstValue, secondValue, thirdValue)(str)
			: chalk.bgRgb(firstValue, secondValue, thirdValue)(str);
	}

	return str;
};

export default colorize;


================================================
FILE: src/components/AccessibilityContext.ts
================================================
import {createContext} from 'react';

export const accessibilityContext = createContext({
	isScreenReaderEnabled: false,
});


================================================
FILE: src/components/App.tsx
================================================
import {EventEmitter} from 'node:events';
import process from 'node:process';
import React, {
	type ReactNode,
	useState,
	useRef,
	useCallback,
	useMemo,
	useEffect,
} from 'react';
import cliCursor from 'cli-cursor';
import {type CursorPosition} from '../log-update.js';
import {createInputParser} from '../input-parser.js';
import AppContext from './AppContext.js';
import StdinContext from './StdinContext.js';
import StdoutContext from './StdoutContext.js';
import StderrContext from './StderrContext.js';
import FocusContext from './FocusContext.js';
import CursorContext from './CursorContext.js';
import ErrorBoundary from './ErrorBoundary.js';

const tab = '\t';
const shiftTab = '\u001B[Z';
const escape = '\u001B';

type Props = {
	readonly children: ReactNode;
	readonly stdin: NodeJS.ReadStream;
	readonly stdout: NodeJS.WriteStream;
	readonly stderr: NodeJS.WriteStream;
	readonly writeToStdout: (data: string) => void;
	readonly writeToStderr: (data: string) => void;
	readonly exitOnCtrlC: boolean;
	readonly onExit: (errorOrResult?: unknown) => void;
	readonly onWaitUntilRenderFlush: () => Promise<void>;
	readonly setCursorPosition: (position: CursorPosition | undefined) => void;
	readonly interactive: boolean;
};

type Focusable = {
	readonly id: string;
	readonly isActive: boolean;
};

// Root component for all Ink apps
// It renders stdin and stdout contexts, so that children can access them if needed
// It also handles Ctrl+C exiting and cursor visibility
function App({
	children,
	stdin,
	stdout,
	stderr,
	writeToStdout,
	writeToStderr,
	exitOnCtrlC,
	onExit,
	onWaitUntilRenderFlush,
	setCursorPosition,
	interactive,
}: Props): React.ReactNode {
	const [isFocusEnabled, setIsFocusEnabled] = useState(true);
	const [activeFocusId, setActiveFocusId] = useState<string | undefined>(
		undefined,
	);
	// Focusables array is managed internally via setFocusables callback pattern
	// eslint-disable-next-line react/hook-use-state
	const [, setFocusables] = useState<Focusable[]>([]);
	// Track focusables count for tab navigation check (avoids stale closure)
	const focusablesCountRef = useRef(0);

	// Count how many components enabled raw mode to avoid disabling
	// raw mode until all components don't need it anymore
	const rawModeEnabledCount = useRef(0);
	// Count how many components enabled bracketed paste mode
	const bracketedPasteModeEnabledCount = useRef(0);
	// eslint-disable-next-line @typescript-eslint/naming-convention
	const internal_eventEmitter = useRef(new EventEmitter());
	// Each useInput hook adds a listener, so the count can legitimately exceed the default limit of 10.
	internal_eventEmitter.current.setMaxListeners(Infinity);
	// Store the currently attached readable listener to avoid stale closure issues
	const readableListenerRef = useRef<(() => void) | undefined>(undefined);
	const inputParserRef = useRef(createInputParser());
	const pendingInputFlushRef = useRef<NodeJS.Timeout | undefined>(undefined);
	// Small delay to let chunked escape sequences complete before flushing as literal input.
	const pendingInputFlushDelayMilliseconds = 20;

	const clearPendingInputFlush = useCallback((): void => {
		if (!pendingInputFlushRef.current) {
			return;
		}

		clearTimeout(pendingInputFlushRef.current);
		pendingInputFlushRef.current = undefined;
	}, []);

	// Determines if TTY is supported on the provided stdin
	const isRawModeSupported = stdin.isTTY;

	const detachReadableListener = useCallback((): void => {
		if (!readableListenerRef.current) {
			return;
		}

		stdin.removeListener('readable', readableListenerRef.current);
		readableListenerRef.current = undefined;
	}, [stdin]);

	const disableRawMode = useCallback((): void => {
		stdin.setRawMode(false);
		detachReadableListener();
		stdin.unref();
		rawModeEnabledCount.current = 0;
		inputParserRef.current.reset();
		clearPendingInputFlush();
	}, [stdin, detachReadableListener, clearPendingInputFlush]);

	const handleExit = useCallback(
		(errorOrResult?: unknown): void => {
			if (isRawModeSupported && rawModeEnabledCount.current > 0) {
				disableRawMode();
			}

			onExit(errorOrResult);
		},
		[isRawModeSupported, disableRawMode, onExit],
	);

	const handleInput = useCallback(
		(input: string): void => {
			// Exit on Ctrl+C
			// eslint-disable-next-line unicorn/no-hex-escape
			if (input === '\x03' && exitOnCtrlC) {
				handleExit();
				return;
			}

			// Reset focus when there's an active focused component on Esc
			if (input === escape) {
				setActiveFocusId(currentActiveFocusId => {
					if (currentActiveFocusId) {
						return undefined;
					}

					return currentActiveFocusId;
				});
			}
		},
		[exitOnCtrlC, handleExit],
	);

	const emitInput = useCallback(
		(input: string): void => {
			handleInput(input);
			internal_eventEmitter.current.emit('input', input);
		},
		[handleInput],
	);

	const schedulePendingInputFlush = useCallback((): void => {
		clearPendingInputFlush();
		pendingInputFlushRef.current = setTimeout(() => {
			pendingInputFlushRef.current = undefined;
			const pendingEscape = inputParserRef.current.flushPendingEscape();
			if (!pendingEscape) {
				return;
			}

			emitInput(pendingEscape);
		}, pendingInputFlushDelayMilliseconds);
	}, [clearPendingInputFlush, emitInput]);

	const handleReadable = useCallback((): void => {
		clearPendingInputFlush();
		let chunk;
		// eslint-disable-next-line @typescript-eslint/no-restricted-types
		while ((chunk = stdin.read() as string | null) !== null) {
			const inputEvents = inputParserRef.current.push(chunk);
			for (const event of inputEvents) {
				if (typeof event === 'string') {
					emitInput(event);
				} else {
					// Keep paste on a separate channel from `useInput` so key handlers
					// don't need to branch on mixed key-vs-paste event shapes.
					if (internal_eventEmitter.current.listenerCount('paste') === 0) {
						emitInput(event.paste);
						continue;
					}

					internal_eventEmitter.current.emit('paste', event.paste);
				}
			}
		}

		if (inputParserRef.current.hasPendingEscape()) {
			schedulePendingInputFlush();
		}
	}, [stdin, emitInput, clearPendingInputFlush, schedulePendingInputFlush]);

	const handleSetRawMode = useCallback(
		(isEnabled: boolean): void => {
			if (!isRawModeSupported) {
				if (stdin === process.stdin) {
					throw new Error(
						'Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported',
					);
				} else {
					throw new Error(
						'Raw mode is not supported on the stdin provided to Ink.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported',
					);
				}
			}

			stdin.setEncoding('utf8');

			if (isEnabled) {
				// Ensure raw mode is enabled only once
				if (rawModeEnabledCount.current === 0) {
					stdin.ref();
					stdin.setRawMode(true);
					// Store the listener reference to avoid stale closure when removing
					readableListenerRef.current = handleReadable;
					stdin.addListener('readable', handleReadable);
				}

				rawModeEnabledCount.current++;
				return;
			}

			// Disable raw mode only when no components left that are using it
			if (rawModeEnabledCount.current === 0) {
				return;
			}

			if (--rawModeEnabledCount.current === 0) {
				disableRawMode();
			}
		},
		[isRawModeSupported, stdin, handleReadable, disableRawMode],
	);

	const handleSetBracketedPasteMode = useCallback(
		(isEnabled: boolean): void => {
			if (!stdout.isTTY) {
				return;
			}

			if (isEnabled) {
				if (bracketedPasteModeEnabledCount.current === 0) {
					stdout.write('\u001B[?2004h');
				}

				bracketedPasteModeEnabledCount.current++;
				return;
			}

			if (bracketedPasteModeEnabledCount.current === 0) {
				return;
			}

			if (--bracketedPasteModeEnabledCount.current === 0) {
				stdout.write('\u001B[?2004l');
			}
		},
		[stdout],
	);

	// Focus navigation helpers
	const findNextFocusable = useCallback(
		(
			currentFocusables: Focusable[],
			currentActiveFocusId: string | undefined,
		): string | undefined => {
			const activeIndex = currentFocusables.findIndex(focusable => {
				return focusable.id === currentActiveFocusId;
			});

			for (
				let index = activeIndex + 1;
				index < currentFocusables.length;
				index++
			) {
				const focusable = currentFocusables[index];

				if (focusable?.isActive) {
					return focusable.id;
				}
			}

			return undefined;
		},
		[],
	);

	const findPreviousFocusable = useCallback(
		(
			currentFocusables: Focusable[],
			currentActiveFocusId: string | undefined,
		): string | undefined => {
			const activeIndex = currentFocusables.findIndex(focusable => {
				return focusable.id === currentActiveFocusId;
			});

			for (let index = activeIndex - 1; index >= 0; index--) {
				const focusable = currentFocusables[index];

				if (focusable?.isActive) {
					return focusable.id;
				}
			}

			return undefined;
		},
		[],
	);

	const focusNext = useCallback((): void => {
		setFocusables(currentFocusables => {
			setActiveFocusId(currentActiveFocusId => {
				const firstFocusableId = currentFocusables.find(
					focusable => focusable.isActive,
				)?.id;
				const nextFocusableId = findNextFocusable(
					currentFocusables,
					currentActiveFocusId,
				);

				return nextFocusableId ?? firstFocusableId;
			});
			return currentFocusables;
		});
	}, [findNextFocusable]);

	const focusPrevious = useCallback((): void => {
		setFocusables(currentFocusables => {
			setActiveFocusId(currentActiveFocusId => {
				const lastFocusableId = currentFocusables.findLast(
					focusable => focusable.isActive,
				)?.id;
				const previousFocusableId = findPreviousFocusable(
					currentFocusables,
					currentActiveFocusId,
				);

				return previousFocusableId ?? lastFocusableId;
			});
			return currentFocusables;
		});
	}, [findPreviousFocusable]);

	// Handle tab navigation via effect that subscribes to input events
	useEffect(() => {
		const handleTabNavigation = (input: string): void => {
			if (!isFocusEnabled || focusablesCountRef.current === 0) return;

			if (input === tab) {
				focusNext();
			}

			if (input === shiftTab) {
				focusPrevious();
			}
		};

		internal_eventEmitter.current.on('input', handleTabNavigation);
		const emitter = internal_eventEmitter.current;

		return () => {
			emitter.off('input', handleTabNavigation);
		};
	}, [isFocusEnabled, focusNext, focusPrevious]);

	const enableFocus = useCallback((): void => {
		setIsFocusEnabled(true);
	}, []);

	const disableFocus = useCallback((): void => {
		setIsFocusEnabled(false);
	}, []);

	const focus = useCallback((id: string): void => {
		setFocusables(currentFocusables => {
			const hasFocusableId = currentFocusables.some(
				focusable => focusable?.id === id,
			);

			if (hasFocusableId) {
				setActiveFocusId(id);
			}

			return currentFocusables;
		});
	}, []);

	const addFocusable = useCallback(
		(id: string, {autoFocus}: {autoFocus: boolean}): void => {
			setFocusables(currentFocusables => {
				focusablesCountRef.current = currentFocusables.length + 1;

				return [
					...currentFocusables,
					{
						id,
						isActive: true,
					},
				];
			});

			if (autoFocus) {
				setActiveFocusId(currentActiveFocusId => {
					if (!currentActiveFocusId) {
						return id;
					}

					return currentActiveFocusId;
				});
			}
		},
		[],
	);

	const removeFocusable = useCallback((id: string): void => {
		setActiveFocusId(currentActiveFocusId => {
			if (currentActiveFocusId === id) {
				return undefined;
			}

			return currentActiveFocusId;
		});

		setFocusables(currentFocusables => {
			const filtered = currentFocusables.filter(focusable => {
				return focusable.id !== id;
			});
			focusablesCountRef.current = filtered.length;

			return filtered;
		});
	}, []);

	const activateFocusable = useCallback((id: string): void => {
		setFocusables(currentFocusables =>
			currentFocusables.map(focusable => {
				if (focusable.id !== id) {
					return focusable;
				}

				return {
					id,
					isActive: true,
				};
			}),
		);
	}, []);

	const deactivateFocusable = useCallback((id: string): void => {
		setActiveFocusId(currentActiveFocusId => {
			if (currentActiveFocusId === id) {
				return undefined;
			}

			return currentActiveFocusId;
		});

		setFocusables(currentFocusables =>
			currentFocusables.map(focusable => {
				if (focusable.id !== id) {
					return focusable;
				}

				return {
					id,
					isActive: false,
				};
			}),
		);
	}, []);

	// Handle cursor visibility, raw mode, and bracketed paste mode cleanup on unmount
	useEffect(() => {
		return () => {
			const canWriteToStdout = !stdout.destroyed && !stdout.writableEnded;

			if (interactive && canWriteToStdout) {
				cliCursor.show(stdout);
			}

			if (isRawModeSupported && rawModeEnabledCount.current > 0) {
				disableRawMode();
			}

			if (bracketedPasteModeEnabledCount.current > 0) {
				if (stdout.isTTY && canWriteToStdout) {
					stdout.write('\u001B[?2004l');
				}

				bracketedPasteModeEnabledCount.current = 0;
			}
		};
	}, [stdout, isRawModeSupported, disableRawMode, interactive]);

	// Memoize context values to prevent unnecessary re-renders
	const appContextValue = useMemo(
		() => ({
			exit: handleExit,
			waitUntilRenderFlush: onWaitUntilRenderFlush,
		}),
		[handleExit, onWaitUntilRenderFlush],
	);

	const stdinContextValue = useMemo(
		() => ({
			stdin,
			setRawMode: handleSetRawMode,
			setBracketedPasteMode: handleSetBracketedPasteMode,
			isRawModeSupported,
			// eslint-disable-next-line @typescript-eslint/naming-convention
			internal_exitOnCtrlC: exitOnCtrlC,
			// eslint-disable-next-line @typescript-eslint/naming-convention
			internal_eventEmitter: internal_eventEmitter.current,
		}),
		[
			stdin,
			handleSetRawMode,
			handleSetBracketedPasteMode,
			isRawModeSupported,
			exitOnCtrlC,
		],
	);

	const stdoutContextValue = useMemo(
		() => ({
			stdout,
			write: writeToStdout,
		}),
		[stdout, writeToStdout],
	);

	const stderrContextValue = useMemo(
		() => ({
			stderr,
			write: writeToStderr,
		}),
		[stderr, writeToStderr],
	);

	const cursorContextValue = useMemo(
		() => ({
			setCursorPosition,
		}),
		[setCursorPosition],
	);

	const focusContextValue = useMemo(
		() => ({
			activeId: activeFocusId,
			add: addFocusable,
			remove: removeFocusable,
			activate: activateFocusable,
			deactivate: deactivateFocusable,
			enableFocus,
			disableFocus,
			focusNext,
			focusPrevious,
			focus,
		}),
		[
			activeFocusId,
			addFocusable,
			removeFocusable,
			activateFocusable,
			deactivateFocusable,
			enableFocus,
			disableFocus,
			focusNext,
			focusPrevious,
			focus,
		],
	);

	return (
		<AppContext.Provider value={appContextValue}>
			<StdinContext.Provider value={stdinContextValue}>
				<StdoutContext.Provider value={stdoutContextValue}>
					<StderrContext.Provider value={stderrContextValue}>
						<FocusContext.Provider value={focusContextValue}>
							<CursorContext.Provider value={cursorContextValue}>
								<ErrorBoundary onError={handleExit}>{children}</ErrorBoundary>
							</CursorContext.Provider>
						</FocusContext.Provider>
					</StderrContext.Provider>
				</StdoutContext.Provider>
			</StdinContext.Provider>
		</AppContext.Provider>
	);
}

App.displayName = 'InternalApp';

export default App;


================================================
FILE: src/components/AppContext.ts
================================================
import {createContext} from 'react';

export type Props = {
	/**
	Exit (unmount) the whole Ink app.

	- `exit()` — resolves `waitUntilExit()` with `undefined`.
	- `exit(new Error('…'))` — rejects `waitUntilExit()` with the error.
	- `exit(value)` — resolves `waitUntilExit()` with `value`.
	*/
	readonly exit: (errorOrResult?: Error | unknown) => void;

	/**
	Returns a promise that settles after pending render output is flushed to stdout.

	@example
	```jsx
	import {useEffect} from 'react';
	import {useApp} from 'ink';

	const Example = () => {
		const {waitUntilRenderFlush} = useApp();

		useEffect(() => {
			void (async () => {
				await waitUntilRenderFlush();
				runNextCommand();
			})();
		}, [waitUntilRenderFlush]);

		return …;
	};
	```
	*/
	readonly waitUntilRenderFlush: () => Promise<void>;
};

/**
`AppContext` is a React context that exposes lifecycle methods for the app.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
const AppContext = createContext<Props>({
	exit() {},
	async waitUntilRenderFlush() {},
});

AppContext.displayName = 'InternalAppContext';

export default AppContext;


================================================
FILE: src/components/BackgroundContext.ts
================================================
import {createContext} from 'react';
import {type LiteralUnion} from 'type-fest';
import {type ForegroundColorName} from 'ansi-styles';

export type BackgroundColor = LiteralUnion<ForegroundColorName, string>;

export const backgroundContext = createContext<BackgroundColor | undefined>(
	undefined,
);


================================================
FILE: src/components/Box.tsx
================================================
import React, {forwardRef, useContext, type PropsWithChildren} from 'react';
import {type Except} from 'type-fest';
import {type Styles} from '../styles.js';
import {type DOMElement} from '../dom.js';
import {accessibilityContext} from './AccessibilityContext.js';
import {backgroundContext} from './BackgroundContext.js';

export type Props = Except<Styles, 'textWrap'> & {
	/**
	A label for the element for screen readers.
	*/
	readonly 'aria-label'?: string;

	/**
	Hide the element from screen readers.
	*/
	readonly 'aria-hidden'?: boolean;

	/**
	The role of the element.
	*/
	readonly 'aria-role'?:
		| 'button'
		| 'checkbox'
		| 'combobox'
		| 'list'
		| 'listbox'
		| 'listitem'
		| 'menu'
		| 'menuitem'
		| 'option'
		| 'progressbar'
		| 'radio'
		| 'radiogroup'
		| 'tab'
		| 'tablist'
		| 'table'
		| 'textbox'
		| 'timer'
		| 'toolbar';

	/**
	The state of the element.
	*/
	readonly 'aria-state'?: {
		readonly busy?: boolean;
		readonly checked?: boolean;
		readonly disabled?: boolean;
		readonly expanded?: boolean;
		readonly multiline?: boolean;
		readonly multiselectable?: boolean;
		readonly readonly?: boolean;
		readonly required?: boolean;
		readonly selected?: boolean;
	};
};

/**
`<Box>` is an essential Ink component to build your layout. It's like `<div style="display: flex">` in the browser.
*/
const Box = forwardRef<DOMElement, PropsWithChildren<Props>>(
	(
		{
			children,
			backgroundColor,
			'aria-label': ariaLabel,
			'aria-hidden': ariaHidden,
			'aria-role': role,
			'aria-state': ariaState,
			...style
		},
		ref,
	) => {
		const {isScreenReaderEnabled} = useContext(accessibilityContext);
		const label = ariaLabel ? <ink-text>{ariaLabel}</ink-text> : undefined;
		if (isScreenReaderEnabled && ariaHidden) {
			return null;
		}

		const boxElement = (
			<ink-box
				ref={ref}
				style={{
					flexWrap: 'nowrap',
					flexDirection: 'row',
					flexGrow: 0,
					flexShrink: 1,
					...style,
					backgroundColor,
					overflowX: style.overflowX ?? style.overflow ?? 'visible',
					overflowY: style.overflowY ?? style.overflow ?? 'visible',
				}}
				internal_accessibility={{
					role,
					state: ariaState,
				}}
			>
				{isScreenReaderEnabled && label ? label : children}
			</ink-box>
		);

		// If this Box has a background color, provide it to children via context
		if (backgroundColor) {
			return (
				<backgroundContext.Provider value={backgroundColor}>
					{boxElement}
				</backgroundContext.Provider>
			);
		}

		return boxElement;
	},
);

Box.displayName = 'Box';

export default Box;


================================================
FILE: src/components/CursorContext.ts
================================================
import {createContext} from 'react';
import {type CursorPosition} from '../log-update.js';

export type Props = {
	/**
	Set the cursor position relative to the Ink output.

	Pass `undefined` to hide the cursor.
	*/
	readonly setCursorPosition: (position: CursorPosition | undefined) => void;
};

// eslint-disable-next-line @typescript-eslint/naming-convention
const CursorContext = createContext<Props>({
	setCursorPosition() {},
});

CursorContext.displayName = 'InternalCursorContext';

export default CursorContext;


================================================
FILE: src/components/ErrorBoundary.tsx
================================================
import React, {PureComponent, type ReactNode} from 'react';
import ErrorOverview from './ErrorOverview.js';

type Props = {
	readonly children: ReactNode;
	readonly onError: (error: Error) => void;
};

type State = {
	readonly error?: Error;
};

// Error boundary must be a class component since getDerivedStateFromError
// and componentDidCatch are not available as hooks
export default class ErrorBoundary extends PureComponent<Props, State> {
	static displayName = 'InternalErrorBoundary';

	static getDerivedStateFromError(error: Error) {
		return {error};
	}

	override state: State = {
		error: undefined,
	};

	override componentDidCatch(error: Error): void {
		this.props.onError(error);
	}

	override render(): ReactNode {
		if (this.state.error) {
			return <ErrorOverview error={this.state.error} />;
		}

		return this.props.children;
	}
}


================================================
FILE: src/components/ErrorOverview.tsx
================================================
import * as fs from 'node:fs';
import {cwd} from 'node:process';
import React from 'react';
import StackUtils from 'stack-utils';
import codeExcerpt, {type CodeExcerpt} from 'code-excerpt';
import Box from './Box.js';
import Text from './Text.js';

// Error's source file is reported as file:///home/user/file.js
// This function removes the file://[cwd] part
const cleanupPath = (path: string | undefined): string | undefined => {
	return path?.replace(`file://${cwd()}/`, '');
};

const stackUtils = new StackUtils({
	cwd: cwd(),
	internals: StackUtils.nodeInternals(),
});

type Props = {
	readonly error: Error;
};

export default function ErrorOverview({error}: Props) {
	const stack = error.stack ? error.stack.split('\n').slice(1) : undefined;
	const origin = stack ? stackUtils.parseLine(stack[0]!) : undefined;
	const filePath = cleanupPath(origin?.file);
	let excerpt: CodeExcerpt[] | undefined;
	let lineWidth = 0;

	if (filePath && origin?.line && fs.existsSync(filePath)) {
		const sourceCode = fs.readFileSync(filePath, 'utf8');
		excerpt = codeExcerpt(sourceCode, origin.line);

		if (excerpt) {
			for (const {line} of excerpt) {
				lineWidth = Math.max(lineWidth, String(line).length);
			}
		}
	}

	return (
		<Box flexDirection="column" padding={1}>
			<Box>
				<Text backgroundColor="red" color="white">
					{' '}
					ERROR{' '}
				</Text>

				<Text> {error.message}</Text>
			</Box>

			{origin && filePath ? (
				<Box marginTop={1}>
					<Text dimColor>
						{filePath}:{origin.line}:{origin.column}
					</Text>
				</Box>
			) : null}

			{origin && excerpt ? (
				<Box marginTop={1} flexDirection="column">
					{excerpt.map(({line, value}) => (
						<Box key={line}>
							<Box width={lineWidth + 1}>
								<Text
									dimColor={line !== origin.line}
									backgroundColor={line === origin.line ? 'red' : undefined}
									color={line === origin.line ? 'white' : undefined}
									aria-label={
										line === origin.line
											? `Line ${line}, error`
											: `Line ${line}`
									}
								>
									{String(line).padStart(lineWidth, ' ')}:
								</Text>
							</Box>

							<Text
								key={line}
								backgroundColor={line === origin.line ? 'red' : undefined}
								color={line === origin.line ? 'white' : undefined}
							>
								{' ' + value}
							</Text>
						</Box>
					))}
				</Box>
			) : null}

			{error.stack ? (
				<Box marginTop={1} flexDirection="column">
					{error.stack
						.split('\n')
						.slice(1)
						.map(line => {
							const parsedLine = stackUtils.parseLine(line);

							// If the line from the stack cannot be parsed, we print out the unparsed line.
							if (!parsedLine) {
								return (
									<Box key={line}>
										<Text dimColor>- </Text>
										<Text dimColor bold>
											{line}
											\t{' '}
										</Text>
									</Box>
								);
							}

							return (
								<Box key={line}>
									<Text dimColor>- </Text>
									<Text dimColor bold>
										{parsedLine.function}
									</Text>
									<Text
										dimColor
										color="gray"
										aria-label={`at ${
											cleanupPath(parsedLine.file) ?? ''
										} line ${parsedLine.line} column ${parsedLine.column}`}
									>
										{' '}
										({cleanupPath(parsedLine.file) ?? ''}:{parsedLine.line}:
										{parsedLine.column})
									</Text>
								</Box>
							);
						})}
				</Box>
			) : null}
		</Box>
	);
}


================================================
FILE: src/components/FocusContext.ts
================================================
import {createContext} from 'react';

export type Props = {
	readonly activeId?: string;
	readonly add: (id: string, options: {autoFocus: boolean}) => void;
	readonly remove: (id: string) => void;
	readonly activate: (id: string) => void;
	readonly deactivate: (id: string) => void;
	readonly enableFocus: () => void;
	readonly disableFocus: () => void;
	readonly focusNext: () => void;
	readonly focusPrevious: () => void;
	readonly focus: (id: string) => void;
};

// eslint-disable-next-line @typescript-eslint/naming-convention
const FocusContext = createContext<Props>({
	activeId: undefined,
	add() {},
	remove() {},
	activate() {},
	deactivate() {},
	enableFocus() {},
	disableFocus() {},
	focusNext() {},
	focusPrevious() {},
	focus() {},
});

FocusContext.displayName = 'InternalFocusContext';

export default FocusContext;


================================================
FILE: src/components/Newline.tsx
================================================
import React from 'react';

export type Props = {
	/**
	Number of newlines to insert.

	@default 1
	*/
	readonly count?: number;
};

/**
Adds one or more newline (`\n`) characters. Must be used within `<Text>` components.
*/
export default function Newline({count = 1}: Props) {
	return <ink-text>{'\n'.repeat(count)}</ink-text>;
}


================================================
FILE: src/components/Spacer.tsx
================================================
import React from 'react';
import Box from './Box.js';

/**
A flexible space that expands along the major axis of its containing layout.

It's useful as a shortcut for filling all the available space between elements.
*/
export default function Spacer() {
	return <Box flexGrow={1} />;
}


================================================
FILE: src/components/Static.tsx
================================================
import React, {useMemo, useState, useLayoutEffect, type ReactNode} from 'react';
import {type Styles} from '../styles.js';

export type Props<T> = {
	/**
	Array of items of any type to render using the function you pass as a component child.
	*/
	readonly items: T[];

	/**
	Styles to apply to a container of child elements. See <Box> for supported properties.
	*/
	readonly style?: Styles;

	/**
	Function that is called to render every item in the `items` array. The first argument is the item itself, and the second argument is the index of that item in the `items` array. Note that a `key` must be assigned to the root component.
	*/
	readonly children: (item: T, index: number) => ReactNode;
};

/**
`<Static>` component permanently renders its output above everything else. It's useful for displaying activity like completed tasks or logs—things that don't change after they're rendered (hence the name "Static").

It's preferred to use `<Static>` for use cases like these when you can't know or control the number of items that need to be rendered.

For example, [Tap](https://github.com/tapjs/node-tap) uses `<Static>` to display a list of completed tests. [Gatsby](https://github.com/gatsbyjs/gatsby) uses it to display a list of generated pages while still displaying a live progress bar.
*/
export default function Static<T>(props: Props<T>) {
	const {items, children: render, style: customStyle} = props;
	const [index, setIndex] = useState(0);

	const itemsToRender: T[] = useMemo(() => {
		return items.slice(index);
	}, [items, index]);

	useLayoutEffect(() => {
		setIndex(items.length);
	}, [items.length]);

	const children = itemsToRender.map((item, itemIndex): ReactNode => {
		return render(item, index + itemIndex);
	});

	const style: Styles = useMemo(
		() => ({
			position: 'absolute',
			flexDirection: 'column',
			...customStyle,
		}),
		[customStyle],
	);

	return (
		<ink-box internal_static style={style}>
			{children}
		</ink-box>
	);
}


================================================
FILE: src/components/StderrContext.ts
================================================
import process from 'node:process';
import {createContext} from 'react';

export type Props = {
	/**
	Stderr stream passed to `render()` in `options.stderr` or `process.stderr` by default.
	*/
	readonly stderr: NodeJS.WriteStream;

	/**
	Write any string to stderr while preserving Ink's output. It's useful when you want to display external information outside of Ink's rendering and ensure there's no conflict between the two. It's similar to `<Static>`, except it can't accept components; it only works with strings.
	*/
	readonly write: (data: string) => void;
};

/**
`StderrContext` is a React context that exposes the stderr stream.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
const StderrContext = createContext<Props>({
	stderr: process.stderr,
	write() {},
});

StderrContext.displayName = 'InternalStderrContext';

export default StderrContext;


================================================
FILE: src/components/StdinContext.ts
================================================
import {EventEmitter} from 'node:events';
import process from 'node:process';
import {createContext} from 'react';

export type PublicProps = {
	/**
	The stdin stream passed to `render()` in `options.stdin`, or `process.stdin` by default. Useful if your app needs to handle user input.
	*/
	readonly stdin: NodeJS.ReadStream;

	/**
	Ink exposes this function via own `<StdinContext>` to be able to handle Ctrl+C, that's why you should use Ink's `setRawMode` instead of `process.stdin.setRawMode`. If the `stdin` stream passed to Ink does not support setRawMode, this function does nothing.
	*/
	readonly setRawMode: (value: boolean) => void;

	/**
	A boolean flag determining if the current `stdin` supports `setRawMode`. A component using `setRawMode` might want to use `isRawModeSupported` to nicely fall back in environments where raw mode is not supported.
	*/
	readonly isRawModeSupported: boolean;
};

export type Props = PublicProps & {
	/**
	Enable or disable bracketed paste mode on the terminal. When enabled, pasted text is wrapped in escape sequences that allow it to be distinguished from typed input.
	*/
	readonly setBracketedPasteMode: (value: boolean) => void;

	readonly internal_exitOnCtrlC: boolean;

	readonly internal_eventEmitter: EventEmitter;
};

/**
`StdinContext` is a React context that exposes the input stream.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
const StdinContext = createContext<Props>({
	stdin: process.stdin,
	// eslint-disable-next-line @typescript-eslint/naming-convention
	internal_eventEmitter: new EventEmitter(),
	setRawMode() {},
	setBracketedPasteMode() {},
	isRawModeSupported: false,
	// eslint-disable-next-line @typescript-eslint/naming-convention
	internal_exitOnCtrlC: true,
});

StdinContext.displayName = 'InternalStdinContext';

export default StdinContext;


================================================
FILE: src/components/StdoutContext.ts
================================================
import process from 'node:process';
import {createContext} from 'react';

export type Props = {
	/**
	Stdout stream passed to `render()` in `options.stdout` or `process.stdout` by default.
	*/
	readonly stdout: NodeJS.WriteStream;

	/**
	Write any string to stdout while preserving Ink's output. It's useful when you want to display external information outside of Ink's rendering and ensure there's no conflict between the two. It's similar to `<Static>`, except it can't accept components; it only works with strings.
	*/
	readonly write: (data: string) => void;
};

/**
`StdoutContext` is a React context that exposes the stdout stream where Ink renders your app.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
const StdoutContext = createContext<Props>({
	stdout: process.stdout,
	write() {},
});

StdoutContext.displayName = 'InternalStdoutContext';

export default StdoutContext;


================================================
FILE: src/components/Text.tsx
================================================
import React, {useContext, type ReactNode} from 'react';
import chalk, {type ForegroundColorName} from 'chalk';
import {type LiteralUnion} from 'type-fest';
import colorize from '../colorize.js';
import {type Styles} from '../styles.js';
import {accessibilityContext} from './AccessibilityContext.js';
import {backgroundContext} from './BackgroundContext.js';

export type Props = {
	/**
	A label for the element for screen readers.
	*/
	readonly 'aria-label'?: string;

	/**
	Hide the element from screen readers.
	*/
	readonly 'aria-hidden'?: boolean;

	/**
	Change text color. Ink uses Chalk under the hood, so all its functionality is supported.
	*/
	readonly color?: LiteralUnion<ForegroundColorName, string>;

	/**
	Same as `color`, but for the background.
	*/
	readonly backgroundColor?: LiteralUnion<ForegroundColorName, string>;

	/**
	Dim the color (make it less bright).
	*/
	readonly dimColor?: boolean;

	/**
	Make the text bold.
	*/
	readonly bold?: boolean;

	/**
	Make the text italic.
	*/
	readonly italic?: boolean;

	/**
	Make the text underlined.
	*/
	readonly underline?: boolean;

	/**
	Make the text crossed out with a line.
	*/
	readonly strikethrough?: boolean;

	/**
	Inverse background and foreground colors.
	*/
	readonly inverse?: boolean;

	/**
	This property tells Ink to wrap or truncate text if its width is larger than the container. If `wrap` is passed (the default), Ink will wrap text and split it into multiple lines. If `truncate-*` is passed, Ink will truncate text instead, resulting in one line of text with the rest cut off.
	*/
	readonly wrap?: Styles['textWrap'];

	readonly children?: ReactNode;
};

/**
This component can display text and change its style to make it bold, underlined, italic, or strikethrough.
*/
export default function Text({
	color,
	backgroundColor,
	dimColor = false,
	bold = false,
	italic = false,
	underline = false,
	strikethrough = false,
	inverse = false,
	wrap = 'wrap',
	children,
	'aria-label': ariaLabel,
	'aria-hidden': ariaHidden = false,
}: Props) {
	const {isScreenReaderEnabled} = useContext(accessibilityContext);
	const inheritedBackgroundColor = useContext(backgroundContext);
	const childrenOrAriaLabel =
		isScreenReaderEnabled && ariaLabel ? ariaLabel : children;

	if (childrenOrAriaLabel === undefined || childrenOrAriaLabel === null) {
		return null;
	}

	const transform = (children: string): string => {
		if (dimColor) {
			children = chalk.dim(children);
		}

		if (color) {
			children = colorize(children, color, 'foreground');
		}

		// Use explicit backgroundColor if provided, otherwise use inherited from parent Box
		const effectiveBackgroundColor =
			backgroundColor ?? inheritedBackgroundColor;
		if (effectiveBackgroundColor) {
			children = colorize(children, effectiveBackgroundColor, 'background');
		}

		if (bold) {
			children = chalk.bold(children);
		}

		if (italic) {
			children = chalk.italic(children);
		}

		if (underline) {
			children = chalk.underline(children);
		}

		if (strikethrough) {
			children = chalk.strikethrough(children);
		}

		if (inverse) {
			children = chalk.inverse(children);
		}

		return children;
	};

	if (isScreenReaderEnabled && ariaHidden) {
		return null;
	}

	return (
		<ink-text
			style={{flexGrow: 0, flexShrink: 1, flexDirection: 'row', textWrap: wrap}}
			internal_transform={transform}
		>
			{isScreenReaderEnabled && ariaLabel ? ariaLabel : children}
		</ink-text>
	);
}


================================================
FILE: src/components/Transform.tsx
================================================
import React, {useContext, type ReactNode} from 'react';
import {accessibilityContext} from './AccessibilityContext.js';

export type Props = {
	/**
	Screen-reader-specific text to output. If this is set, all children will be ignored.
	*/
	readonly accessibilityLabel?: string;

	/**
	Function that transforms children output. It accepts children and must return transformed children as well. Note that when children use `<Text>` styling props (e.g. `color`, `bold`), the string will contain ANSI escape codes.
	*/
	readonly transform: (children: string, index: number) => string;

	readonly children?: ReactNode;
};

/**
Transform a string representation of React components before they're written to output. For example, you might want to apply a gradient to text, add a clickable link, or create some text effects. These use cases can't accept React nodes as input; they expect a string. That's what the <Transform> component does: it gives you an output string of its child components and lets you transform it in any way.
*/
export default function Transform({
	children,
	transform,
	accessibilityLabel,
}: Props) {
	const {isScreenReaderEnabled} = useContext(accessibilityContext);

	if (children === undefined || children === null) {
		return null;
	}

	return (
		<ink-text
			style={{flexGrow: 0, flexShrink: 1, flexDirection: 'row'}}
			internal_transform={transform}
		>
			{isScreenReaderEnabled && accessibilityLabel
				? accessibilityLabel
				: children}
		</ink-text>
	);
}


================================================
FILE: src/cursor-helpers.ts
================================================
import ansiEscapes from 'ansi-escapes';

export type CursorPosition = {
	x: number;
	y: number;
};

const showCursorEscape = '\u001B[?25h';
const hideCursorEscape = '\u001B[?25l';

export {showCursorEscape, hideCursorEscape};

/**
Compare two cursor positions. Returns true if they differ.
*/
export const cursorPositionChanged = (
	a: CursorPosition | undefined,
	b: CursorPosition | undefined,
): boolean => a?.x !== b?.x || a?.y !== b?.y;

/**
Build escape sequence to move cursor from bottom of output to the target position and show it.
Assumes cursor is at (col 0, line visibleLineCount) — i.e. just after the last output line.
*/
export const buildCursorSuffix = (
	visibleLineCount: number,
	cursorPosition: CursorPosition | undefined,
): string => {
	if (!cursorPosition) {
		return '';
	}

	const moveUp = visibleLineCount - cursorPosition.y;
	return (
		(moveUp > 0 ? ansiEscapes.cursorUp(moveUp) : '') +
		ansiEscapes.cursorTo(cursorPosition.x) +
		showCursorEscape
	);
};

/**
Build escape sequence to move cursor from previousCursorPosition back to the bottom of output.
This must be done before eraseLines or any operation that assumes cursor is at the bottom.
*/
export const buildReturnToBottom = (
	previousLineCount: number,
	previousCursorPosition: CursorPosition | undefined,
): string => {
	if (!previousCursorPosition) {
		return '';
	}

	// PreviousLineCount includes trailing newline, so visible lines = previousLineCount - 1
	// cursor is at previousCursorPosition.y, need to go to line (previousLineCount - 1)
	const down = previousLineCount - 1 - previousCursorPosition.y;
	return (
		(down > 0 ? ansiEscapes.cursorDown(down) : '') + ansiEscapes.cursorTo(0)
	);
};

export type CursorOnlyInput = {
	cursorWasShown: boolean;
	previousLineCount: number;
	previousCursorPosition: CursorPosition | undefined;
	visibleLineCount: number;
	cursorPosition: CursorPosition | undefined;
};

/**
Build the escape sequence for cursor-only updates (output unchanged, cursor moved).
Hides cursor if it was previously shown, returns to bottom, then repositions.
*/
export const buildCursorOnlySequence = (input: CursorOnlyInput): string => {
	const hidePrefix = input.cursorWasShown ? hideCursorEscape : '';
	const returnToBottom = buildReturnToBottom(
		input.previousLineCount,
		input.previousCursorPosition,
	);
	const cursorSuffix = buildCursorSuffix(
		input.visibleLineCount,
		input.cursorPosition,
	);
	return hidePrefix + returnToBottom + cursorSuffix;
};

/**
Build the prefix that hides cursor and returns to bottom before erasing or rewriting.
Returns empty string if cursor was not shown.
*/
export const buildReturnToBottomPrefix = (
	cursorWasShown: boolean,
	previousLineCount: number,
	previousCursorPosition: CursorPosition | undefined,
): string => {
	if (!cursorWasShown) {
		return '';
	}

	return (
		hideCursorEscape +
		buildReturnToBottom(previousLineCount, previousCursorPosition)
	);
};


================================================
FILE: src/devtools-window-polyfill.ts
================================================
// Ignoring missing types error to avoid adding another dependency for this hack to work
import ws from 'ws';

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const customGlobal = globalThis as any;

// These things must exist before importing `react-devtools-core`
// Using ||= intentionally to set falsy values, not just null/undefined

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
customGlobal.WebSocket ||= ws;

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
customGlobal.window ||= globalThis;

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
customGlobal.self ||= globalThis;

// Filter out Ink's internal components from devtools for a cleaner view.
// Also, ince `react-devtools-shared` package isn't published on npm, we can't
// use its types, that's why there are hard-coded values in `type` fields below.
// See https://github.com/facebook/react/blob/edf6eac8a181860fd8a2d076a43806f1237495a1/packages/react-devtools-shared/src/types.js#L24
customGlobal.window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = [
	{
		// ComponentFilterElementType
		type: 1,
		// ElementTypeHostComponent
		value: 7,
		isEnabled: true,
	},
	{
		// ComponentFilterDisplayName
		type: 2,
		value: 'InternalApp',
		isEnabled: true,
		isValid: true,
	},
	{
		// ComponentFilterDisplayName
		type: 2,
		value: 'InternalAppContext',
		isEnabled: true,
		isValid: true,
	},
	{
		// ComponentFilterDisplayName
		type: 2,
		value: 'InternalStdoutContext',
		isEnabled: true,
		isValid: true,
	},
	{
		// ComponentFilterDisplayName
		type: 2,
		value: 'InternalStderrContext',
		isEnabled: true,
		isValid: true,
	},
	{
		// ComponentFilterDisplayName
		type: 2,
		value: 'InternalStdinContext',
		isEnabled: true,
		isValid: true,
	},
	{
		// ComponentFilterDisplayName
		type: 2,
		value: 'InternalFocusContext',
		isEnabled: true,
		isValid: true,
	},
];


================================================
FILE: src/devtools.ts
================================================
/* eslint-disable import-x/order */

// eslint-disable-next-line import-x/no-unassigned-import
import './devtools-window-polyfill.js';

import WebSocket from 'ws';

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import devtools from 'react-devtools-core';

const isDevToolsReachable = async (): Promise<boolean> =>
	new Promise(resolve => {
		const socket = new WebSocket('ws://localhost:8097');

		const timeout = setTimeout(() => {
			socket.terminate();
			resolve(false);
		}, 2000);
		// Don't let the timeout keep the process alive on its own
		timeout.unref();

		socket.on('open', () => {
			clearTimeout(timeout);
			socket.terminate();
			resolve(true);
		});

		socket.on('error', () => {
			clearTimeout(timeout);
			socket.terminate();
			resolve(false);
		});
	});

if (await isDevToolsReachable()) {
	// eslint-disable-next-line @typescript-eslint/no-unsafe-call
	(devtools as any).initialize();
	// eslint-disable-next-line @typescript-eslint/no-unsafe-call
	(devtools as any).connectToDevTools();
} else {
	console.warn(
		'DEV is set to true, but the React DevTools server is not running. Start it with:\n\n$ npx react-devtools\n',
	);
}


================================================
FILE: src/dom.ts
================================================
import Yoga, {type Node as YogaNode} from 'yoga-layout';
import measureText from './measure-text.js';
import {type Styles} from './styles.js';
import wrapText from './wrap-text.js';
import squashTextNodes from './squash-text-nodes.js';
import {type OutputTransformer} from './render-node-to-output.js';

type InkNode = {
	parentNode: DOMElement | undefined;
	yogaNode?: YogaNode;
	internal_static?: boolean;
	style: Styles;
};

type LayoutListener = () => void;

export type TextName = '#text';
export type ElementNames =
	| 'ink-root'
	| 'ink-box'
	| 'ink-text'
	| 'ink-virtual-text';

export type NodeNames = ElementNames | TextName;

// eslint-disable-next-line @typescript-eslint/naming-convention
export type DOMElement = {
	nodeName: ElementNames;
	attributes: Record<string, DOMNodeAttribute>;
	childNodes: DOMNode[];
	internal_transform?: OutputTransformer;

	internal_accessibility?: {
		role?:
			| 'button'
			| 'checkbox'
			| 'combobox'
			| 'list'
			| 'listbox'
			| 'listitem'
			| 'menu'
			| 'menuitem'
			| 'option'
			| 'progressbar'
			| 'radio'
			| 'radiogroup'
			| 'tab'
			| 'tablist'
			| 'table'
			| 'textbox'
			| 'timer'
			| 'toolbar';
		state?: {
			busy?: boolean;
			checked?: boolean;
			disabled?: boolean;
			expanded?: boolean;
			multiline?: boolean;
			multiselectable?: boolean;
			readonly?: boolean;
			required?: boolean;
			selected?: boolean;
		};
	};

	// Internal properties
	isStaticDirty?: boolean;
	staticNode?: DOMElement;
	onComputeLayout?: () => void;
	onRender?: () => void;
	onImmediateRender?: () => void;
	internal_layoutListeners?: Set<LayoutListener>;
} & InkNode;

export type TextNode = {
	nodeName: TextName;
	nodeValue: string;
} & InkNode;

// eslint-disable-next-line @typescript-eslint/naming-convention
export type DOMNode<T = {nodeName: NodeNames}> = T extends {
	nodeName: infer U;
}
	? U extends '#text'
		? TextNode
		: DOMElement
	: never;

// eslint-disable-next-line @typescript-eslint/naming-convention
export type DOMNodeAttribute = boolean | string | number;

export const createNode = (nodeName: ElementNames): DOMElement => {
	const node: DOMElement = {
		nodeName,
		style: {},
		attributes: {},
		childNodes: [],
		parentNode: undefined,
		yogaNode: nodeName === 'ink-virtual-text' ? undefined : Yoga.Node.create(),
		// eslint-disable-next-line @typescript-eslint/naming-convention
		internal_accessibility: {},
	};

	if (nodeName === 'ink-text') {
		node.yogaNode?.setMeasureFunc(measureTextNode.bind(null, node));
	}

	return node;
};

export const appendChildNode = (
	node: DOMElement,
	childNode: DOMElement,
): void => {
	if (childNode.parentNode) {
		removeChildNode(childNode.parentNode, childNode);
	}

	childNode.parentNode = node;
	node.childNodes.push(childNode);

	if (childNode.yogaNode) {
		node.yogaNode?.insertChild(
			childNode.yogaNode,
			node.yogaNode.getChildCount(),
		);
	}

	if (node.nodeName === 'ink-text' || node.nodeName === 'ink-virtual-text'
Download .txt
gitextract_jq_9codb/

├── .editorconfig
├── .gitattributes
├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .npmrc
├── benchmark/
│   ├── simple/
│   │   ├── index.ts
│   │   └── simple.tsx
│   └── static/
│       ├── index.ts
│       └── static.tsx
├── examples/
│   ├── alternate-screen/
│   │   ├── alternate-screen.tsx
│   │   └── index.ts
│   ├── aria/
│   │   ├── aria.tsx
│   │   └── index.ts
│   ├── borders/
│   │   ├── borders.tsx
│   │   └── index.ts
│   ├── box-backgrounds/
│   │   ├── box-backgrounds.tsx
│   │   └── index.ts
│   ├── chat/
│   │   ├── chat.tsx
│   │   └── index.ts
│   ├── concurrent-suspense/
│   │   ├── concurrent-suspense.tsx
│   │   └── index.ts
│   ├── counter/
│   │   ├── counter.tsx
│   │   └── index.ts
│   ├── cursor-ime/
│   │   ├── cursor-ime.tsx
│   │   └── index.ts
│   ├── incremental-rendering/
│   │   ├── incremental-rendering.tsx
│   │   └── index.ts
│   ├── jest/
│   │   ├── index.ts
│   │   ├── jest.tsx
│   │   ├── summary.tsx
│   │   └── test.tsx
│   ├── justify-content/
│   │   ├── index.ts
│   │   └── justify-content.tsx
│   ├── render-throttle/
│   │   └── index.tsx
│   ├── router/
│   │   ├── index.ts
│   │   └── router.tsx
│   ├── select-input/
│   │   ├── index.ts
│   │   └── select-input.tsx
│   ├── static/
│   │   ├── index.ts
│   │   └── static.tsx
│   ├── subprocess-output/
│   │   ├── index.ts
│   │   └── subprocess-output.tsx
│   ├── suspense/
│   │   ├── index.ts
│   │   └── suspense.tsx
│   ├── table/
│   │   ├── index.ts
│   │   └── table.tsx
│   ├── terminal-resize/
│   │   ├── index.ts
│   │   └── terminal-resize.tsx
│   ├── use-focus/
│   │   ├── index.ts
│   │   └── use-focus.tsx
│   ├── use-focus-with-id/
│   │   ├── index.ts
│   │   └── use-focus-with-id.tsx
│   ├── use-input/
│   │   ├── index.ts
│   │   └── use-input.tsx
│   ├── use-stderr/
│   │   ├── index.ts
│   │   └── use-stderr.tsx
│   ├── use-stdout/
│   │   ├── index.ts
│   │   └── use-stdout.tsx
│   └── use-transition/
│       ├── index.ts
│       └── use-transition.tsx
├── license
├── media/
│   └── demo.js
├── package.json
├── readme.md
├── recipes/
│   └── routing.md
├── src/
│   ├── ansi-tokenizer.ts
│   ├── colorize.ts
│   ├── components/
│   │   ├── AccessibilityContext.ts
│   │   ├── App.tsx
│   │   ├── AppContext.ts
│   │   ├── BackgroundContext.ts
│   │   ├── Box.tsx
│   │   ├── CursorContext.ts
│   │   ├── ErrorBoundary.tsx
│   │   ├── ErrorOverview.tsx
│   │   ├── FocusContext.ts
│   │   ├── Newline.tsx
│   │   ├── Spacer.tsx
│   │   ├── Static.tsx
│   │   ├── StderrContext.ts
│   │   ├── StdinContext.ts
│   │   ├── StdoutContext.ts
│   │   ├── Text.tsx
│   │   └── Transform.tsx
│   ├── cursor-helpers.ts
│   ├── devtools-window-polyfill.ts
│   ├── devtools.ts
│   ├── dom.ts
│   ├── get-max-width.ts
│   ├── global.d.ts
│   ├── hooks/
│   │   ├── use-app.ts
│   │   ├── use-box-metrics.ts
│   │   ├── use-cursor.ts
│   │   ├── use-focus-manager.ts
│   │   ├── use-focus.ts
│   │   ├── use-input.ts
│   │   ├── use-is-screen-reader-enabled.ts
│   │   ├── use-paste.ts
│   │   ├── use-stderr.ts
│   │   ├── use-stdin.ts
│   │   ├── use-stdout.ts
│   │   └── use-window-size.ts
│   ├── index.ts
│   ├── ink.tsx
│   ├── input-parser.ts
│   ├── instances.ts
│   ├── kitty-keyboard.ts
│   ├── log-update.ts
│   ├── measure-element.ts
│   ├── measure-text.ts
│   ├── output.ts
│   ├── parse-keypress.ts
│   ├── reconciler.ts
│   ├── render-background.ts
│   ├── render-border.ts
│   ├── render-node-to-output.ts
│   ├── render-to-string.ts
│   ├── render.ts
│   ├── renderer.ts
│   ├── sanitize-ansi.ts
│   ├── squash-text-nodes.ts
│   ├── styles.ts
│   ├── utils.ts
│   ├── wrap-text.ts
│   └── write-synchronized.ts
├── test/
│   ├── alternate-screen-example.tsx
│   ├── ansi-tokenizer.ts
│   ├── background.tsx
│   ├── borders.tsx
│   ├── components.tsx
│   ├── cursor-helpers.tsx
│   ├── cursor.tsx
│   ├── display.tsx
│   ├── errors.tsx
│   ├── exit.tsx
│   ├── fixtures/
│   │   ├── alternate-screen-full-board-win.tsx
│   │   ├── ci-debug-after-exit.tsx
│   │   ├── ci-debug.tsx
│   │   ├── ci.tsx
│   │   ├── clear.tsx
│   │   ├── console.tsx
│   │   ├── erase-with-state-change.tsx
│   │   ├── erase-with-static.tsx
│   │   ├── erase.tsx
│   │   ├── exit-double-raw-mode.tsx
│   │   ├── exit-normally.tsx
│   │   ├── exit-on-exit-with-error-value-property.tsx
│   │   ├── exit-on-exit-with-error.tsx
│   │   ├── exit-on-exit-with-result.tsx
│   │   ├── exit-on-exit-with-value-object.tsx
│   │   ├── exit-on-exit.tsx
│   │   ├── exit-on-finish.tsx
│   │   ├── exit-on-unmount.tsx
│   │   ├── exit-raw-on-exit-with-error.tsx
│   │   ├── exit-raw-on-exit.tsx
│   │   ├── exit-raw-on-unmount.tsx
│   │   ├── exit-with-static.tsx
│   │   ├── exit-with-thrown-error.tsx
│   │   ├── fullscreen-no-extra-newline.tsx
│   │   ├── issue-442-full-height.tsx
│   │   ├── issue-450-fixture-helpers.tsx
│   │   ├── issue-450-full-height-rerender-with-marker.tsx
│   │   ├── issue-450-full-height-rerender.tsx
│   │   ├── issue-450-full-height-with-static-rerender.tsx
│   │   ├── issue-450-grow-to-fullscreen-rerender.tsx
│   │   ├── issue-450-grow-to-overflow-rerender.tsx
│   │   ├── issue-450-height-minus-one-rerender.tsx
│   │   ├── issue-450-initial-fullscreen.tsx
│   │   ├── issue-450-initial-overflow.tsx
│   │   ├── issue-450-shrink-from-fullscreen-rerender.tsx
│   │   ├── issue-450-shrink-from-overflow-rerender.tsx
│   │   ├── issue-450-static-shrink-from-fullscreen-rerender.tsx
│   │   ├── issue-725-child-process.tsx
│   │   ├── use-input-ctrl-c.tsx
│   │   ├── use-input-discrete-priority.tsx
│   │   ├── use-input-kitty.tsx
│   │   ├── use-input-many.tsx
│   │   ├── use-input-multiple.tsx
│   │   ├── use-input.tsx
│   │   ├── use-paste.tsx
│   │   └── use-stdout.tsx
│   ├── flex-align-content.tsx
│   ├── flex-align-items.tsx
│   ├── flex-align-self.tsx
│   ├── flex-direction.tsx
│   ├── flex-justify-content.tsx
│   ├── flex-wrap.tsx
│   ├── flex.tsx
│   ├── focus.tsx
│   ├── gap.tsx
│   ├── helpers/
│   │   ├── create-stdin.ts
│   │   ├── create-stdout.ts
│   │   ├── force-colors.ts
│   │   ├── render-to-string.ts
│   │   ├── run.ts
│   │   ├── term.ts
│   │   └── test-renderer.ts
│   ├── hooks-use-input-kitty.tsx
│   ├── hooks-use-input-navigation.tsx
│   ├── hooks-use-input.tsx
│   ├── hooks-use-paste.tsx
│   ├── hooks.tsx
│   ├── input-parser.ts
│   ├── kitty-keyboard.tsx
│   ├── log-update.tsx
│   ├── margin.tsx
│   ├── measure-element.tsx
│   ├── measure-text.tsx
│   ├── overflow.tsx
│   ├── padding.tsx
│   ├── position.tsx
│   ├── reconciler.tsx
│   ├── render-to-string.tsx
│   ├── render.tsx
│   ├── sanitize-ansi.ts
│   ├── screen-reader.tsx
│   ├── terminal-resize.tsx
│   ├── text-width.tsx
│   ├── text.tsx
│   ├── tsconfig.json
│   ├── use-box-metrics.tsx
│   ├── width-height.tsx
│   └── write-synchronized.tsx
├── tsconfig.json
└── xo.config.ts
Download .txt
SYMBOL INDEX (507 symbols across 124 files)

FILE: benchmark/simple/simple.tsx
  function App (line 4) | function App() {

FILE: benchmark/static/static.tsx
  function App (line 4) | function App() {

FILE: examples/alternate-screen/alternate-screen.tsx
  type Point (line 11) | type Point = {
  type Direction (line 16) | type Direction = 'up' | 'down' | 'left' | 'right';
  type GameState (line 18) | type GameState = {
  type Action (line 27) | type Action = {type: 'tick'; direction: Direction} | {type: 'restart'};
  function randomPosition (line 73) | function randomPosition(exclude: Point[]): Point {
  function createInitialState (line 98) | function createInitialState(): GameState {
  function gameReducer (line 109) | function gameReducer(state: GameState, action: Action): GameState {
  function buildBoard (line 170) | function buildBoard(snake: Point[], food: Point): string {
  function SnakeGame (line 198) | function SnakeGame() {
  function runAlternateScreenExample (line 286) | function runAlternateScreenExample() {

FILE: examples/aria/aria.tsx
  function AriaExample (line 4) | function AriaExample() {

FILE: examples/borders/borders.tsx
  function Borders (line 4) | function Borders() {

FILE: examples/box-backgrounds/box-backgrounds.tsx
  function BoxBackgrounds (line 4) | function BoxBackgrounds() {

FILE: examples/chat/chat.tsx
  function ChatApp (line 6) | function ChatApp() {

FILE: examples/concurrent-suspense/concurrent-suspense.tsx
  function fetchData (line 10) | function fetchData(key: string, delay: number): string {
  function DataItem (line 39) | function DataItem({
  function Loading (line 55) | function Loading({message}: {readonly message: string}) {
  function App (line 64) | function App() {

FILE: examples/counter/counter.tsx
  function Counter (line 4) | function Counter() {

FILE: examples/cursor-ime/cursor-ime.tsx
  function App (line 5) | function App() {

FILE: examples/incremental-rendering/incremental-rendering.tsx
  function IncrementalRendering (line 58) | function IncrementalRendering() {

FILE: examples/jest/jest.tsx
  type State (line 22) | type State = {
  class Jest (line 34) | class Jest extends React.Component<Record<string, unknown>, State> {
    method constructor (line 35) | constructor(properties: Record<string, unknown>) {
    method render (line 45) | render() {
    method componentDidMount (line 74) | componentDidMount() {
    method runTest (line 82) | async runTest(path: string) {

FILE: examples/jest/summary.tsx
  type Properties (line 4) | type Properties = {
  function Summary (line 11) | function Summary({isFinished, passed, failed, time}: Properties) {

FILE: examples/jest/test.tsx
  type Properties (line 24) | type Properties = {
  function Test (line 29) | function Test({status, path}: Properties) {

FILE: examples/justify-content/justify-content.tsx
  function JustifyContent (line 4) | function JustifyContent() {

FILE: examples/render-throttle/index.tsx
  function App (line 4) | function App() {

FILE: examples/router/router.tsx
  function Home (line 5) | function Home() {
  function About (line 29) | function About() {
  function App (line 53) | function App() {

FILE: examples/select-input/select-input.tsx
  function SelectInput (line 12) | function SelectInput() {

FILE: examples/static/static.tsx
  function Example (line 4) | function Example() {

FILE: examples/subprocess-output/subprocess-output.tsx
  function SubprocessOutput (line 7) | function SubprocessOutput() {

FILE: examples/suspense/suspense.tsx
  function Example (line 33) | function Example() {
  function Fallback (line 38) | function Fallback() {

FILE: examples/table/table.tsx
  function Table (line 13) | function Table() {

FILE: examples/terminal-resize/terminal-resize.tsx
  function TerminalResizeExample (line 4) | function TerminalResizeExample() {

FILE: examples/use-focus-with-id/use-focus-with-id.tsx
  function Focus (line 11) | function Focus() {
  type ItemProperties (line 43) | type ItemProperties = {
  function Item (line 48) | function Item({label, id}: ItemProperties) {

FILE: examples/use-focus/use-focus.tsx
  function Focus (line 4) | function Focus() {
  function Item (line 20) | function Item({label}) {

FILE: examples/use-input/use-input.tsx
  function Robot (line 4) | function Robot() {

FILE: examples/use-stderr/use-stderr.tsx
  function Example (line 4) | function Example() {

FILE: examples/use-stdout/use-stdout.tsx
  function Example (line 4) | function Example() {

FILE: examples/use-transition/use-transition.tsx
  function generateItems (line 5) | function generateItems(filter: string): string[] {
  function SearchApp (line 28) | function SearchApp() {

FILE: media/demo.js
  class Counter (line 5) | class Counter extends React.PureComponent {
    method constructor (line 6) | constructor() {
    method render (line 14) | render() {
    method componentDidMount (line 38) | componentDidMount() {
    method componentWillUnmount (line 50) | componentWillUnmount() {

FILE: src/ansi-tokenizer.ts
  type ControlStringType (line 11) | type ControlStringType = 'osc' | 'dcs' | 'pm' | 'apc' | 'sos';
  type CsiToken (line 13) | type CsiToken = {
  type EscToken (line 21) | type EscToken = {
  type ControlStringToken (line 28) | type ControlStringToken = {
  type TextToken (line 33) | type TextToken = {
  type StToken (line 38) | type StToken = {
  type C1Token (line 43) | type C1Token = {
  type InvalidToken (line 48) | type InvalidToken = {
  type AnsiToken (line 53) | type AnsiToken =

FILE: src/colorize.ts
  type ColorType (line 3) | type ColorType = 'foreground' | 'background';

FILE: src/components/App.tsx
  type Props (line 26) | type Props = {
  type Focusable (line 40) | type Focusable = {
  function App (line 48) | function App({

FILE: src/components/AppContext.ts
  type Props (line 3) | type Props = {
  method exit (line 43) | exit() {}
  method waitUntilRenderFlush (line 44) | async waitUntilRenderFlush() {}

FILE: src/components/BackgroundContext.ts
  type BackgroundColor (line 5) | type BackgroundColor = LiteralUnion<ForegroundColorName, string>;

FILE: src/components/Box.tsx
  type Props (line 8) | type Props = Except<Styles, 'textWrap'> & {

FILE: src/components/CursorContext.ts
  type Props (line 4) | type Props = {
  method setCursorPosition (line 15) | setCursorPosition() {}

FILE: src/components/ErrorBoundary.tsx
  type Props (line 4) | type Props = {
  type State (line 9) | type State = {
  class ErrorBoundary (line 15) | class ErrorBoundary extends PureComponent<Props, State> {
    method getDerivedStateFromError (line 18) | static getDerivedStateFromError(error: Error) {
    method componentDidCatch (line 26) | override componentDidCatch(error: Error): void {
    method render (line 30) | override render(): ReactNode {

FILE: src/components/ErrorOverview.tsx
  type Props (line 20) | type Props = {
  function ErrorOverview (line 24) | function ErrorOverview({error}: Props) {

FILE: src/components/FocusContext.ts
  type Props (line 3) | type Props = {
  method add (line 19) | add() {}
  method remove (line 20) | remove() {}
  method activate (line 21) | activate() {}
  method deactivate (line 22) | deactivate() {}
  method enableFocus (line 23) | enableFocus() {}
  method disableFocus (line 24) | disableFocus() {}
  method focusNext (line 25) | focusNext() {}
  method focusPrevious (line 26) | focusPrevious() {}
  method focus (line 27) | focus() {}

FILE: src/components/Newline.tsx
  type Props (line 3) | type Props = {
  function Newline (line 15) | function Newline({count = 1}: Props) {

FILE: src/components/Spacer.tsx
  function Spacer (line 9) | function Spacer() {

FILE: src/components/Static.tsx
  type Props (line 4) | type Props<T> = {
  function Static (line 28) | function Static<T>(props: Props<T>) {

FILE: src/components/StderrContext.ts
  type Props (line 4) | type Props = {
  method write (line 22) | write() {}

FILE: src/components/StdinContext.ts
  type PublicProps (line 5) | type PublicProps = {
  type Props (line 22) | type Props = PublicProps & {
  method setRawMode (line 41) | setRawMode() {}
  method setBracketedPasteMode (line 42) | setBracketedPasteMode() {}

FILE: src/components/StdoutContext.ts
  type Props (line 4) | type Props = {
  method write (line 22) | write() {}

FILE: src/components/Text.tsx
  type Props (line 9) | type Props = {
  function Text (line 71) | function Text({

FILE: src/components/Transform.tsx
  type Props (line 4) | type Props = {
  function Transform (line 21) | function Transform({

FILE: src/cursor-helpers.ts
  type CursorPosition (line 3) | type CursorPosition = {
  type CursorOnlyInput (line 61) | type CursorOnlyInput = {

FILE: src/dom.ts
  type InkNode (line 8) | type InkNode = {
  type LayoutListener (line 15) | type LayoutListener = () => void;
  type TextName (line 17) | type TextName = '#text';
  type ElementNames (line 18) | type ElementNames =
  type NodeNames (line 24) | type NodeNames = ElementNames | TextName;
  type DOMElement (line 27) | type DOMElement = {
  type TextNode (line 75) | type TextNode = {
  type DOMNode (line 81) | type DOMNode<T = {nodeName: NodeNames}> = T extends {
  type DOMNodeAttribute (line 90) | type DOMNodeAttribute = boolean | string | number;

FILE: src/global.d.ts
  type IntrinsicElements (line 9) | interface IntrinsicElements {
  type Box (line 17) | type Box = {
  type Text (line 26) | type Text = {

FILE: src/hooks/use-box-metrics.ts
  type BoxMetrics (line 11) | type BoxMetrics = {
  type UseBoxMetricsResult (line 33) | type UseBoxMetricsResult = BoxMetrics & {

FILE: src/hooks/use-focus-manager.ts
  type Output (line 4) | type Output = {

FILE: src/hooks/use-focus.ts
  type Input (line 5) | type Input = {
  type Output (line 22) | type Output = {

FILE: src/hooks/use-input.ts
  type Key (line 9) | type Key = {
  type Handler (line 126) | type Handler = (input: string, key: Key) => void;
  type Options (line 128) | type Options = {

FILE: src/hooks/use-paste.ts
  type Options (line 5) | type Options = {

FILE: src/hooks/use-window-size.ts
  type WindowSize (line 8) | type WindowSize = {

FILE: src/ink.tsx
  type KittyQueryResponseMatch (line 44) | type KittyQueryResponseMatch =
  type MaybeWritableStream (line 161) | type MaybeWritableStream = NodeJS.WriteStream & {
  type RenderMetrics (line 207) | type RenderMetrics = {
  type Options (line 214) | type Options = {
  class Ink (line 278) | class Ink {
    method constructor (line 318) | constructor(options: Options) {
    method render (line 627) | render(node: ReactNode): void {
    method writeToStdout (line 659) | writeToStdout(data: string): void {
    method writeToStderr (line 688) | writeToStderr(data: string): void {
    method unmount (line 719) | unmount(error?: Error | number | null): void {
    method waitUntilExit (line 862) | async waitUntilExit(): Promise<unknown> {
    method waitUntilRenderFlush (line 874) | async waitUntilRenderFlush(): Promise<void> {
    method clear (line 922) | clear(): void {
    method patchConsole (line 931) | patchConsole(): void {
    method setAlternateScreen (line 951) | private setAlternateScreen(enabled: boolean): void {
    method resolveInteractiveOption (line 966) | private resolveInteractiveOption(interactive: boolean | undefined): bo...
    method resolveAlternateScreenOption (line 970) | private resolveAlternateScreenOption(
    method shouldSync (line 981) | private shouldSync(): boolean {
    method writeBestEffort (line 986) | private writeBestEffort(stream: NodeJS.WriteStream, data: string): void {
    method awaitExit (line 994) | private async awaitExit(): Promise<void> {
    method hasPendingConcurrentWork (line 1000) | private hasPendingConcurrentWork(): boolean {
    method awaitNextRender (line 1012) | private async awaitNextRender(): Promise<void> {
    method renderInteractiveFrame (line 1024) | private renderInteractiveFrame(
    method initKittyKeyboard (line 1091) | private initKittyKeyboard(): void {
    method confirmKittySupport (line 1132) | private confirmKittySupport(flags: KittyFlagName[]): void {
    method enableKittyProtocol (line 1176) | private enableKittyProtocol(flags: KittyFlagName[]): void {

FILE: src/input-parser.ts
  type InputEvent (line 5) | type InputEvent = string | {readonly paste: string};
  type ParsedInput (line 7) | type ParsedInput = {
  type ParsedSequence (line 12) | type ParsedSequence =
  type ParsedEscapeSequence (line 127) | type ParsedEscapeSequence =
  type InputParser (line 249) | type InputParser = {
  method push (line 260) | push(chunk) {
  method hasPendingEscape (line 265) | hasPendingEscape() {
  method flushPendingEscape (line 274) | flushPendingEscape() {
  method reset (line 283) | reset() {

FILE: src/kitty-keyboard.ts
  type KittyFlagName (line 12) | type KittyFlagName = keyof typeof kittyFlags;
  function resolveFlags (line 15) | function resolveFlags(flags: KittyFlagName[]): number {
  type KittyKeyboardOptions (line 40) | type KittyKeyboardOptions = {

FILE: src/log-update.ts
  type LogUpdate (line 15) | type LogUpdate = {

FILE: src/measure-element.ts
  type Output (line 3) | type Output = {

FILE: src/measure-text.ts
  type Output (line 5) | type Output = {

FILE: src/output.ts
  type Options (line 19) | type Options = {
  type Operation (line 24) | type Operation = WriteOperation | ClipOperation | UnclipOperation;
  type WriteOperation (line 26) | type WriteOperation = {
  type ClipOperation (line 34) | type ClipOperation = {
  type Clip (line 39) | type Clip = {
  type UnclipOperation (line 46) | type UnclipOperation = {
  class OutputCaches (line 50) | class OutputCaches {
    method getStyledChars (line 55) | getStyledChars(line: string): StyledChar[] {
    method getStringWidth (line 65) | getStringWidth(text: string): number {
    method getWidestLine (line 75) | getWidestLine(text: string): number {
  class Output (line 91) | class Output {
    method constructor (line 98) | constructor(options: Options) {
    method write (line 105) | write(
    method clip (line 126) | clip(clip: Clip) {
    method unclip (line 133) | unclip() {
    method get (line 139) | get(): {output: string; height: number} {

FILE: src/parse-keypress.ts
  type ParsedKey (line 130) | type ParsedKey = {
  type EventType (line 300) | type EventType = 'press' | 'repeat' | 'release';
  function resolveEventType (line 302) | function resolveEventType(value: number): EventType {
  function parseKittyModifiers (line 308) | function parseKittyModifiers(

FILE: src/reconciler.ts
  type AnyObject (line 46) | type AnyObject = Record<string, unknown>;
  type Props (line 86) | type Props = Record<string, unknown>;
  type HostContext (line 88) | type HostContext = {
  function loadPackageJson (line 96) | async function loadPackageJson() {
  method resetAfterCommit (line 160) | resetAfterCommit(rootNode) {
  method getChildHostContext (line 183) | getChildHostContext(parentHostContext, type) {
  method createInstance (line 194) | createInstance(originalType, newProps, rootNode, hostContext) {
  method createTextInstance (line 242) | createTextInstance(text, _root, hostContext) {
  method resetTextContent (line 251) | resetTextContent() {}
  method hideTextInstance (line 252) | hideTextInstance(node) {
  method unhideTextInstance (line 255) | unhideTextInstance(node, text) {
  method hideInstance (line 259) | hideInstance(node) {
  method unhideInstance (line 262) | unhideInstance(node) {
  method finalizeInitialChildren (line 268) | finalizeInitialChildren() {
  method beforeActiveInstanceBlur (line 286) | beforeActiveInstanceBlur() {}
  method afterActiveInstanceBlur (line 287) | afterActiveInstanceBlur() {}
  method detachDeletedInstance (line 288) | detachDeletedInstance() {}
  method prepareScopeUpdate (line 290) | prepareScopeUpdate() {}
  method removeChildFromContainer (line 294) | removeChildFromContainer(node, removeNode) {
  method commitUpdate (line 298) | commitUpdate(node, _type, oldProps, newProps) {
  method commitTextUpdate (line 343) | commitTextUpdate(node, _oldText, newText) {
  method removeChild (line 346) | removeChild(node, removeNode) {
  method setCurrentUpdatePriority (line 350) | setCurrentUpdatePriority(newPriority: number) {
  method resolveUpdatePriority (line 354) | resolveUpdatePriority() {
  method maySuspendCommit (line 361) | maySuspendCommit() {
  method resetFormInstance (line 371) | resetFormInstance() {}
  method requestPostPaintCallback (line 372) | requestPostPaintCallback() {}
  method shouldAttemptEagerTransition (line 373) | shouldAttemptEagerTransition() {
  method trackSchedulerEvent (line 376) | trackSchedulerEvent() {}
  method resolveEventType (line 377) | resolveEventType() {
  method resolveEventTimeStamp (line 380) | resolveEventTimeStamp() {
  method preloadInstance (line 383) | preloadInstance() {
  method startSuspendingCommit (line 386) | startSuspendingCommit() {}
  method suspendInstance (line 387) | suspendInstance() {}
  method waitForCommitToBeReady (line 388) | waitForCommitToBeReady() {

FILE: src/render-node-to-output.ts
  type OutputTransformer (line 30) | type OutputTransformer = (s: string, index: number) => string;

FILE: src/render-to-string.ts
  type RenderToStringOptions (line 8) | type RenderToStringOptions = {

FILE: src/render.ts
  type RenderOptions (line 8) | type RenderOptions = {
  type Instance (line 138) | type Instance = {
  method unmount (line 225) | unmount() {
  method cleanup (line 230) | cleanup() {

FILE: src/renderer.ts
  type Result (line 7) | type Result = {

FILE: src/styles.ts
  type Styles (line 7) | type Styles = {

FILE: src/write-synchronized.ts
  function shouldSynchronize (line 7) | function shouldSynchronize(

FILE: test/background.tsx
  function Test (line 360) | function Test({bgColor}: {readonly bgColor?: string}) {
  function Test (line 416) | function Test({bgColor}: {readonly bgColor?: string}) {

FILE: test/borders.tsx
  function Test (line 431) | function Test({borderColor}: {readonly borderColor?: string}) {
  function Test (line 474) | function Test({borderTop}: {readonly borderTop?: boolean}) {
  function Test (line 1002) | function Test({borderColor}: {readonly borderColor?: string}) {

FILE: test/components.tsx
  function World (line 54) | function World() {
  class ErrorBoundary (line 221) | class ErrorBoundary extends Component<{children?: React.ReactNode}> {
    method render (line 222) | override render(): React.ReactNode {
    method componentDidCatch (line 226) | override componentDidCatch(reactError: Error): void {
    method render (line 251) | override render(): React.ReactNode {
    method componentDidCatch (line 255) | override componentDidCatch(reactError: Error): void {
    method render (line 277) | override render(): React.ReactNode {
    method componentDidCatch (line 281) | override componentDidCatch(reactError: Error): void {
  class ErrorBoundary (line 250) | class ErrorBoundary extends Component<{children?: React.ReactNode}> {
    method render (line 222) | override render(): React.ReactNode {
    method componentDidCatch (line 226) | override componentDidCatch(reactError: Error): void {
    method render (line 251) | override render(): React.ReactNode {
    method componentDidCatch (line 255) | override componentDidCatch(reactError: Error): void {
    method render (line 277) | override render(): React.ReactNode {
    method componentDidCatch (line 281) | override componentDidCatch(reactError: Error): void {
  class ErrorBoundary (line 276) | class ErrorBoundary extends Component<{children?: React.ReactNode}> {
    method render (line 222) | override render(): React.ReactNode {
    method componentDidCatch (line 226) | override componentDidCatch(reactError: Error): void {
    method render (line 251) | override render(): React.ReactNode {
    method componentDidCatch (line 255) | override componentDidCatch(reactError: Error): void {
    method render (line 277) | override render(): React.ReactNode {
    method componentDidCatch (line 281) | override componentDidCatch(reactError: Error): void {
  function WithHooks (line 425) | function WithHooks() {
  function Dynamic (line 454) | function Dynamic({items}: {readonly items: string[]}) {
  function Dynamic (line 474) | function Dynamic({items}: {readonly items: string[]}) {
  function Dynamic (line 510) | function Dynamic({replace}: {readonly replace?: boolean}) {
  class Input (line 542) | class Input extends React.Component<{setRawMode: (mode: boolean) => void...
    method render (line 543) | override render() {
    method componentDidMount (line 547) | override componentDidMount() {
    method componentWillUnmount (line 551) | override componentWillUnmount() {
    method render (line 613) | override render() {
    method componentDidMount (line 617) | override componentDidMount() {
    method componentWillUnmount (line 621) | override componentWillUnmount() {
    method render (line 700) | override render() {
    method componentDidMount (line 704) | override componentDidMount() {
    method componentWillUnmount (line 712) | override componentWillUnmount() {
    method render (line 749) | override render() {
    method componentDidMount (line 753) | override componentDidMount() {
    method componentWillUnmount (line 757) | override componentWillUnmount() {
  function Test (line 556) | function Test({
  class Input (line 612) | class Input extends React.Component<{setRawMode: (mode: boolean) => void...
    method render (line 543) | override render() {
    method componentDidMount (line 547) | override componentDidMount() {
    method componentWillUnmount (line 551) | override componentWillUnmount() {
    method render (line 613) | override render() {
    method componentDidMount (line 617) | override componentDidMount() {
    method componentWillUnmount (line 621) | override componentWillUnmount() {
    method render (line 700) | override render() {
    method componentDidMount (line 704) | override componentDidMount() {
    method componentWillUnmount (line 712) | override componentWillUnmount() {
    method render (line 749) | override render() {
    method componentDidMount (line 753) | override componentDidMount() {
    method componentWillUnmount (line 757) | override componentWillUnmount() {
  function Test (line 626) | function Test({onInput}: {readonly onInput: (input: string) => void}) {
  class Input (line 699) | class Input extends React.Component<{setRawMode: (mode: boolean) => void...
    method render (line 543) | override render() {
    method componentDidMount (line 547) | override componentDidMount() {
    method componentWillUnmount (line 551) | override componentWillUnmount() {
    method render (line 613) | override render() {
    method componentDidMount (line 617) | override componentDidMount() {
    method componentWillUnmount (line 621) | override componentWillUnmount() {
    method render (line 700) | override render() {
    method componentDidMount (line 704) | override componentDidMount() {
    method componentWillUnmount (line 712) | override componentWillUnmount() {
    method render (line 749) | override render() {
    method componentDidMount (line 753) | override componentDidMount() {
    method componentWillUnmount (line 757) | override componentWillUnmount() {
  function Test (line 721) | function Test() {
  class Input (line 748) | class Input extends React.Component<{setRawMode: (mode: boolean) => void...
    method render (line 543) | override render() {
    method componentDidMount (line 547) | override componentDidMount() {
    method componentWillUnmount (line 551) | override componentWillUnmount() {
    method render (line 613) | override render() {
    method componentDidMount (line 617) | override componentDidMount() {
    method componentWillUnmount (line 621) | override componentWillUnmount() {
    method render (line 700) | override render() {
    method componentDidMount (line 704) | override componentDidMount() {
    method componentWillUnmount (line 712) | override componentWillUnmount() {
    method render (line 749) | override render() {
    method componentDidMount (line 753) | override componentDidMount() {
    method componentWillUnmount (line 757) | override componentWillUnmount() {
  function Test (line 762) | function Test({
  function Counter (line 853) | function Counter() {
  function Counter (line 908) | function Counter() {
  function Counter (line 950) | function Counter() {
  function Test (line 1056) | function Test() {
  function Test (line 1095) | function Test() {
  function Test (line 1138) | function Test() {
  function Test (line 1195) | function Test() {
  function App (line 1454) | function App() {
  function Dynamic (line 1516) | function Dynamic({remove}: {readonly remove?: boolean}) {

FILE: test/cursor.tsx
  function InputApp (line 65) | function InputApp() {
  function CursorChild (line 201) | function CursorChild() {
  function Parent (line 207) | function Parent() {
  function CursorChild (line 266) | function CursorChild() {
  function Test (line 277) | function Test() {
  function MultiLineApp (line 309) | function MultiLineApp() {
  function StdoutWriteApp (line 359) | function StdoutWriteApp() {
  function StderrWriteApp (line 372) | function StderrWriteApp() {
  type HookWriteCase (line 385) | type HookWriteCase = {
  method assertTargetWrite (line 400) | assertTargetWrite(t, output) {
  method assertTargetWrite (line 408) | assertTargetWrite(t, _output, stderr) {
  function DebugStdoutWriteApp (line 440) | function DebugStdoutWriteApp() {
  function DebugStderrWriteApp (line 450) | function DebugStderrWriteApp() {
  function DebugStderrWriteAfterRerenderApp (line 542) | function DebugStderrWriteAfterRerenderApp() {
  function DebugStdoutWriteAfterRerenderApp (line 559) | function DebugStdoutWriteAfterRerenderApp() {

FILE: test/errors.tsx
  function Parent (line 95) | function Parent() {
  function Test (line 142) | function Test() {

FILE: test/fixtures/ci.tsx
  type TestState (line 4) | type TestState = {
  class Test (line 9) | class Test extends React.Component<Record<string, unknown>, TestState> {
    method render (line 17) | override render() {
    method componentDidMount (line 29) | override componentDidMount() {
    method componentWillUnmount (line 46) | override componentWillUnmount() {

FILE: test/fixtures/clear.tsx
  function Clear (line 4) | function Clear() {

FILE: test/fixtures/console.tsx
  function App (line 4) | function App() {

FILE: test/fixtures/erase-with-state-change.tsx
  function Erase (line 5) | function Erase() {

FILE: test/fixtures/erase-with-static.tsx
  function EraseWithStatic (line 5) | function EraseWithStatic() {

FILE: test/fixtures/erase.tsx
  function Erase (line 5) | function Erase() {

FILE: test/fixtures/exit-double-raw-mode.tsx
  class ExitDoubleRawMode (line 5) | class ExitDoubleRawMode extends React.Component<{
    method render (line 8) | override render() {
    method componentDidMount (line 12) | override componentDidMount() {
  function Test (line 27) | function Test() {

FILE: test/fixtures/exit-on-exit-with-error-value-property.tsx
  function Test (line 4) | function Test() {

FILE: test/fixtures/exit-on-exit-with-error.tsx
  class Exit (line 4) | class Exit extends React.Component<
    method render (line 14) | override render() {
    method componentDidMount (line 18) | override componentDidMount() {
    method componentWillUnmount (line 30) | override componentWillUnmount() {
  function Test (line 35) | function Test() {

FILE: test/fixtures/exit-on-exit-with-result.tsx
  function Test (line 4) | function Test() {

FILE: test/fixtures/exit-on-exit-with-value-object.tsx
  function Test (line 4) | function Test() {

FILE: test/fixtures/exit-on-exit.tsx
  class Exit (line 4) | class Exit extends React.Component<
    method render (line 14) | override render() {
    method componentDidMount (line 18) | override componentDidMount() {
    method componentWillUnmount (line 28) | override componentWillUnmount() {
  function Test (line 33) | function Test() {

FILE: test/fixtures/exit-on-finish.tsx
  class Test (line 4) | class Test extends React.Component<Record<string, unknown>, {counter: nu...
    method render (line 11) | override render() {
    method componentDidMount (line 15) | override componentDidMount() {
    method componentWillUnmount (line 31) | override componentWillUnmount() {

FILE: test/fixtures/exit-on-unmount.tsx
  class Test (line 4) | class Test extends React.Component<Record<string, unknown>, {counter: nu...
    method render (line 11) | override render() {
    method componentDidMount (line 15) | override componentDidMount() {
    method componentWillUnmount (line 23) | override componentWillUnmount() {

FILE: test/fixtures/exit-raw-on-exit-with-error.tsx
  class Exit (line 4) | class Exit extends React.Component<{
    method render (line 8) | override render() {
    method componentDidMount (line 12) | override componentDidMount() {
  function Test (line 21) | function Test() {

FILE: test/fixtures/exit-raw-on-exit.tsx
  class Exit (line 4) | class Exit extends React.Component<{
    method render (line 8) | override render() {
    method componentDidMount (line 12) | override componentDidMount() {
  function Test (line 18) | function Test() {

FILE: test/fixtures/exit-raw-on-unmount.tsx
  class Exit (line 4) | class Exit extends React.Component<{
    method render (line 7) | override render() {
    method componentDidMount (line 11) | override componentDidMount() {
  function Test (line 16) | function Test() {

FILE: test/fixtures/exit-with-static.tsx
  function Test (line 4) | function Test() {

FILE: test/fixtures/fullscreen-no-extra-newline.tsx
  function Fullscreen (line 5) | function Fullscreen() {

FILE: test/fixtures/issue-442-full-height.tsx
  function App (line 5) | function App() {

FILE: test/fixtures/issue-450-fixture-helpers.tsx
  type RerenderFixtureOptions (line 5) | type RerenderFixtureOptions = {
  function Issue450RerenderFixtureComponent (line 13) | function Issue450RerenderFixtureComponent({
  type InitialFixtureOptions (line 93) | type InitialFixtureOptions = {
  function Issue450InitialFixtureComponent (line 100) | function Issue450InitialFixtureComponent({

FILE: test/fixtures/issue-725-child-process.tsx
  function App (line 4) | function App() {

FILE: test/fixtures/use-input-ctrl-c.tsx
  function UserInput (line 5) | function UserInput() {

FILE: test/fixtures/use-input-discrete-priority.tsx
  function App (line 11) | function App() {

FILE: test/fixtures/use-input-kitty.tsx
  function UserInput (line 5) | function UserInput({test}: {readonly test: string | undefined}) {

FILE: test/fixtures/use-input-many.tsx
  function InputHandler (line 12) | function InputHandler() {
  function App (line 17) | function App() {

FILE: test/fixtures/use-input-multiple.tsx
  function App (line 5) | function App() {

FILE: test/fixtures/use-input.tsx
  function UserInput (line 5) | function UserInput({test}: {readonly test: string | undefined}) {

FILE: test/fixtures/use-paste.tsx
  function PasteDemo (line 5) | function PasteDemo({test}: {readonly test: string | undefined}) {
  function MultipleHooksDemo (line 40) | function MultipleHooksDemo() {

FILE: test/fixtures/use-stdout.tsx
  function WriteToStdout (line 4) | function WriteToStdout() {

FILE: test/flex-align-content.tsx
  function Test (line 66) | function Test({
  function Test (line 95) | function Test({
  function Test (line 124) | function Test({showAlignContent}: {readonly showAlignContent: boolean}) {

FILE: test/focus.tsx
  type TestProps (line 31) | type TestProps = {
  function Test (line 43) | function Test({
  type ItemProps (line 91) | type ItemProps = {
  function Item (line 97) | function Item({label, autoFocus, disabled = false}: ItemProps) {
  function ItemWithId (line 559) | function ItemWithId({
  function ActiveIdReader (line 576) | function ActiveIdReader({
  function FocusCapture (line 671) | function FocusCapture() {

FILE: test/helpers/create-stdout.ts
  type FakeStdout (line 5) | type FakeStdout = {

FILE: test/helpers/render-to-string.ts
  type RenderToStringOptions (line 5) | type RenderToStringOptions = {

FILE: test/helpers/run.ts
  type Run (line 13) | type Run = (

FILE: test/helpers/term.ts
  method write (line 49) | write(input: string) {

FILE: test/helpers/test-renderer.ts
  type TestRenderOptions (line 5) | type TestRenderOptions = {
  type TestInstance (line 10) | type TestInstance = Instance & {
  function renderAsync (line 21) | async function renderAsync(
  function renderSync (line 53) | function renderSync(
  function withAct (line 81) | async function withAct<T>(fn: () => T | Promise<T>): Promise<T> {
  function waitForSuspense (line 92) | async function waitForSuspense(ms = 0): Promise<void> {

FILE: test/measure-element.tsx
  function Test (line 17) | function Test() {
  function Test (line 46) | function Test() {
  function Test (line 89) | function Test() {
  function Test (line 135) | function Test() {
  function Test (line 177) | function Test() {

FILE: test/position.tsx
  function Test (line 100) | function Test({top}: {readonly top?: number}) {
  function Test (line 124) | function Test({top, left}: {readonly top?: string; readonly left?: strin...
  function Test (line 148) | function Test({showOffsets}: {readonly showOffsets: boolean}) {
  function Test (line 175) | function Test({

FILE: test/reconciler.tsx
  function Test (line 8) | function Test({update}: {readonly update?: boolean}) {
  function Test (line 40) | function Test({update}: {readonly update?: boolean}) {
  function Test (line 77) | function Test({withStyle}: {readonly withStyle: boolean}) {
  function Test (line 99) | function Test({append}: {readonly append?: boolean}) {
  function Test (line 155) | function Test({insert}: {readonly insert?: boolean}) {
  function Test (line 215) | function Test({remove}: {readonly remove?: boolean}) {
  function Test (line 271) | function Test({reorder}: {readonly reorder?: boolean}) {
  function Dynamic (line 331) | function Dynamic({replace}: {readonly replace?: boolean}) {
  function Suspendable (line 376) | function Suspendable() {
  function Test (line 380) | function Test() {
  function Suspendable (line 412) | function Suspendable() {
  function Test (line 421) | function Test() {

FILE: test/render-to-string.tsx
  function World (line 28) | function World() {
  function App (line 264) | function App() {
  function App (line 279) | function App() {
  function App (line 296) | function App() {
  function Broken (line 314) | function Broken(): React.JSX.Element {
  function Broken (line 328) | function Broken(): React.JSX.Element {

FILE: test/render.tsx
  method write (line 68) | write(input: string) {
  method write (line 135) | write(
  type Issue450Fixture (line 160) | type Issue450Fixture =
  type Issue450FixtureResult (line 233) | type Issue450FixtureResult = {
  class SynchronousErrorBoundary (line 289) | class SynchronousErrorBoundary extends PureComponent<
    method getDerivedStateFromError (line 298) | static override getDerivedStateFromError(error: Error) {
    method componentDidCatch (line 306) | override componentDidCatch(error: Error) {
    method render (line 310) | override render() {
  function SynchronousRenderErrorComponent (line 319) | function SynchronousRenderErrorComponent() {
  function ThrowingComponentWithBoundary (line 323) | function ThrowingComponentWithBoundary() {
  function NonTtyRerenderTestComponent (line 619) | function NonTtyRerenderTestComponent({
  function NonTtyOverflowTransitionTestComponent (line 660) | function NonTtyOverflowTransitionTestComponent({
  function ResizeBoundaryTestComponent (line 697) | function ResizeBoundaryTestComponent() {
  function Test (line 799) | function Test() {
  function ThrottleTestComponent (line 831) | function ThrottleTestComponent({text}: {readonly text: string}) {
  function ThrottleCursorTestComponent (line 835) | function ThrottleCursorTestComponent({text}: {readonly text: string}) {
  method onRender (line 877) | onRender(metrics: RenderMetrics) {
  function Test (line 885) | function Test({children}: {readonly children?: ReactNode}) {
  method write (line 1018) | write(_chunk, _encoding, callback) {
  method shouldDelay (line 1043) | shouldDelay(chunk) {
  method onDelayElapsed (line 1046) | onDelayElapsed() {
  method shouldDelay (line 1092) | shouldDelay(chunk) {
  method onDelayElapsed (line 1095) | onDelayElapsed() {
  method shouldDelay (line 1182) | shouldDelay(chunk) {
  method onDelayElapsed (line 1188) | onDelayElapsed() {
  method write (line 1217) | write(
  method shouldDelay (line 1258) | shouldDelay(chunk) {
  method onDelayElapsed (line 1264) | onDelayElapsed() {
  method shouldDelay (line 1304) | shouldDelay(chunk) {
  method onDelayElapsed (line 1307) | onDelayElapsed() {
  function Test (line 1312) | function Test() {
  method shouldDelay (line 1341) | shouldDelay(chunk) {
  method onDelayElapsed (line 1347) | onDelayElapsed() {
  function Test (line 1352) | function Test() {
  method shouldDelay (line 1390) | shouldDelay(chunk) {
  method onDelayElapsed (line 1396) | onDelayElapsed() {
  function Test (line 1401) | function Test() {
  method shouldDelay (line 1453) | shouldDelay(chunk) {
  method onDelayElapsed (line 1456) | onDelayElapsed() {
  function Test (line 1491) | function Test() {
  method shouldDelay (line 1517) | shouldDelay(chunk) {
  method onDelayElapsed (line 1520) | onDelayElapsed() {
  function Test (line 1525) | function Test() {
  method write (line 1552) | write(
  function Test (line 1568) | function Test() {
  method write (line 1605) | write(_chunk, _encoding, callback) {
  function Test (line 1618) | function Test() {
  method write (line 1646) | write(_chunk, _encoding, callback) {
  function Test (line 1659) | function Test() {
  method write (line 1687) | write(_chunk, _encoding, callback) {
  function Test (line 1700) | function Test() {
  function Test (line 1730) | function Test() {
  method write (line 1756) | write(chunk: string | Uint8Array, _encoding, callback) {
  function Test (line 1773) | function Test() {

FILE: test/screen-reader.tsx
  function NullComponent (line 154) | function NullComponent(): undefined {

FILE: test/terminal-resize.tsx
  function Test (line 20) | function Test() {
  function Test (line 47) | function Test() {
  function Test (line 72) | function Test() {
  function Test (line 98) | function Test() {
  function Test (line 140) | function Test() {
  function Test (line 156) | function Test() {
  function Test (line 185) | function Test() {
  function Test (line 216) | function Test() {
  function Test (line 251) | function Test() {

FILE: test/text.tsx
  function Test (line 110) | function Test({show}: {readonly show?: boolean}) {
  function Test (line 130) | function Test({add}: {readonly add?: boolean}) {
  function Test (line 147) | function Test({add}: {readonly add?: boolean}) {
  function Test (line 478) | function Test({add}: {readonly add?: boolean}) {
  function Test (line 494) | function Test({add}: {readonly add?: boolean}) {

FILE: test/use-box-metrics.tsx
  function Test (line 17) | function Test() {
  function Test (line 40) | function Test() {
  function Test (line 66) | function Test() {
  function Test (line 93) | function Test() {
  function Test (line 142) | function Test() {
  function Test (line 186) | function Test() {
  function Test (line 233) | function Test() {
  function Test (line 271) | function Test() {
  function SimpleBox (line 291) | function SimpleBox() {
  function Test (line 330) | function Test() {
  function Test (line 352) | function Test() {
  function Test (line 375) | function Test() {
  function Test (line 429) | function Test() {
  function Test (line 468) | function Test() {

FILE: test/width-height.tsx
  function Test (line 169) | function Test({maxWidth}: {readonly maxWidth?: number}) {
  function Test (line 214) | function Test({maxHeight}: {readonly maxHeight?: number}) {
  function Test (line 290) | function Test({aspectRatio}: {readonly aspectRatio?: number}) {
Condensed preview — 225 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (853K chars).
[
  {
    "path": ".editorconfig",
    "chars": 175,
    "preview": "root = true\n\n[*]\nindent_style = tab\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newlin"
  },
  {
    "path": ".gitattributes",
    "chars": 19,
    "preview": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 536,
    "preview": "name: test\non: [push, pull_request]\njobs:\n  test:\n    name: Node.js ${{ matrix.node_version }}\n    runs-on: ubuntu-lates"
  },
  {
    "path": ".gitignore",
    "chars": 20,
    "preview": "node_modules\n/build\n"
  },
  {
    "path": ".npmrc",
    "chars": 19,
    "preview": "package-lock=false\n"
  },
  {
    "path": "benchmark/simple/index.ts",
    "chars": 22,
    "preview": "import './simple.js';\n"
  },
  {
    "path": "benchmark/simple/simple.tsx",
    "chars": 1072,
    "preview": "import React from 'react';\nimport {render, Box, Text} from '../../src/index.js';\n\nfunction App() {\n\treturn (\n\t\t<Box flex"
  },
  {
    "path": "benchmark/static/index.ts",
    "chars": 22,
    "preview": "import './static.js';\n"
  },
  {
    "path": "benchmark/static/static.tsx",
    "chars": 1813,
    "preview": "import React from 'react';\nimport {render, Box, Text, Static} from '../../src/index.js';\n\nfunction App() {\n\tconst [items"
  },
  {
    "path": "examples/alternate-screen/alternate-screen.tsx",
    "chars": 6006,
    "preview": "import React, {useReducer, useEffect, useRef, useCallback} from 'react';\nimport {\n\trender,\n\tText,\n\tBox,\n\tuseInput,\n\tuseA"
  },
  {
    "path": "examples/alternate-screen/index.ts",
    "chars": 95,
    "preview": "import {runAlternateScreenExample} from './alternate-screen.js';\n\nrunAlternateScreenExample();\n"
  },
  {
    "path": "examples/aria/aria.tsx",
    "chars": 710,
    "preview": "import React, {useState} from 'react';\nimport {render, Text, Box, useInput} from '../../src/index.js';\n\nfunction AriaExa"
  },
  {
    "path": "examples/aria/index.ts",
    "chars": 20,
    "preview": "import './aria.js';\n"
  },
  {
    "path": "examples/borders/borders.tsx",
    "chars": 820,
    "preview": "import React from 'react';\nimport {render, Box, Text} from '../../src/index.js';\n\nfunction Borders() {\n\treturn (\n\t\t<Box "
  },
  {
    "path": "examples/borders/index.ts",
    "chars": 23,
    "preview": "import './borders.js';\n"
  },
  {
    "path": "examples/box-backgrounds/box-backgrounds.tsx",
    "chars": 2411,
    "preview": "import React from 'react';\nimport {Box, Text} from '../../src/index.js';\n\nfunction BoxBackgrounds() {\n\treturn (\n\t\t<Box f"
  },
  {
    "path": "examples/box-backgrounds/index.ts",
    "chars": 187,
    "preview": "#!/usr/bin/env node\nimport React from 'react';\nimport {render} from '../../src/index.js';\nimport BoxBackgrounds from './"
  },
  {
    "path": "examples/chat/chat.tsx",
    "chars": 993,
    "preview": "import React, {useState} from 'react';\nimport {render, Text, Box, useInput} from '../../src/index.js';\n\nlet messageId = "
  },
  {
    "path": "examples/chat/index.ts",
    "chars": 20,
    "preview": "import './chat.js';\n"
  },
  {
    "path": "examples/concurrent-suspense/concurrent-suspense.tsx",
    "chars": 2735,
    "preview": "import React, {Suspense, useState} from 'react';\nimport {render, Box, Text} from '../../src/index.js';\n\n// Simulated asy"
  },
  {
    "path": "examples/concurrent-suspense/index.ts",
    "chars": 35,
    "preview": "import './concurrent-suspense.js';\n"
  },
  {
    "path": "examples/counter/counter.tsx",
    "chars": 462,
    "preview": "import React from 'react';\nimport {render, Text} from '../../src/index.js';\n\nfunction Counter() {\n\tconst [counter, setCo"
  },
  {
    "path": "examples/counter/index.ts",
    "chars": 23,
    "preview": "import './counter.js';\n"
  },
  {
    "path": "examples/cursor-ime/cursor-ime.tsx",
    "chars": 812,
    "preview": "import React, {useState} from 'react';\nimport stringWidth from 'string-width';\nimport {render, Box, Text, useInput, useC"
  },
  {
    "path": "examples/cursor-ime/index.ts",
    "chars": 26,
    "preview": "import './cursor-ime.js';\n"
  },
  {
    "path": "examples/incremental-rendering/incremental-rendering.tsx",
    "chars": 9080,
    "preview": "import React, {useState, useEffect} from 'react';\nimport {\n\trender,\n\tText,\n\tBox,\n\tuseInput,\n\tuseWindowSize,\n\tuseApp,\n} f"
  },
  {
    "path": "examples/incremental-rendering/index.ts",
    "chars": 37,
    "preview": "import './incremental-rendering.js';\n"
  },
  {
    "path": "examples/jest/index.ts",
    "chars": 20,
    "preview": "import './jest.js';\n"
  },
  {
    "path": "examples/jest/jest.tsx",
    "chars": 2313,
    "preview": "import React from 'react';\nimport PQueue from 'p-queue';\nimport delay from 'delay';\nimport ms from 'ms';\nimport {Static,"
  },
  {
    "path": "examples/jest/summary.tsx",
    "chars": 901,
    "preview": "import React from 'react';\nimport {Box, Text} from '../../src/index.js';\n\ntype Properties = {\n\treadonly isFinished: bool"
  },
  {
    "path": "examples/jest/test.tsx",
    "chars": 776,
    "preview": "import React from 'react';\nimport {Box, Text} from '../../src/index.js';\n\nconst getBackgroundForStatus = (status: string"
  },
  {
    "path": "examples/justify-content/index.ts",
    "chars": 31,
    "preview": "import './justify-content.js';\n"
  },
  {
    "path": "examples/justify-content/justify-content.tsx",
    "chars": 1271,
    "preview": "import React from 'react';\nimport {render, Box, Text} from '../../src/index.js';\n\nfunction JustifyContent() {\n\treturn (\n"
  },
  {
    "path": "examples/render-throttle/index.tsx",
    "chars": 672,
    "preview": "import React, {useState, useEffect} from 'react';\nimport {render, Box, Text} from '../../src/index.js';\n\nfunction App() "
  },
  {
    "path": "examples/router/index.ts",
    "chars": 22,
    "preview": "import './router.js';\n"
  },
  {
    "path": "examples/router/router.tsx",
    "chars": 1107,
    "preview": "import React from 'react';\nimport {MemoryRouter, Routes, Route, useNavigate} from 'react-router';\nimport {render, useInp"
  },
  {
    "path": "examples/select-input/index.ts",
    "chars": 28,
    "preview": "import './select-input.js';\n"
  },
  {
    "path": "examples/select-input/select-input.tsx",
    "chars": 1451,
    "preview": "import React, {useState} from 'react';\nimport {\n\trender,\n\tText,\n\tBox,\n\tuseInput,\n\tuseIsScreenReaderEnabled,\n} from '../."
  },
  {
    "path": "examples/static/index.ts",
    "chars": 22,
    "preview": "import './static.js';\n"
  },
  {
    "path": "examples/static/static.tsx",
    "chars": 917,
    "preview": "import React from 'react';\nimport {Box, Text, render, Static} from '../../src/index.js';\n\nfunction Example() {\n\tconst [t"
  },
  {
    "path": "examples/subprocess-output/index.ts",
    "chars": 33,
    "preview": "import './subprocess-output.js';\n"
  },
  {
    "path": "examples/subprocess-output/subprocess-output.tsx",
    "chars": 861,
    "preview": "import childProcess from 'node:child_process';\nimport type {Buffer} from 'node:buffer';\nimport React from 'react';\nimpor"
  },
  {
    "path": "examples/suspense/index.ts",
    "chars": 24,
    "preview": "import './suspense.js';\n"
  },
  {
    "path": "examples/suspense/suspense.tsx",
    "chars": 794,
    "preview": "import React from 'react';\nimport {render, Text} from '../../src/index.js';\n\nlet promise: Promise<void> | undefined;\nlet"
  },
  {
    "path": "examples/table/index.ts",
    "chars": 21,
    "preview": "import './table.js';\n"
  },
  {
    "path": "examples/table/table.tsx",
    "chars": 844,
    "preview": "import React from 'react';\nimport {faker} from '@faker-js/faker';\nimport {Box, Text, render} from '../../src/index.js';\n"
  },
  {
    "path": "examples/terminal-resize/index.ts",
    "chars": 31,
    "preview": "import './terminal-resize.js';\n"
  },
  {
    "path": "examples/terminal-resize/terminal-resize.tsx",
    "chars": 585,
    "preview": "import React from 'react';\nimport {render, Box, Text, useWindowSize} from '../../src/index.js';\n\nfunction TerminalResize"
  },
  {
    "path": "examples/use-focus/index.ts",
    "chars": 25,
    "preview": "import './use-focus.js';\n"
  },
  {
    "path": "examples/use-focus/use-focus.tsx",
    "chars": 600,
    "preview": "import React from 'react';\nimport {Box, Text, render, useFocus} from '../../src/index.js';\n\nfunction Focus() {\n\treturn ("
  },
  {
    "path": "examples/use-focus-with-id/index.ts",
    "chars": 33,
    "preview": "import './use-focus-with-id.js';\n"
  },
  {
    "path": "examples/use-focus-with-id/use-focus-with-id.tsx",
    "chars": 981,
    "preview": "import React from 'react';\nimport {\n\trender,\n\tBox,\n\tText,\n\tuseFocus,\n\tuseInput,\n\tuseFocusManager,\n} from '../../src/inde"
  },
  {
    "path": "examples/use-input/index.ts",
    "chars": 25,
    "preview": "import './use-input.js';\n"
  },
  {
    "path": "examples/use-input/use-input.tsx",
    "chars": 750,
    "preview": "import React from 'react';\nimport {render, useInput, useApp, Box, Text} from '../../src/index.js';\n\nfunction Robot() {\n\t"
  },
  {
    "path": "examples/use-stderr/index.ts",
    "chars": 26,
    "preview": "import './use-stderr.js';\n"
  },
  {
    "path": "examples/use-stderr/use-stderr.tsx",
    "chars": 369,
    "preview": "import React from 'react';\nimport {render, Text, useStderr} from '../../src/index.js';\n\nfunction Example() {\n\tconst {wri"
  },
  {
    "path": "examples/use-stdout/index.ts",
    "chars": 26,
    "preview": "import './use-stdout.js';\n"
  },
  {
    "path": "examples/use-stdout/use-stdout.tsx",
    "chars": 680,
    "preview": "import React from 'react';\nimport {render, Box, Text, useStdout} from '../../src/index.js';\n\nfunction Example() {\n\tconst"
  },
  {
    "path": "examples/use-transition/index.ts",
    "chars": 30,
    "preview": "import './use-transition.js';\n"
  },
  {
    "path": "examples/use-transition/use-transition.tsx",
    "chars": 2603,
    "preview": "import React, {useState, useMemo, useTransition} from 'react';\nimport {render, Box, Text, useInput} from '../../src/inde"
  },
  {
    "path": "license",
    "chars": 1202,
    "preview": "MIT License\n\nCopyright (c) Vadym Demedes <vadimdemedes@hey.com> (https://github.com/vadimdemedes)\nCopyright (c) Sindre S"
  },
  {
    "path": "media/demo.js",
    "chars": 1075,
    "preview": "import process from 'node:process';\nimport React from 'react';\nimport {render, Box, Text} from 'ink';\n\nclass Counter ext"
  },
  {
    "path": "package.json",
    "chars": 2988,
    "preview": "{\n\t\"name\": \"ink\",\n\t\"version\": \"6.8.0\",\n\t\"description\": \"React for CLI\",\n\t\"license\": \"MIT\",\n\t\"repository\": \"vadimdemedes/"
  },
  {
    "path": "readme.md",
    "chars": 75433,
    "preview": "[![](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://github.com/vshyman"
  },
  {
    "path": "recipes/routing.md",
    "chars": 1535,
    "preview": "# Routing with React Router\n\n[React Router](https://reactrouter.com) can be used for routing in Ink apps via its [`Memor"
  },
  {
    "path": "src/ansi-tokenizer.ts",
    "chars": 11955,
    "preview": "const bellCharacter = '\\u0007';\nconst escapeCharacter = '\\u001B';\nconst stringTerminatorCharacter = '\\u009C';\nconst csiC"
  },
  {
    "path": "src/colorize.ts",
    "chars": 1498,
    "preview": "import chalk, {type ForegroundColorName, type BackgroundColorName} from 'chalk';\n\ntype ColorType = 'foreground' | 'backg"
  },
  {
    "path": "src/components/AccessibilityContext.ts",
    "chars": 125,
    "preview": "import {createContext} from 'react';\n\nexport const accessibilityContext = createContext({\n\tisScreenReaderEnabled: false,"
  },
  {
    "path": "src/components/App.tsx",
    "chars": 15422,
    "preview": "import {EventEmitter} from 'node:events';\nimport process from 'node:process';\nimport React, {\n\ttype ReactNode,\n\tuseState"
  },
  {
    "path": "src/components/AppContext.ts",
    "chars": 1130,
    "preview": "import {createContext} from 'react';\n\nexport type Props = {\n\t/**\n\tExit (unmount) the whole Ink app.\n\n\t- `exit()` — resol"
  },
  {
    "path": "src/components/BackgroundContext.ts",
    "chars": 303,
    "preview": "import {createContext} from 'react';\nimport {type LiteralUnion} from 'type-fest';\nimport {type ForegroundColorName} from"
  },
  {
    "path": "src/components/Box.tsx",
    "chars": 2563,
    "preview": "import React, {forwardRef, useContext, type PropsWithChildren} from 'react';\nimport {type Except} from 'type-fest';\nimpo"
  },
  {
    "path": "src/components/CursorContext.ts",
    "chars": 520,
    "preview": "import {createContext} from 'react';\nimport {type CursorPosition} from '../log-update.js';\n\nexport type Props = {\n\t/**\n\t"
  },
  {
    "path": "src/components/ErrorBoundary.tsx",
    "chars": 852,
    "preview": "import React, {PureComponent, type ReactNode} from 'react';\nimport ErrorOverview from './ErrorOverview.js';\n\ntype Props "
  },
  {
    "path": "src/components/ErrorOverview.tsx",
    "chars": 3468,
    "preview": "import * as fs from 'node:fs';\nimport {cwd} from 'node:process';\nimport React from 'react';\nimport StackUtils from 'stac"
  },
  {
    "path": "src/components/FocusContext.ts",
    "chars": 833,
    "preview": "import {createContext} from 'react';\n\nexport type Props = {\n\treadonly activeId?: string;\n\treadonly add: (id: string, opt"
  },
  {
    "path": "src/components/Newline.tsx",
    "chars": 332,
    "preview": "import React from 'react';\n\nexport type Props = {\n\t/**\n\tNumber of newlines to insert.\n\n\t@default 1\n\t*/\n\treadonly count?:"
  },
  {
    "path": "src/components/Spacer.tsx",
    "chars": 288,
    "preview": "import React from 'react';\nimport Box from './Box.js';\n\n/**\nA flexible space that expands along the major axis of its co"
  },
  {
    "path": "src/components/Static.tsx",
    "chars": 1972,
    "preview": "import React, {useMemo, useState, useLayoutEffect, type ReactNode} from 'react';\nimport {type Styles} from '../styles.js"
  },
  {
    "path": "src/components/StderrContext.ts",
    "chars": 880,
    "preview": "import process from 'node:process';\nimport {createContext} from 'react';\n\nexport type Props = {\n\t/**\n\tStderr stream pass"
  },
  {
    "path": "src/components/StdinContext.ts",
    "chars": 1842,
    "preview": "import {EventEmitter} from 'node:events';\nimport process from 'node:process';\nimport {createContext} from 'react';\n\nexpo"
  },
  {
    "path": "src/components/StdoutContext.ts",
    "chars": 907,
    "preview": "import process from 'node:process';\nimport {createContext} from 'react';\n\nexport type Props = {\n\t/**\n\tStdout stream pass"
  },
  {
    "path": "src/components/Text.tsx",
    "chars": 3436,
    "preview": "import React, {useContext, type ReactNode} from 'react';\nimport chalk, {type ForegroundColorName} from 'chalk';\nimport {"
  },
  {
    "path": "src/components/Transform.tsx",
    "chars": 1492,
    "preview": "import React, {useContext, type ReactNode} from 'react';\nimport {accessibilityContext} from './AccessibilityContext.js';"
  },
  {
    "path": "src/cursor-helpers.ts",
    "chars": 2928,
    "preview": "import ansiEscapes from 'ansi-escapes';\n\nexport type CursorPosition = {\n\tx: number;\n\ty: number;\n};\n\nconst showCursorEsca"
  },
  {
    "path": "src/devtools-window-polyfill.ts",
    "chars": 1931,
    "preview": "// Ignoring missing types error to avoid adding another dependency for this hack to work\nimport ws from 'ws';\n\n// eslint"
  },
  {
    "path": "src/devtools.ts",
    "chars": 1191,
    "preview": "/* eslint-disable import-x/order */\n\n// eslint-disable-next-line import-x/no-unassigned-import\nimport './devtools-window"
  },
  {
    "path": "src/dom.ts",
    "chars": 6734,
    "preview": "import Yoga, {type Node as YogaNode} from 'yoga-layout';\nimport measureText from './measure-text.js';\nimport {type Style"
  },
  {
    "path": "src/get-max-width.ts",
    "chars": 372,
    "preview": "import Yoga, {type Node as YogaNode} from 'yoga-layout';\n\nconst getMaxWidth = (yogaNode: YogaNode) => {\n\treturn (\n\t\tyoga"
  },
  {
    "path": "src/global.d.ts",
    "chars": 904,
    "preview": "import {type ReactNode, type Key, type Ref} from 'react';\nimport {type Except} from 'type-fest';\nimport {type DOMElement"
  },
  {
    "path": "src/hooks/use-app.ts",
    "chars": 256,
    "preview": "import {useContext} from 'react';\nimport AppContext from '../components/AppContext.js';\n\n/**\nA React hook that returns a"
  },
  {
    "path": "src/hooks/use-box-metrics.ts",
    "chars": 3683,
    "preview": "import {type RefObject, useState, useEffect, useCallback, useMemo} from 'react';\nimport {type DOMElement, addLayoutListe"
  },
  {
    "path": "src/hooks/use-cursor.ts",
    "chars": 1333,
    "preview": "import {useContext, useRef, useCallback, useInsertionEffect} from 'react';\nimport CursorContext from '../components/Curs"
  },
  {
    "path": "src/hooks/use-focus-manager.ts",
    "chars": 2012,
    "preview": "import {useContext} from 'react';\nimport FocusContext, {type Props} from '../components/FocusContext.js';\n\ntype Output ="
  },
  {
    "path": "src/hooks/use-focus.ts",
    "chars": 1952,
    "preview": "import {useEffect, useContext, useMemo} from 'react';\nimport FocusContext from '../components/FocusContext.js';\nimport u"
  },
  {
    "path": "src/hooks/use-input.ts",
    "chars": 6272,
    "preview": "import {useEffect} from 'react';\nimport parseKeypress, {nonAlphanumericKeys} from '../parse-keypress.js';\nimport reconci"
  },
  {
    "path": "src/hooks/use-is-screen-reader-enabled.ts",
    "chars": 449,
    "preview": "import {useContext} from 'react';\nimport {accessibilityContext} from '../components/AccessibilityContext.js';\n\n/**\nA Rea"
  },
  {
    "path": "src/hooks/use-paste.ts",
    "chars": 2287,
    "preview": "import {useEffect} from 'react';\nimport reconciler from '../reconciler.js';\nimport {useStdinContext} from './use-stdin.j"
  },
  {
    "path": "src/hooks/use-stderr.ts",
    "chars": 224,
    "preview": "import {useContext} from 'react';\nimport StderrContext from '../components/StderrContext.js';\n\n/**\nA React hook that ret"
  },
  {
    "path": "src/hooks/use-stdin.ts",
    "chars": 368,
    "preview": "import {useContext} from 'react';\nimport StdinContext, {\n\ttype PublicProps,\n\ttype Props,\n} from '../components/StdinCont"
  },
  {
    "path": "src/hooks/use-stdout.ts",
    "chars": 251,
    "preview": "import {useContext} from 'react';\nimport StdoutContext from '../components/StdoutContext.js';\n\n/**\nA React hook that ret"
  },
  {
    "path": "src/hooks/use-window-size.ts",
    "chars": 891,
    "preview": "import {useState, useEffect} from 'react';\nimport {getWindowSize} from '../utils.js';\nimport useStdout from './use-stdou"
  },
  {
    "path": "src/index.ts",
    "chars": 2459,
    "preview": "export type {RenderOptions, Instance} from './render.js';\nexport {default as render} from './render.js';\nexport type {Re"
  },
  {
    "path": "src/ink.tsx",
    "chars": 33600,
    "preview": "import process from 'node:process';\nimport {Buffer} from 'node:buffer';\nimport React, {type ReactNode} from 'react';\nimp"
  },
  {
    "path": "src/input-parser.ts",
    "chars": 6755,
    "preview": "const escape = '\\u001B';\nconst pasteStart = '\\u001B[200~';\nconst pasteEnd = '\\u001B[201~';\n\nexport type InputEvent = str"
  },
  {
    "path": "src/instances.ts",
    "chars": 417,
    "preview": "// Store all instances of Ink (instance.js) to ensure that consecutive render() calls\n// use the same instance of Ink an"
  },
  {
    "path": "src/kitty-keyboard.ts",
    "chars": 1769,
    "preview": "// Kitty keyboard protocol flags.\n// @see https://sw.kovidgoyal.net/kitty/keyboard-protocol/\nexport const kittyFlags = {"
  },
  {
    "path": "src/log-update.ts",
    "chars": 10305,
    "preview": "import {type Writable} from 'node:stream';\nimport ansiEscapes from 'ansi-escapes';\nimport cliCursor from 'cli-cursor';\ni"
  },
  {
    "path": "src/measure-element.ts",
    "chars": 964,
    "preview": "import {type DOMElement} from './dom.js';\n\ntype Output = {\n\t/**\n\tElement width.\n\t*/\n\twidth: number;\n\n\t/**\n\tElement heigh"
  },
  {
    "path": "src/measure-text.ts",
    "chars": 549,
    "preview": "import widestLine from 'widest-line';\n\nconst cache = new Map<string, Output>();\n\ntype Output = {\n\twidth: number;\n\theight"
  },
  {
    "path": "src/output.ts",
    "chars": 6270,
    "preview": "import sliceAnsi from 'slice-ansi';\nimport stringWidth from 'string-width';\nimport {\n\ttype StyledChar,\n\tstyledCharsFromT"
  },
  {
    "path": "src/parse-keypress.ts",
    "chars": 12673,
    "preview": "// Copied from https://github.com/enquirer/enquirer/blob/36785f3399a41cd61e9d28d1eb9c2fcd73d69b4c/lib/keypress.js\nimport"
  },
  {
    "path": "src/reconciler.ts",
    "chars": 9556,
    "preview": "import process from 'node:process';\nimport createReconciler, {type ReactContext} from 'react-reconciler';\nimport {\n\tDefa"
  },
  {
    "path": "src/render-background.ts",
    "chars": 1367,
    "preview": "import colorize from './colorize.js';\nimport {type DOMNode} from './dom.js';\nimport type Output from './output.js';\n\ncon"
  },
  {
    "path": "src/render-border.ts",
    "chars": 3247,
    "preview": "import cliBoxes from 'cli-boxes';\nimport chalk from 'chalk';\nimport colorize from './colorize.js';\nimport {type DOMNode}"
  },
  {
    "path": "src/render-node-to-output.ts",
    "chars": 5679,
    "preview": "import widestLine from 'widest-line';\nimport indentString from 'indent-string';\nimport Yoga from 'yoga-layout';\nimport w"
  },
  {
    "path": "src/render-to-string.ts",
    "chars": 5739,
    "preview": "import type {ReactNode} from 'react';\nimport Yoga from 'yoga-layout';\nimport {LegacyRoot} from 'react-reconciler/constan"
  },
  {
    "path": "src/render.ts",
    "chars": 8614,
    "preview": "import {Stream} from 'node:stream';\nimport process from 'node:process';\nimport type {ReactNode} from 'react';\nimport Ink"
  },
  {
    "path": "src/renderer.ts",
    "chars": 1797,
    "preview": "import renderNodeToOutput, {\n\trenderNodeToScreenReaderOutput,\n} from './render-node-to-output.js';\nimport Output from '."
  },
  {
    "path": "src/sanitize-ansi.ts",
    "chars": 902,
    "preview": "import {hasAnsiControlCharacters, tokenizeAnsi} from './ansi-tokenizer.js';\n\nconst sgrParametersRegex = /^[\\d:;]*$/;\n\n//"
  },
  {
    "path": "src/squash-text-nodes.ts",
    "chars": 1431,
    "preview": "import {type DOMElement} from './dom.js';\nimport sanitizeAnsi from './sanitize-ansi.js';\n\n// Squashing text nodes allows"
  },
  {
    "path": "src/styles.ts",
    "chars": 18622,
    "preview": "/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */\nimport {type Boxes, type BoxStyle} from 'cli-boxes';\ni"
  },
  {
    "path": "src/utils.ts",
    "chars": 691,
    "preview": "import terminalSize from 'terminal-size';\n\n/**\nGet the effective terminal dimensions from the given stdout stream.\n\nFall"
  },
  {
    "path": "src/wrap-text.ts",
    "chars": 914,
    "preview": "import wrapAnsi from 'wrap-ansi';\nimport cliTruncate from 'cli-truncate';\nimport {type Styles} from './styles.js';\n\ncons"
  },
  {
    "path": "src/write-synchronized.ts",
    "chars": 356,
    "preview": "import {type Writable} from 'node:stream';\nimport isInCi from 'is-in-ci';\n\nexport const bsu = '\\u001B[?2026h';\nexport co"
  },
  {
    "path": "test/alternate-screen-example.tsx",
    "chars": 2418,
    "preview": "import {spawn as spawnProcess} from 'node:child_process';\nimport * as path from 'node:path';\nimport url from 'node:url';"
  },
  {
    "path": "test/ansi-tokenizer.ts",
    "chars": 8449,
    "preview": "import test from 'ava';\nimport {tokenizeAnsi} from '../src/ansi-tokenizer.js';\n\ntest('tokenize plain text', t => {\n\tt.de"
  },
  {
    "path": "test/background.tsx",
    "chars": 12731,
    "preview": "import React from 'react';\nimport test from 'ava';\nimport chalk from 'chalk';\nimport {render, Box, Text} from '../src/in"
  },
  {
    "path": "test/borders.tsx",
    "chars": 22939,
    "preview": "import React from 'react';\nimport test from 'ava';\nimport boxen from 'boxen';\nimport indentString from 'indent-string';\n"
  },
  {
    "path": "test/components.tsx",
    "chars": 39607,
    "preview": "import EventEmitter from 'node:events';\nimport process from 'node:process';\nimport test from 'ava';\nimport chalk from 'c"
  },
  {
    "path": "test/cursor-helpers.tsx",
    "chars": 3882,
    "preview": "import test from 'ava';\nimport ansiEscapes from 'ansi-escapes';\nimport {\n\tcursorPositionChanged,\n\tbuildCursorSuffix,\n\tbu"
  },
  {
    "path": "test/cursor.tsx",
    "chars": 16591,
    "preview": "import test, {type ExecutionContext} from 'ava';\nimport React, {Suspense, act, useEffect, useState} from 'react';\nimport"
  },
  {
    "path": "test/display.tsx",
    "chars": 977,
    "preview": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text} from '../src/index.js';\nimport {\n\trenderToString,\n"
  },
  {
    "path": "test/errors.tsx",
    "chars": 4108,
    "preview": "import process from 'node:process';\nimport React, {useEffect} from 'react';\nimport test from 'ava';\nimport patchConsole "
  },
  {
    "path": "test/exit.tsx",
    "chars": 4185,
    "preview": "import process from 'node:process';\nimport * as path from 'node:path';\nimport url from 'node:url';\nimport {createRequire"
  },
  {
    "path": "test/fixtures/alternate-screen-full-board-win.tsx",
    "chars": 810,
    "preview": "import {gameReducer} from '../../examples/alternate-screen/alternate-screen.js';\n\nconst boardWidth = 20;\nconst boardHeig"
  },
  {
    "path": "test/fixtures/ci-debug-after-exit.tsx",
    "chars": 241,
    "preview": "import process from 'node:process';\nimport React from 'react';\nimport {render, Text} from '../../src/index.js';\n\nconst a"
  },
  {
    "path": "test/fixtures/ci-debug.tsx",
    "chars": 120,
    "preview": "import React from 'react';\nimport {render, Text} from '../../src/index.js';\n\nrender(<Text>Hello</Text>, {debug: true});\n"
  },
  {
    "path": "test/fixtures/ci.tsx",
    "chars": 939,
    "preview": "import React from 'react';\nimport {render, Static, Text} from '../../src/index.js';\n\ntype TestState = {\n\tcounter: number"
  },
  {
    "path": "test/fixtures/clear.tsx",
    "chars": 256,
    "preview": "import React from 'react';\nimport {Box, Text, render} from '../../src/index.js';\n\nfunction Clear() {\n\treturn (\n\t\t<Box fl"
  },
  {
    "path": "test/fixtures/console.tsx",
    "chars": 363,
    "preview": "import React, {useEffect} from 'react';\nimport {Text, render} from '../../src/index.js';\n\nfunction App() {\n\tuseEffect(()"
  },
  {
    "path": "test/fixtures/erase-with-state-change.tsx",
    "chars": 562,
    "preview": "import process from 'node:process';\nimport React, {useEffect, useState} from 'react';\nimport {Box, Text, render} from '."
  },
  {
    "path": "test/fixtures/erase-with-static.tsx",
    "chars": 453,
    "preview": "import process from 'node:process';\nimport React from 'react';\nimport {Static, Box, Text, render} from '../../src/index."
  },
  {
    "path": "test/fixtures/erase.tsx",
    "chars": 314,
    "preview": "import process from 'node:process';\nimport React from 'react';\nimport {Box, Text, render} from '../../src/index.js';\n\nfu"
  },
  {
    "path": "test/fixtures/exit-double-raw-mode.tsx",
    "chars": 793,
    "preview": "import process from 'node:process';\nimport React from 'react';\nimport {Text, render, useStdin} from '../../src/index.js'"
  },
  {
    "path": "test/fixtures/exit-normally.tsx",
    "chars": 182,
    "preview": "import React from 'react';\nimport {Text, render} from '../../src/index.js';\n\nconst {waitUntilExit} = render(<Text>Hello "
  },
  {
    "path": "test/fixtures/exit-on-exit-with-error-value-property.tsx",
    "chars": 492,
    "preview": "import React, {useEffect} from 'react';\nimport {render, Text, useApp} from '../../src/index.js';\n\nfunction Test() {\n\tcon"
  },
  {
    "path": "test/fixtures/exit-on-exit-with-error.tsx",
    "chars": 839,
    "preview": "import React from 'react';\nimport {render, Text, useApp} from '../../src/index.js';\n\nclass Exit extends React.Component<"
  },
  {
    "path": "test/fixtures/exit-on-exit-with-result.tsx",
    "chars": 373,
    "preview": "import React, {useEffect} from 'react';\nimport {render, Text, useApp} from '../../src/index.js';\n\nfunction Test() {\n\tcon"
  },
  {
    "path": "test/fixtures/exit-on-exit-with-value-object.tsx",
    "chars": 414,
    "preview": "import React, {useEffect} from 'react';\nimport {render, Text, useApp} from '../../src/index.js';\n\nfunction Test() {\n\tcon"
  },
  {
    "path": "test/fixtures/exit-on-exit.tsx",
    "chars": 750,
    "preview": "import React from 'react';\nimport {render, Text, useApp} from '../../src/index.js';\n\nclass Exit extends React.Component<"
  },
  {
    "path": "test/fixtures/exit-on-finish.tsx",
    "chars": 672,
    "preview": "import React from 'react';\nimport {render, Text} from '../../src/index.js';\n\nclass Test extends React.Component<Record<s"
  },
  {
    "path": "test/fixtures/exit-on-unmount.tsx",
    "chars": 656,
    "preview": "import React from 'react';\nimport {render, Text} from '../../src/index.js';\n\nclass Test extends React.Component<Record<s"
  },
  {
    "path": "test/fixtures/exit-raw-on-exit-with-error.tsx",
    "chars": 691,
    "preview": "import React from 'react';\nimport {render, Text, useApp, useStdin} from '../../src/index.js';\n\nclass Exit extends React."
  },
  {
    "path": "test/fixtures/exit-raw-on-exit.tsx",
    "chars": 601,
    "preview": "import React from 'react';\nimport {render, Text, useApp, useStdin} from '../../src/index.js';\n\nclass Exit extends React."
  },
  {
    "path": "test/fixtures/exit-raw-on-unmount.tsx",
    "chars": 526,
    "preview": "import React from 'react';\nimport {render, Text, useStdin} from '../../src/index.js';\n\nclass Exit extends React.Componen"
  },
  {
    "path": "test/fixtures/exit-with-static.tsx",
    "chars": 490,
    "preview": "import React, {useEffect} from 'react';\nimport {render, Static, Text, useApp} from '../../src/index.js';\n\nfunction Test("
  },
  {
    "path": "test/fixtures/exit-with-thrown-error.tsx",
    "chars": 257,
    "preview": "import React from 'react';\nimport {render} from '../../src/index.js';\n\nconst Test = () => {\n\tthrow new Error('errored');"
  },
  {
    "path": "test/fixtures/fullscreen-no-extra-newline.tsx",
    "chars": 768,
    "preview": "import process from 'node:process';\nimport React, {useEffect} from 'react';\nimport {Box, Text, render, useApp} from '../"
  },
  {
    "path": "test/fixtures/issue-442-full-height.tsx",
    "chars": 656,
    "preview": "import process from 'node:process';\nimport React, {useEffect} from 'react';\nimport {Box, Text, render, useApp} from '../"
  },
  {
    "path": "test/fixtures/issue-450-fixture-helpers.tsx",
    "chars": 3349,
    "preview": "import process from 'node:process';\nimport React, {useEffect, useState} from 'react';\nimport {Box, Static, Text, render,"
  },
  {
    "path": "test/fixtures/issue-450-full-height-rerender-with-marker.tsx",
    "chars": 197,
    "preview": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\tcompletionMark"
  },
  {
    "path": "test/fixtures/issue-450-full-height-rerender.tsx",
    "chars": 140,
    "preview": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\theightForFrame"
  },
  {
    "path": "test/fixtures/issue-450-full-height-with-static-rerender.tsx",
    "chars": 166,
    "preview": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\tincludeStaticL"
  },
  {
    "path": "test/fixtures/issue-450-grow-to-fullscreen-rerender.tsx",
    "chars": 248,
    "preview": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\tcompletionMark"
  },
  {
    "path": "test/fixtures/issue-450-grow-to-overflow-rerender.tsx",
    "chars": 224,
    "preview": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\tframeLimit: 1,"
  },
  {
    "path": "test/fixtures/issue-450-height-minus-one-rerender.tsx",
    "chars": 144,
    "preview": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\theightForFrame"
  },
  {
    "path": "test/fixtures/issue-450-initial-fullscreen.tsx",
    "chars": 220,
    "preview": "import {runIssue450InitialFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450InitialFixture({\n\trenderedMarker: "
  },
  {
    "path": "test/fixtures/issue-450-initial-overflow.tsx",
    "chars": 216,
    "preview": "import {runIssue450InitialFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450InitialFixture({\n\trenderedMarker: "
  },
  {
    "path": "test/fixtures/issue-450-shrink-from-fullscreen-rerender.tsx",
    "chars": 184,
    "preview": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\theightForFrame"
  },
  {
    "path": "test/fixtures/issue-450-shrink-from-overflow-rerender.tsx",
    "chars": 190,
    "preview": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\theightForFrame"
  },
  {
    "path": "test/fixtures/issue-450-static-shrink-from-fullscreen-rerender.tsx",
    "chars": 210,
    "preview": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\tincludeStaticL"
  },
  {
    "path": "test/fixtures/issue-725-child-process.tsx",
    "chars": 315,
    "preview": "import React from 'react';\nimport {Text, useStdin, render} from '../../src/index.js';\n\nfunction App() {\n\tconst {isRawMod"
  },
  {
    "path": "test/fixtures/use-input-ctrl-c.tsx",
    "chars": 496,
    "preview": "import process from 'node:process';\nimport React from 'react';\nimport {render, useInput, useApp} from '../../src/index.j"
  },
  {
    "path": "test/fixtures/use-input-discrete-priority.tsx",
    "chars": 1446,
    "preview": "import process from 'node:process';\nimport React, {\n\tuseState,\n\tuseTransition,\n\tuseMemo,\n\tuseEffect,\n\tuseRef,\n} from 're"
  },
  {
    "path": "test/fixtures/use-input-kitty.tsx",
    "chars": 2153,
    "preview": "import process from 'node:process';\nimport React from 'react';\nimport {render, useInput, useApp} from '../../src/index.j"
  },
  {
    "path": "test/fixtures/use-input-many.tsx",
    "chars": 837,
    "preview": "import process from 'node:process';\nimport React, {useEffect} from 'react';\nimport {render, useInput, useApp, Text} from"
  },
  {
    "path": "test/fixtures/use-input-multiple.tsx",
    "chars": 679,
    "preview": "import process from 'node:process';\nimport React, {useState, useCallback, useEffect} from 'react';\nimport {render, useIn"
  },
  {
    "path": "test/fixtures/use-input.tsx",
    "chars": 4074,
    "preview": "import process from 'node:process';\nimport React from 'react';\nimport {render, useInput, useApp} from '../../src/index.j"
  },
  {
    "path": "test/fixtures/use-paste.tsx",
    "chars": 1358,
    "preview": "import process from 'node:process';\nimport React from 'react';\nimport {render, useApp, useInput, usePaste} from '../../s"
  },
  {
    "path": "test/fixtures/use-stdout.tsx",
    "chars": 354,
    "preview": "import React, {useEffect} from 'react';\nimport {render, useStdout, Text} from '../../src/index.js';\n\nfunction WriteToStd"
  },
  {
    "path": "test/flex-align-content.tsx",
    "chars": 3842,
    "preview": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text, render} from '../src/index.js';\nimport {\n\trenderTo"
  },
  {
    "path": "test/flex-align-items.tsx",
    "chars": 2056,
    "preview": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text, Newline} from '../src/index.js';\nimport {renderToS"
  },
  {
    "path": "test/flex-align-self.tsx",
    "chars": 2235,
    "preview": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text, Newline} from '../src/index.js';\nimport {renderToS"
  },
  {
    "path": "test/flex-direction.tsx",
    "chars": 1560,
    "preview": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text} from '../src/index.js';\nimport {\n\trenderToString,\n"
  },
  {
    "path": "test/flex-justify-content.tsx",
    "chars": 3160,
    "preview": "import React from 'react';\nimport test from 'ava';\nimport chalk from 'chalk';\nimport {Box, Text} from '../src/index.js';"
  },
  {
    "path": "test/flex-wrap.tsx",
    "chars": 1405,
    "preview": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text} from '../src/index.js';\nimport {renderToString} fr"
  },
  {
    "path": "test/flex.tsx",
    "chars": 2163,
    "preview": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text} from '../src/index.js';\nimport {renderToString} fr"
  },
  {
    "path": "test/focus.tsx",
    "chars": 15619,
    "preview": "import EventEmitter from 'node:events';\nimport React, {useEffect} from 'react';\nimport delay from 'delay';\nimport test f"
  },
  {
    "path": "test/gap.tsx",
    "chars": 1339,
    "preview": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text} from '../src/index.js';\nimport {\n\trenderToString,\n"
  },
  {
    "path": "test/helpers/create-stdin.ts",
    "chars": 801,
    "preview": "import EventEmitter from 'node:events';\nimport {stub} from 'sinon';\n\nexport const createStdin = (): NodeJS.WriteStream ="
  },
  {
    "path": "test/helpers/create-stdout.ts",
    "chars": 619,
    "preview": "import EventEmitter from 'node:events';\nimport {spy} from 'sinon';\n\n// Fake process.stdout\nexport type FakeStdout = {\n\tg"
  },
  {
    "path": "test/helpers/force-colors.ts",
    "chars": 388,
    "preview": "import chalk, {supportsColor} from 'chalk';\n\n// Force chalk to output colors even in non-TTY environments for testing\nex"
  },
  {
    "path": "test/helpers/render-to-string.ts",
    "chars": 1105,
    "preview": "import {act} from 'react';\nimport {render} from '../../src/index.js';\nimport createStdout from './create-stdout.js';\n\nty"
  },
  {
    "path": "test/helpers/run.ts",
    "chars": 1391,
    "preview": "import process from 'node:process';\nimport {createRequire} from 'node:module';\nimport path from 'node:path';\nimport url "
  },
  {
    "path": "test/helpers/term.ts",
    "chars": 1853,
    "preview": "import process from 'node:process';\nimport {createRequire} from 'node:module';\nimport path from 'node:path';\nimport url "
  },
  {
    "path": "test/helpers/test-renderer.ts",
    "chars": 2114,
    "preview": "import {act} from 'react';\nimport {render, type Instance} from '../../src/index.js';\nimport createStdout from './create-"
  },
  {
    "path": "test/hooks-use-input-kitty.tsx",
    "chars": 4210,
    "preview": "import test from 'ava';\nimport term from './helpers/term.js';\n\ntest.serial('useInput - handle kitty protocol super modif"
  },
  {
    "path": "test/hooks-use-input-navigation.tsx",
    "chars": 3526,
    "preview": "import test from 'ava';\nimport term from './helpers/term.js';\n\ntest.serial('useInput - handle up arrow', async t => {\n\tc"
  },
  {
    "path": "test/hooks-use-input.tsx",
    "chars": 4569,
    "preview": "import test from 'ava';\nimport term from './helpers/term.js';\n\ntest.serial(\n\t'useInput - discrete priority keeps states "
  }
]

// ... and 25 more files (download for full content)

About this extraction

This page contains the full source code of the vadimdemedes/ink GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 225 files (746.9 KB), approximately 214.7k tokens, and a symbol index with 507 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!