[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = tab\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.yml]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\non: [push, pull_request]\njobs:\n  test:\n    name: Node.js ${{ matrix.node_version }}\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node_version:\n          - 24\n          - 22\n          - 20\n    steps:\n      - uses: actions/checkout@v6\n      - name: Use Node.js ${{ matrix.node_version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node_version }}\n      - run: npm install\n      - run: npm test -- --serial\n        env:\n          FORCE_COLOR: true\n          CI: false\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n/build\n"
  },
  {
    "path": ".npmrc",
    "content": "package-lock=false\n"
  },
  {
    "path": "benchmark/simple/index.ts",
    "content": "import './simple.js';\n"
  },
  {
    "path": "benchmark/simple/simple.tsx",
    "content": "import React from 'react';\nimport {render, Box, Text} from '../../src/index.js';\n\nfunction App() {\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<Text underline bold color=\"red\">\n\t\t\t\t{/* eslint-disable-next-line react/jsx-curly-brace-presence */}\n\t\t\t\t{'Hello World'}\n\t\t\t</Text>\n\n\t\t\t<Box marginTop={1} width={60}>\n\t\t\t\t<Text>\n\t\t\t\t\tCupcake ipsum dolor sit amet candy candy. Sesame snaps cookie I love\n\t\t\t\t\ttootsie roll apple pie bonbon wafer. Caramels sesame snaps icing\n\t\t\t\t\tcotton candy I love cookie sweet roll. I love bonbon sweet.\n\t\t\t\t</Text>\n\t\t\t</Box>\n\n\t\t\t<Box marginTop={1} flexDirection=\"column\">\n\t\t\t\t<Text backgroundColor=\"white\" color=\"black\">\n\t\t\t\t\tColors:\n\t\t\t\t</Text>\n\n\t\t\t\t<Box flexDirection=\"column\" paddingLeft={1}>\n\t\t\t\t\t<Text>\n\t\t\t\t\t\t- <Text color=\"red\">Red</Text>\n\t\t\t\t\t</Text>\n\t\t\t\t\t<Text>\n\t\t\t\t\t\t- <Text color=\"blue\">Blue</Text>\n\t\t\t\t\t</Text>\n\t\t\t\t\t<Text>\n\t\t\t\t\t\t- <Text color=\"green\">Green</Text>\n\t\t\t\t\t</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nconst {rerender} = render(<App />);\n\nfor (let index = 0; index < 100_000; index++) {\n\trerender(<App />);\n}\n"
  },
  {
    "path": "benchmark/static/index.ts",
    "content": "import './static.js';\n"
  },
  {
    "path": "benchmark/static/static.tsx",
    "content": "import React from 'react';\nimport {render, Box, Text, Static} from '../../src/index.js';\n\nfunction App() {\n\tconst [items, setItems] = React.useState<\n\t\tArray<{\n\t\t\tid: number;\n\t\t}>\n\t>([]);\n\tconst itemCountReference = React.useRef(0);\n\n\tReact.useEffect(() => {\n\t\tlet timer: NodeJS.Timeout | undefined;\n\n\t\tconst run = () => {\n\t\t\tif (itemCountReference.current++ > 1000) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetItems(previousItems => [\n\t\t\t\t...previousItems,\n\t\t\t\t{\n\t\t\t\t\tid: previousItems.length,\n\t\t\t\t},\n\t\t\t]);\n\n\t\t\ttimer = setTimeout(run, 10);\n\t\t};\n\n\t\trun();\n\n\t\treturn () => {\n\t\t\tclearTimeout(timer);\n\t\t};\n\t}, []);\n\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Static items={items}>\n\t\t\t\t{(item, index) => (\n\t\t\t\t\t<Box key={item.id} padding={1} flexDirection=\"column\">\n\t\t\t\t\t\t<Text color=\"green\">Item #{index}</Text>\n\t\t\t\t\t\t<Text>Item content</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t)}\n\t\t\t</Static>\n\n\t\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t\t<Text underline bold color=\"red\">\n\t\t\t\t\t{/* eslint-disable-next-line react/jsx-curly-brace-presence */}\n\t\t\t\t\t{'Hello World'}\n\t\t\t\t</Text>\n\n\t\t\t\t<Text>Rendered: {items.length}</Text>\n\n\t\t\t\t<Box marginTop={1} width={60}>\n\t\t\t\t\t<Text>\n\t\t\t\t\t\tCupcake ipsum dolor sit amet candy candy. Sesame snaps cookie I love\n\t\t\t\t\t\ttootsie roll apple pie bonbon wafer. Caramels sesame snaps icing\n\t\t\t\t\t\tcotton candy I love cookie sweet roll. I love bonbon sweet.\n\t\t\t\t\t</Text>\n\t\t\t\t</Box>\n\n\t\t\t\t<Box marginTop={1} flexDirection=\"column\">\n\t\t\t\t\t<Text backgroundColor=\"white\" color=\"black\">\n\t\t\t\t\t\tColors:\n\t\t\t\t\t</Text>\n\n\t\t\t\t\t<Box flexDirection=\"column\" paddingLeft={1}>\n\t\t\t\t\t\t<Text>\n\t\t\t\t\t\t\t- <Text color=\"red\">Red</Text>\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t<Text>\n\t\t\t\t\t\t\t- <Text color=\"blue\">Blue</Text>\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t<Text>\n\t\t\t\t\t\t\t- <Text color=\"green\">Green</Text>\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nrender(<App />);\n"
  },
  {
    "path": "examples/alternate-screen/alternate-screen.tsx",
    "content": "import React, {useReducer, useEffect, useRef, useCallback} from 'react';\nimport {\n\trender,\n\tText,\n\tBox,\n\tuseInput,\n\tuseApp,\n\tuseWindowSize,\n} from '../../src/index.js';\n\ntype Point = {\n\tx: number;\n\ty: number;\n};\n\ntype Direction = 'up' | 'down' | 'left' | 'right';\n\ntype GameState = {\n\tsnake: Point[];\n\tfood: Point;\n\tscore: number;\n\tgameOver: boolean;\n\twon: boolean;\n\tframe: number;\n};\n\ntype Action = {type: 'tick'; direction: Direction} | {type: 'restart'};\n\nconst headCharacter = '🦄';\nconst bodyCharacter = '✨';\nconst foodCharacter = '🌈';\nconst emptyCell = '  ';\nconst tickMs = 150;\n\nconst boardWidth = 20;\nconst boardHeight = 15;\n\nconst opposites: Record<Direction, Direction> = {\n\tup: 'down',\n\tdown: 'up',\n\tleft: 'right',\n\tright: 'left',\n};\n\nconst offsets: Record<Direction, Point> = {\n\tup: {x: 0, y: -1},\n\tdown: {x: 0, y: 1},\n\tleft: {x: -1, y: 0},\n\tright: {x: 1, y: 0},\n};\n\nconst rainbowColors = [\n\t'red',\n\t'#FF7F00',\n\t'yellow',\n\t'green',\n\t'cyan',\n\t'blue',\n\t'magenta',\n] as const;\n\nconst borderH = '─'.repeat(boardWidth * 2);\nconst borderTop = `┌${borderH}┐`;\nconst borderBottom = `└${borderH}┘`;\nconst boardWidthChars = boardWidth * 2 + 2;\n\nconst initialSnake: Point[] = [\n\t{x: 10, y: 7},\n\t{x: 9, y: 7},\n\t{x: 8, y: 7},\n];\n\nfunction randomPosition(exclude: Point[]): Point {\n\tlet point = {\n\t\tx: 0,\n\t\ty: 0,\n\t};\n\tlet isExcluded = true;\n\n\twhile (isExcluded) {\n\t\tpoint = {\n\t\t\tx: Math.floor(Math.random() * boardWidth),\n\t\t\ty: Math.floor(Math.random() * boardHeight),\n\t\t};\n\n\t\tisExcluded = false;\n\t\tfor (const segment of exclude) {\n\t\t\tif (segment.x === point.x && segment.y === point.y) {\n\t\t\t\tisExcluded = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn point;\n}\n\nfunction createInitialState(): GameState {\n\treturn {\n\t\tsnake: initialSnake,\n\t\tfood: randomPosition(initialSnake),\n\t\tscore: 0,\n\t\tgameOver: false,\n\t\twon: false,\n\t\tframe: 0,\n\t};\n}\n\nexport function gameReducer(state: GameState, action: Action): GameState {\n\tif (action.type === 'restart') {\n\t\treturn createInitialState();\n\t}\n\n\tif (state.gameOver) {\n\t\treturn state;\n\t}\n\n\tconst head = state.snake[0]!;\n\tconst offset = offsets[action.direction];\n\tconst newHead: Point = {x: head.x + offset.x, y: head.y + offset.y};\n\n\t// Wall collision\n\tif (\n\t\tnewHead.x < 0 ||\n\t\tnewHead.x >= boardWidth ||\n\t\tnewHead.y < 0 ||\n\t\tnewHead.y >= boardHeight\n\t) {\n\t\treturn {...state, gameOver: true, won: false};\n\t}\n\n\tconst ateFood = newHead.x === state.food.x && newHead.y === state.food.y;\n\tconst collisionSegments = ateFood ? state.snake : state.snake.slice(0, -1);\n\n\tif (\n\t\tcollisionSegments.some(\n\t\t\tsegment => segment.x === newHead.x && segment.y === newHead.y,\n\t\t)\n\t) {\n\t\treturn {...state, gameOver: true, won: false};\n\t}\n\n\tconst newSnake = [newHead, ...state.snake];\n\n\tif (!ateFood) {\n\t\tnewSnake.pop();\n\t}\n\n\tif (ateFood && newSnake.length === boardWidth * boardHeight) {\n\t\treturn {\n\t\t\tsnake: newSnake,\n\t\t\tfood: state.food,\n\t\t\tscore: state.score + 1,\n\t\t\tgameOver: true,\n\t\t\twon: true,\n\t\t\tframe: state.frame + 1,\n\t\t};\n\t}\n\n\treturn {\n\t\tsnake: newSnake,\n\t\tfood: ateFood ? randomPosition(newSnake) : state.food,\n\t\tscore: state.score + (ateFood ? 1 : 0),\n\t\tgameOver: false,\n\t\twon: false,\n\t\tframe: state.frame + 1,\n\t};\n}\n\nfunction buildBoard(snake: Point[], food: Point): string {\n\tconst headKey = `${snake[0]!.x},${snake[0]!.y}`;\n\tconst snakeSet = new Set(snake.map(segment => `${segment.x},${segment.y}`));\n\n\tconst rows: string[] = [borderTop];\n\tfor (let y = 0; y < boardHeight; y++) {\n\t\tlet row = '│';\n\t\tfor (let x = 0; x < boardWidth; x++) {\n\t\t\tconst key = `${x},${y}`;\n\t\t\tif (key === headKey) {\n\t\t\t\trow += headCharacter;\n\t\t\t} else if (snakeSet.has(key)) {\n\t\t\t\trow += bodyCharacter;\n\t\t\t} else if (food.x === x && food.y === y) {\n\t\t\t\trow += foodCharacter;\n\t\t\t} else {\n\t\t\t\trow += emptyCell;\n\t\t\t}\n\t\t}\n\n\t\trow += '│';\n\t\trows.push(row);\n\t}\n\n\trows.push(borderBottom);\n\treturn rows.join('\\n');\n}\n\nfunction SnakeGame() {\n\tconst {exit} = useApp();\n\tconst {columns} = useWindowSize();\n\tconst [game, dispatch] = useReducer(\n\t\tgameReducer,\n\t\tundefined,\n\t\tcreateInitialState,\n\t);\n\tconst directionReference = useRef<Direction>('right');\n\n\tconst tick = useCallback(() => {\n\t\tdispatch({type: 'tick', direction: directionReference.current});\n\t}, []);\n\n\tuseEffect(() => {\n\t\tconst timer = setInterval(tick, tickMs);\n\t\treturn () => {\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, [tick]);\n\n\tuseInput((input, key) => {\n\t\tif (input === 'q') {\n\t\t\texit();\n\t\t}\n\n\t\tif (game.gameOver && input === 'r') {\n\t\t\tdirectionReference.current = 'right';\n\t\t\tdispatch({type: 'restart'});\n\t\t\treturn;\n\t\t}\n\n\t\tif (game.gameOver) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst {current} = directionReference;\n\t\tif (key.upArrow && current !== 'down') {\n\t\t\tdirectionReference.current = 'up';\n\t\t} else if (key.downArrow && current !== 'up') {\n\t\t\tdirectionReference.current = 'down';\n\t\t} else if (key.leftArrow && current !== 'right') {\n\t\t\tdirectionReference.current = 'left';\n\t\t} else if (key.rightArrow && current !== 'left') {\n\t\t\tdirectionReference.current = 'right';\n\t\t}\n\t});\n\n\tconst titleColor = rainbowColors[game.frame % rainbowColors.length]!;\n\tconst board = buildBoard(game.snake, game.food);\n\tconst marginLeft = Math.max(Math.floor((columns - boardWidthChars) / 2), 0);\n\n\treturn (\n\t\t<Box flexDirection=\"column\" paddingY={1}>\n\t\t\t<Box justifyContent=\"center\">\n\t\t\t\t<Text bold color={titleColor}>\n\t\t\t\t\t🦄 Unicorn Snake 🦄\n\t\t\t\t</Text>\n\t\t\t</Box>\n\n\t\t\t<Box justifyContent=\"center\" marginTop={1}>\n\t\t\t\t<Text bold color=\"yellow\">\n\t\t\t\t\tScore: {game.score}\n\t\t\t\t</Text>\n\t\t\t</Box>\n\n\t\t\t<Box marginLeft={marginLeft} marginTop={1}>\n\t\t\t\t<Text>{board}</Text>\n\t\t\t</Box>\n\n\t\t\t{game.gameOver ? (\n\t\t\t\t<Box justifyContent=\"center\" marginTop={1}>\n\t\t\t\t\t<Text bold color=\"red\">\n\t\t\t\t\t\t{game.won ? 'You Win!' : 'Game Over!'}{' '}\n\t\t\t\t\t</Text>\n\t\t\t\t\t<Text dimColor>r: restart | q: quit</Text>\n\t\t\t\t</Box>\n\t\t\t) : (\n\t\t\t\t<Box justifyContent=\"center\" marginTop={1}>\n\t\t\t\t\t<Text dimColor>\n\t\t\t\t\t\tArrow keys: move | Eat {foodCharacter} to grow | q: quit\n\t\t\t\t\t</Text>\n\t\t\t\t</Box>\n\t\t\t)}\n\t\t</Box>\n\t);\n}\n\nexport function runAlternateScreenExample() {\n\trender(<SnakeGame />, {alternateScreen: true});\n}\n"
  },
  {
    "path": "examples/alternate-screen/index.ts",
    "content": "import {runAlternateScreenExample} from './alternate-screen.js';\n\nrunAlternateScreenExample();\n"
  },
  {
    "path": "examples/aria/aria.tsx",
    "content": "import React, {useState} from 'react';\nimport {render, Text, Box, useInput} from '../../src/index.js';\n\nfunction AriaExample() {\n\tconst [checked, setChecked] = useState(false);\n\n\tuseInput(key => {\n\t\tif (key === ' ') {\n\t\t\tsetChecked(!checked);\n\t\t}\n\t});\n\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>\n\t\t\t\tPress spacebar to toggle the checkbox. This example is best experienced\n\t\t\t\twith a screen reader.\n\t\t\t</Text>\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Box aria-role=\"checkbox\" aria-state={{checked}}>\n\t\t\t\t\t<Text>{checked ? '[x]' : '[ ]'}</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text aria-hidden=\"true\">This text is hidden from screen readers.</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nrender(<AriaExample />);\n"
  },
  {
    "path": "examples/aria/index.ts",
    "content": "import './aria.js';\n"
  },
  {
    "path": "examples/borders/borders.tsx",
    "content": "import React from 'react';\nimport {render, Box, Text} from '../../src/index.js';\n\nfunction Borders() {\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={2}>\n\t\t\t<Box>\n\t\t\t\t<Box borderStyle=\"single\" marginRight={2}>\n\t\t\t\t\t<Text>single</Text>\n\t\t\t\t</Box>\n\n\t\t\t\t<Box borderStyle=\"double\" marginRight={2}>\n\t\t\t\t\t<Text>double</Text>\n\t\t\t\t</Box>\n\n\t\t\t\t<Box borderStyle=\"round\" marginRight={2}>\n\t\t\t\t\t<Text>round</Text>\n\t\t\t\t</Box>\n\n\t\t\t\t<Box borderStyle=\"bold\">\n\t\t\t\t\t<Text>bold</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Box borderStyle=\"singleDouble\" marginRight={2}>\n\t\t\t\t\t<Text>singleDouble</Text>\n\t\t\t\t</Box>\n\n\t\t\t\t<Box borderStyle=\"doubleSingle\" marginRight={2}>\n\t\t\t\t\t<Text>doubleSingle</Text>\n\t\t\t\t</Box>\n\n\t\t\t\t<Box borderStyle=\"classic\">\n\t\t\t\t\t<Text>classic</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nrender(<Borders />);\n"
  },
  {
    "path": "examples/borders/index.ts",
    "content": "import './borders.js';\n"
  },
  {
    "path": "examples/box-backgrounds/box-backgrounds.tsx",
    "content": "import React from 'react';\nimport {Box, Text} from '../../src/index.js';\n\nfunction BoxBackgrounds() {\n\treturn (\n\t\t<Box flexDirection=\"column\" gap={1}>\n\t\t\t<Text bold>Box Background Examples:</Text>\n\n\t\t\t<Box>\n\t\t\t\t<Text>1. Standard red background (10x3):</Text>\n\t\t\t</Box>\n\t\t\t<Box backgroundColor=\"red\" width={10} height={3} alignSelf=\"flex-start\">\n\t\t\t\t<Text>Hello</Text>\n\t\t\t</Box>\n\n\t\t\t<Box>\n\t\t\t\t<Text>2. Blue background with border (12x4):</Text>\n\t\t\t</Box>\n\t\t\t<Box\n\t\t\t\tbackgroundColor=\"blue\"\n\t\t\t\tborderStyle=\"round\"\n\t\t\t\twidth={12}\n\t\t\t\theight={4}\n\t\t\t\talignSelf=\"flex-start\"\n\t\t\t>\n\t\t\t\t<Text>Border</Text>\n\t\t\t</Box>\n\n\t\t\t<Box>\n\t\t\t\t<Text>3. Green background with padding (14x4):</Text>\n\t\t\t</Box>\n\t\t\t<Box\n\t\t\t\tbackgroundColor=\"green\"\n\t\t\t\tpadding={1}\n\t\t\t\twidth={14}\n\t\t\t\theight={4}\n\t\t\t\talignSelf=\"flex-start\"\n\t\t\t>\n\t\t\t\t<Text>Padding</Text>\n\t\t\t</Box>\n\n\t\t\t<Box>\n\t\t\t\t<Text>4. Yellow background with center alignment (16x3):</Text>\n\t\t\t</Box>\n\t\t\t<Box\n\t\t\t\tbackgroundColor=\"yellow\"\n\t\t\t\twidth={16}\n\t\t\t\theight={3}\n\t\t\t\tjustifyContent=\"center\"\n\t\t\t\talignSelf=\"flex-start\"\n\t\t\t>\n\t\t\t\t<Text>Centered</Text>\n\t\t\t</Box>\n\n\t\t\t<Box>\n\t\t\t\t<Text>5. Magenta background, column layout (12x5):</Text>\n\t\t\t</Box>\n\t\t\t<Box\n\t\t\t\tbackgroundColor=\"magenta\"\n\t\t\t\tflexDirection=\"column\"\n\t\t\t\twidth={12}\n\t\t\t\theight={5}\n\t\t\t\talignSelf=\"flex-start\"\n\t\t\t>\n\t\t\t\t<Text>Line 1</Text>\n\t\t\t\t<Text>Line 2</Text>\n\t\t\t</Box>\n\n\t\t\t<Box>\n\t\t\t\t<Text>6. Hex color background #FF8800 (10x3):</Text>\n\t\t\t</Box>\n\t\t\t<Box\n\t\t\t\tbackgroundColor=\"#FF8800\"\n\t\t\t\twidth={10}\n\t\t\t\theight={3}\n\t\t\t\talignSelf=\"flex-start\"\n\t\t\t>\n\t\t\t\t<Text>Hex</Text>\n\t\t\t</Box>\n\n\t\t\t<Box>\n\t\t\t\t<Text>7. RGB background rgb(0,255,0) (10x3):</Text>\n\t\t\t</Box>\n\t\t\t<Box\n\t\t\t\tbackgroundColor=\"rgb(0,255,0)\"\n\t\t\t\twidth={10}\n\t\t\t\theight={3}\n\t\t\t\talignSelf=\"flex-start\"\n\t\t\t>\n\t\t\t\t<Text>RGB</Text>\n\t\t\t</Box>\n\n\t\t\t<Box>\n\t\t\t\t<Text>8. Text inheritance test:</Text>\n\t\t\t</Box>\n\t\t\t<Box backgroundColor=\"cyan\" alignSelf=\"flex-start\">\n\t\t\t\t<Text>Inherited </Text>\n\t\t\t\t<Text backgroundColor=\"red\">Override </Text>\n\t\t\t\t<Text>Back to inherited</Text>\n\t\t\t</Box>\n\n\t\t\t<Box>\n\t\t\t\t<Text>9. Nested background inheritance:</Text>\n\t\t\t</Box>\n\t\t\t<Box backgroundColor=\"blue\" alignSelf=\"flex-start\">\n\t\t\t\t<Text>Outer: </Text>\n\t\t\t\t<Box backgroundColor=\"yellow\">\n\t\t\t\t\t<Text>Inner: </Text>\n\t\t\t\t\t<Text backgroundColor=\"red\">Deep</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text>Press Ctrl+C to exit</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nexport default BoxBackgrounds;\n"
  },
  {
    "path": "examples/box-backgrounds/index.ts",
    "content": "#!/usr/bin/env node\nimport React from 'react';\nimport {render} from '../../src/index.js';\nimport BoxBackgrounds from './box-backgrounds.js';\n\nrender(React.createElement(BoxBackgrounds));\n"
  },
  {
    "path": "examples/chat/chat.tsx",
    "content": "import React, {useState} from 'react';\nimport {render, Text, Box, useInput} from '../../src/index.js';\n\nlet messageId = 0;\n\nfunction ChatApp() {\n\tconst [input, setInput] = useState('');\n\n\tconst [messages, setMessages] = useState<\n\t\tArray<{\n\t\t\tid: number;\n\t\t\ttext: string;\n\t\t}>\n\t>([]);\n\n\tuseInput((character, key) => {\n\t\tif (key.return) {\n\t\t\tif (input) {\n\t\t\t\tsetMessages(previousMessages => [\n\t\t\t\t\t...previousMessages,\n\t\t\t\t\t{\n\t\t\t\t\t\tid: messageId++,\n\t\t\t\t\t\ttext: `User: ${input}`,\n\t\t\t\t\t},\n\t\t\t\t]);\n\t\t\t\tsetInput('');\n\t\t\t}\n\t\t} else if (key.backspace || key.delete) {\n\t\t\tsetInput(currentInput => currentInput.slice(0, -1));\n\t\t} else {\n\t\t\tsetInput(currentInput => currentInput + character);\n\t\t}\n\t});\n\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t{messages.map(message => (\n\t\t\t\t\t<Text key={message.id}>{message.text}</Text>\n\t\t\t\t))}\n\t\t\t</Box>\n\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text>Enter your message: {input}</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nrender(<ChatApp />);\n"
  },
  {
    "path": "examples/chat/index.ts",
    "content": "import './chat.js';\n"
  },
  {
    "path": "examples/concurrent-suspense/concurrent-suspense.tsx",
    "content": "import React, {Suspense, useState} from 'react';\nimport {render, Box, Text} from '../../src/index.js';\n\n// Simulated async data fetching with cache\nconst cache = new Map<\n\tstring,\n\t{status: string; data?: string; promise?: Promise<void>}\n>();\n\nfunction fetchData(key: string, delay: number): string {\n\tconst cached = cache.get(key);\n\n\tif (cached?.status === 'resolved') {\n\t\treturn cached.data!;\n\t}\n\n\tif (cached?.status === 'pending') {\n\t\t// eslint-disable-next-line @typescript-eslint/only-throw-error\n\t\tthrow cached.promise;\n\t}\n\n\t// Start fetching\n\tconst promise = new Promise<void>(resolve => {\n\t\tsetTimeout(() => {\n\t\t\tcache.set(key, {\n\t\t\t\tstatus: 'resolved',\n\t\t\t\tdata: `Data for \"${key}\" (fetched in ${delay}ms)`,\n\t\t\t});\n\t\t\tresolve();\n\t\t}, delay);\n\t});\n\n\tcache.set(key, {status: 'pending', promise});\n\t// eslint-disable-next-line @typescript-eslint/only-throw-error\n\tthrow promise;\n}\n\n// Component that suspends while fetching\nfunction DataItem({\n\tname,\n\tdelay,\n}: {\n\treadonly name: string;\n\treadonly delay: number;\n}) {\n\tconst data = fetchData(name, delay);\n\treturn (\n\t\t<Box marginLeft={2}>\n\t\t\t<Text color=\"green\">{data}</Text>\n\t\t</Box>\n\t);\n}\n\n// Loading fallback\nfunction Loading({message}: {readonly message: string}) {\n\treturn (\n\t\t<Box marginLeft={2}>\n\t\t\t<Text color=\"yellow\">{message}</Text>\n\t\t</Box>\n\t);\n}\n\n// Main app demonstrating concurrent suspense\nfunction App() {\n\tconst [showMore, setShowMore] = useState(false);\n\n\t// Auto-trigger \"show more\" after 2 seconds\n\tReact.useEffect(() => {\n\t\tconst timer = setTimeout(() => {\n\t\t\tsetShowMore(true);\n\t\t}, 2000);\n\t\treturn () => {\n\t\t\tclearTimeout(timer);\n\t\t};\n\t}, []);\n\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text bold underline>\n\t\t\t\tConcurrent Suspense Demo\n\t\t\t</Text>\n\t\t\t<Text dimColor>\n\t\t\t\t(With concurrent: true, Suspense re-renders automatically)\n\t\t\t</Text>\n\t\t\t<Box marginTop={1} />\n\n\t\t\t<Text>Fast data (200ms):</Text>\n\t\t\t<Suspense fallback={<Loading message=\"Loading fast data...\" />}>\n\t\t\t\t<DataItem name=\"fast\" delay={200} />\n\t\t\t</Suspense>\n\n\t\t\t<Box marginTop={1} />\n\n\t\t\t<Text>Medium data (800ms):</Text>\n\t\t\t<Suspense fallback={<Loading message=\"Loading medium data...\" />}>\n\t\t\t\t<DataItem name=\"medium\" delay={800} />\n\t\t\t</Suspense>\n\n\t\t\t<Box marginTop={1} />\n\n\t\t\t<Text>Slow data (1500ms):</Text>\n\t\t\t<Suspense fallback={<Loading message=\"Loading slow data...\" />}>\n\t\t\t\t<DataItem name=\"slow\" delay={1500} />\n\t\t\t</Suspense>\n\n\t\t\t{showMore ? (\n\t\t\t\t<>\n\t\t\t\t\t<Box marginTop={1} />\n\t\t\t\t\t<Text>Dynamically added (500ms):</Text>\n\t\t\t\t\t<Suspense fallback={<Loading message=\"Loading dynamic data...\" />}>\n\t\t\t\t\t\t<DataItem name=\"dynamic\" delay={500} />\n\t\t\t\t\t</Suspense>\n\t\t\t\t</>\n\t\t\t) : null}\n\t\t</Box>\n\t);\n}\n\n// Render with concurrent mode enabled\nrender(<App />, {concurrent: true});\n"
  },
  {
    "path": "examples/concurrent-suspense/index.ts",
    "content": "import './concurrent-suspense.js';\n"
  },
  {
    "path": "examples/counter/counter.tsx",
    "content": "import React from 'react';\nimport {render, Text} from '../../src/index.js';\n\nfunction Counter() {\n\tconst [counter, setCounter] = React.useState(0);\n\n\tReact.useEffect(() => {\n\t\tconst timer = setInterval(() => {\n\t\t\tsetCounter(prevCounter => prevCounter + 1); // eslint-disable-line unicorn/prevent-abbreviations\n\t\t}, 100);\n\n\t\treturn () => {\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, []);\n\n\treturn <Text color=\"green\">{counter} tests passed</Text>;\n}\n\nrender(<Counter />);\n"
  },
  {
    "path": "examples/counter/index.ts",
    "content": "import './counter.js';\n"
  },
  {
    "path": "examples/cursor-ime/cursor-ime.tsx",
    "content": "import React, {useState} from 'react';\nimport stringWidth from 'string-width';\nimport {render, Box, Text, useInput, useCursor} from '../../src/index.js';\n\nfunction App() {\n\tconst [text, setText] = useState('');\n\tconst {setCursorPosition} = useCursor();\n\n\tuseInput((input, key) => {\n\t\tif (key.backspace || key.delete) {\n\t\t\tsetText(previous => previous.slice(0, -1));\n\t\t\treturn;\n\t\t}\n\n\t\tif (!key.ctrl && !key.meta && input) {\n\t\t\tsetText(previous => previous + input);\n\t\t}\n\t});\n\n\t// Use stringWidth for correct cursor position with wide characters (Korean, CJK, emoji)\n\tconst prompt = '> ';\n\tsetCursorPosition({x: stringWidth(prompt + text), y: 1});\n\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>Type Korean (Ctrl+C to exit):</Text>\n\t\t\t<Text>\n\t\t\t\t{prompt}\n\t\t\t\t{text}\n\t\t\t</Text>\n\t\t</Box>\n\t);\n}\n\nrender(<App />);\n"
  },
  {
    "path": "examples/cursor-ime/index.ts",
    "content": "import './cursor-ime.js';\n"
  },
  {
    "path": "examples/incremental-rendering/incremental-rendering.tsx",
    "content": "import React, {useState, useEffect} from 'react';\nimport {\n\trender,\n\tText,\n\tBox,\n\tuseInput,\n\tuseWindowSize,\n\tuseApp,\n} from '../../src/index.js';\n\nconst rows = [\n\t'Server Authentication Module - Handles JWT token validation, OAuth2 flows, and session management across distributed systems',\n\t'Database Connection Pool - Maintains persistent connections to PostgreSQL cluster with automatic failover and load balancing',\n\t'API Gateway Service - Routes incoming HTTP requests to microservices with rate limiting and request transformation',\n\t'User Profile Manager - Caches user data in Redis with write-through policy and invalidation strategies',\n\t'Payment Processing Engine - Integrates with Stripe, PayPal, and Square APIs for transaction processing',\n\t'Email Notification Queue - Processes outbound emails through SendGrid with retry logic and delivery tracking',\n\t'File Storage Handler - Manages S3 bucket operations with multipart uploads and CDN integration',\n\t'Search Indexer Service - Maintains Elasticsearch indices with real-time document updates and reindexing',\n\t'Metrics Aggregation Pipeline - Collects and processes telemetry data for Prometheus and Grafana dashboards',\n\t'WebSocket Connection Manager - Handles real-time bidirectional communication for chat and notifications',\n\t'Cache Invalidation Service - Coordinates distributed cache updates across Redis cluster nodes',\n\t'Background Job Processor - Executes async tasks via RabbitMQ with dead letter queue handling',\n\t'Session Store Manager - Persists user sessions in DynamoDB with TTL and cross-region replication',\n\t'Rate Limiter Module - Enforces API quotas using token bucket algorithm with Redis backend',\n\t'Content Delivery Network - Serves static assets through Cloudflare with edge caching and GZIP compression',\n\t'Logging Aggregator - Streams application logs to ELK stack with structured JSON formatting',\n\t'Health Check Monitor - Performs periodic service health checks with circuit breaker pattern implementation',\n\t'Configuration Manager - Loads environment-specific settings from Consul with hot reload capability',\n\t'Security Scanner Service - Runs automated vulnerability scans and dependency checks on deployed applications',\n\t'Backup Orchestrator - Schedules and executes automated database backups with encryption and versioning',\n\t'Load Balancer Controller - Manages NGINX upstream servers with health-based traffic distribution',\n\t'Container Orchestration - Coordinates Docker container lifecycle via Kubernetes with auto-scaling policies',\n\t'Message Bus Coordinator - Routes events through Apache Kafka topics with guaranteed delivery semantics',\n\t'Analytics Data Warehouse - Aggregates business metrics in Snowflake with incremental ETL processes',\n\t'API Documentation Service - Generates and serves OpenAPI specs with interactive Swagger UI',\n\t'Feature Flag Manager - Controls feature rollouts using LaunchDarkly with user targeting and percentage rollouts',\n\t'Audit Trail Logger - Records all user actions and system events for compliance and security analysis',\n\t'Image Processing Pipeline - Resizes and optimizes uploaded images using Sharp with multiple format outputs',\n\t'Geolocation Service - Resolves IP addresses to geographic coordinates using MaxMind GeoIP2 database',\n\t'Recommendation Engine - Generates personalized content suggestions using collaborative filtering algorithms',\n];\n\nconst generateLogLine = (index: number, value: number) => {\n\tconst timestamp = new Date().toLocaleTimeString();\n\tconst actions = [\n\t\t'PROCESSING',\n\t\t'COMPLETED',\n\t\t'UPDATING',\n\t\t'SYNCING',\n\t\t'VALIDATING',\n\t\t'EXECUTING',\n\t];\n\tconst action = actions[Math.floor(Math.random() * actions.length)];\n\treturn `[${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)}%`;\n};\n\nfunction IncrementalRendering() {\n\tconst {exit} = useApp();\n\tconst {rows: terminalHeight} = useWindowSize();\n\n\t// Calculate available space for dynamic content\n\t// Header box: ~9 lines (border + content)\n\t// Logs box: variable (border + title + log lines)\n\t// Services box: variable (border + title + services)\n\t// Footer box: ~3 lines\n\t// Margins: ~3 lines\n\t// Total fixed: ~15 lines, so available = terminalHeight - 15\n\tconst availableLines = Math.max(terminalHeight - 15, 10);\n\n\t// Split available space: ~30% for logs, ~70% for services\n\tconst logLineCount = Math.max(Math.floor(availableLines * 0.3), 3);\n\tconst serviceCount = Math.min(\n\t\tMath.max(Math.floor(availableLines * 0.7), 5),\n\t\trows.length,\n\t);\n\n\tconst [selectedIndex, setSelectedIndex] = useState(0);\n\tconst [timestamp, setTimestamp] = useState(new Date().toLocaleTimeString());\n\tconst [counter, setCounter] = useState(0);\n\tconst [fps, setFps] = useState(0);\n\tconst [progress1, setProgress1] = useState(0);\n\tconst [progress2, setProgress2] = useState(0);\n\tconst [progress3, setProgress3] = useState(0);\n\tconst [randomValue, setRandomValue] = useState(0);\n\tconst [logLines, setLogLines] = useState(\n\t\tArray.from({length: logLineCount}, (_, i) => generateLogLine(i, 0)),\n\t);\n\n\t// Update timestamp and counter every second to show live updates\n\tuseEffect(() => {\n\t\tconst timer = setInterval(() => {\n\t\t\tsetTimestamp(new Date().toLocaleTimeString());\n\t\t\tsetCounter(previous => previous + 1);\n\t\t}, 1000);\n\n\t\treturn () => {\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, []);\n\n\t// Rapid updates to degrade performance - updates every 16ms (~60fps)\n\tuseEffect(() => {\n\t\tlet frameCount = 0;\n\t\tlet lastTime = Date.now();\n\n\t\tconst timer = setInterval(() => {\n\t\t\tsetProgress1(previous => (previous + 1) % 101);\n\t\t\tsetProgress2(previous => (previous + 2) % 101);\n\t\t\tsetProgress3(previous => (previous + 3) % 101);\n\t\t\tsetRandomValue(Math.floor(Math.random() * 1000));\n\n\t\t\t// Update only 1-2 log lines each frame (simulating real log updates)\n\t\t\tsetLogLines(previous => {\n\t\t\t\tconst newLines = [...previous];\n\t\t\t\tconst updateIndex = Math.floor(Math.random() * newLines.length);\n\t\t\t\tnewLines[updateIndex] = generateLogLine(\n\t\t\t\t\tupdateIndex,\n\t\t\t\t\tMath.floor(Math.random() * 1000),\n\t\t\t\t);\n\t\t\t\treturn newLines;\n\t\t\t});\n\n\t\t\t// Calculate FPS\n\t\t\tframeCount++;\n\t\t\tconst now = Date.now();\n\t\t\tif (now - lastTime >= 1000) {\n\t\t\t\tsetFps(frameCount);\n\t\t\t\tframeCount = 0;\n\t\t\t\tlastTime = now;\n\t\t\t}\n\t\t}, 16); // ~60 updates per second\n\n\t\treturn () => {\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, []);\n\n\tuseInput((input, key) => {\n\t\tif (key.upArrow) {\n\t\t\tsetSelectedIndex(previousIndex =>\n\t\t\t\tpreviousIndex === 0 ? serviceCount - 1 : previousIndex - 1,\n\t\t\t);\n\t\t}\n\n\t\tif (key.downArrow) {\n\t\t\tsetSelectedIndex(previousIndex =>\n\t\t\t\tpreviousIndex === serviceCount - 1 ? 0 : previousIndex + 1,\n\t\t\t);\n\t\t}\n\n\t\tif (input === 'q') {\n\t\t\texit();\n\t\t}\n\t});\n\n\tconst progressBar = (value: number) => {\n\t\tconst filled = Math.floor(value / 5);\n\t\tconst empty = 20 - filled;\n\t\treturn '█'.repeat(filled) + '░'.repeat(empty);\n\t};\n\n\treturn (\n\t\t<Box flexDirection=\"column\" height=\"100%\">\n\t\t\t<Box borderStyle=\"round\" borderColor=\"cyan\" paddingX={2} paddingY={1}>\n\t\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t\t<Text bold color=\"cyan\">\n\t\t\t\t\t\tIncremental Rendering Demo - incrementalRendering={String(true)}\n\t\t\t\t\t</Text>\n\t\t\t\t\t<Text dimColor>\n\t\t\t\t\t\tUse ↑/↓ arrows to navigate • Press q to quit • FPS: {fps}\n\t\t\t\t\t</Text>\n\t\t\t\t\t<Text>\n\t\t\t\t\t\tTime: <Text color=\"green\">{timestamp}</Text> • Updates:{' '}\n\t\t\t\t\t\t<Text color=\"yellow\">{counter}</Text> • Random:{' '}\n\t\t\t\t\t\t<Text color=\"cyan\">{randomValue}</Text>\n\t\t\t\t\t</Text>\n\t\t\t\t\t<Text>\n\t\t\t\t\t\tProgress 1: <Text color=\"green\">{progressBar(progress1)}</Text>{' '}\n\t\t\t\t\t\t{progress1}%\n\t\t\t\t\t</Text>\n\t\t\t\t\t<Text>\n\t\t\t\t\t\tProgress 2: <Text color=\"yellow\">{progressBar(progress2)}</Text>{' '}\n\t\t\t\t\t\t{progress2}%\n\t\t\t\t\t</Text>\n\t\t\t\t\t<Text>\n\t\t\t\t\t\tProgress 3: <Text color=\"red\">{progressBar(progress3)}</Text>{' '}\n\t\t\t\t\t\t{progress3}%\n\t\t\t\t\t</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\n\t\t\t<Box\n\t\t\t\tborderStyle=\"single\"\n\t\t\t\tborderColor=\"yellow\"\n\t\t\t\tpaddingX={2}\n\t\t\t\tpaddingY={1}\n\t\t\t\tmarginTop={1}\n\t\t\t>\n\t\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t\t<Text bold color=\"yellow\">\n\t\t\t\t\t\tLive Logs (only 1-2 lines update per frame):\n\t\t\t\t\t</Text>\n\t\t\t\t\t{logLines.map(line => (\n\t\t\t\t\t\t<Text key={line} color=\"green\">\n\t\t\t\t\t\t\t{line}\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t))}\n\t\t\t\t</Box>\n\t\t\t</Box>\n\n\t\t\t<Box\n\t\t\t\tborderStyle=\"single\"\n\t\t\t\tborderColor=\"gray\"\n\t\t\t\tpaddingX={2}\n\t\t\t\tpaddingY={1}\n\t\t\t\tmarginTop={1}\n\t\t\t\tflexGrow={1}\n\t\t\t\tflexDirection=\"column\"\n\t\t\t>\n\t\t\t\t<Text bold color=\"magenta\">\n\t\t\t\t\tSystem Services Monitor ({serviceCount} of {rows.length} services):\n\t\t\t\t</Text>\n\t\t\t\t{rows.slice(0, serviceCount).map((row, index) => {\n\t\t\t\t\tconst isSelected = index === selectedIndex;\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<Text key={row} color={isSelected ? 'blue' : 'white'}>\n\t\t\t\t\t\t\t{isSelected ? '> ' : '  '}\n\t\t\t\t\t\t\t{row}\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</Box>\n\n\t\t\t<Box borderStyle=\"round\" borderColor=\"magenta\" paddingX={2} marginTop={1}>\n\t\t\t\t<Text>\n\t\t\t\t\tSelected:{' '}\n\t\t\t\t\t<Text bold color=\"magenta\">\n\t\t\t\t\t\t{rows.slice(0, serviceCount)[selectedIndex]}\n\t\t\t\t\t</Text>\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nrender(<IncrementalRendering />, {incrementalRendering: true});\n"
  },
  {
    "path": "examples/incremental-rendering/index.ts",
    "content": "import './incremental-rendering.js';\n"
  },
  {
    "path": "examples/jest/index.ts",
    "content": "import './jest.js';\n"
  },
  {
    "path": "examples/jest/jest.tsx",
    "content": "import React from 'react';\nimport PQueue from 'p-queue';\nimport delay from 'delay';\nimport ms from 'ms';\nimport {Static, Box, render} from '../../src/index.js';\nimport Summary from './summary.jsx';\nimport Test from './test.js';\n\nconst paths = [\n\t'tests/login.js',\n\t'tests/signup.js',\n\t'tests/forgot-password.js',\n\t'tests/reset-password.js',\n\t'tests/view-profile.js',\n\t'tests/edit-profile.js',\n\t'tests/delete-profile.js',\n\t'tests/posts.js',\n\t'tests/post.js',\n\t'tests/comments.js',\n];\n\ntype State = {\n\tstartTime: number;\n\tcompletedTests: Array<{\n\t\tpath: string;\n\t\tstatus: string;\n\t}>;\n\trunningTests: Array<{\n\t\tpath: string;\n\t\tstatus: string;\n\t}>;\n};\n\nclass Jest extends React.Component<Record<string, unknown>, State> {\n\tconstructor(properties: Record<string, unknown>) {\n\t\tsuper(properties);\n\n\t\tthis.state = {\n\t\t\tstartTime: Date.now(),\n\t\t\tcompletedTests: [],\n\t\t\trunningTests: [],\n\t\t};\n\t}\n\n\trender() {\n\t\tconst {startTime, completedTests, runningTests} = this.state;\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Static items={completedTests}>\n\t\t\t\t\t{test => (\n\t\t\t\t\t\t<Test key={test.path} status={test.status} path={test.path} />\n\t\t\t\t\t)}\n\t\t\t\t</Static>\n\n\t\t\t\t{runningTests.length > 0 && (\n\t\t\t\t\t<Box flexDirection=\"column\" marginTop={1}>\n\t\t\t\t\t\t{runningTests.map(test => (\n\t\t\t\t\t\t\t<Test key={test.path} status={test.status} path={test.path} />\n\t\t\t\t\t\t))}\n\t\t\t\t\t</Box>\n\t\t\t\t)}\n\n\t\t\t\t<Summary\n\t\t\t\t\tisFinished={runningTests.length === 0}\n\t\t\t\t\tpassed={completedTests.filter(test => test.status === 'pass').length}\n\t\t\t\t\tfailed={completedTests.filter(test => test.status === 'fail').length}\n\t\t\t\t\ttime={ms(Date.now() - startTime)}\n\t\t\t\t/>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tcomponentDidMount() {\n\t\tconst queue = new PQueue({concurrency: 4});\n\n\t\tfor (const path of paths) {\n\t\t\tvoid queue.add(this.runTest.bind(this, path));\n\t\t}\n\t}\n\n\tasync runTest(path: string) {\n\t\tthis.setState(previousState => ({\n\t\t\trunningTests: [\n\t\t\t\t...previousState.runningTests,\n\t\t\t\t{\n\t\t\t\t\tstatus: 'runs',\n\t\t\t\t\tpath,\n\t\t\t\t},\n\t\t\t],\n\t\t}));\n\n\t\tawait delay(1000 * Math.random());\n\n\t\tthis.setState(previousState => ({\n\t\t\trunningTests: previousState.runningTests.filter(\n\t\t\t\ttest => test.path !== path,\n\t\t\t),\n\t\t\tcompletedTests: [\n\t\t\t\t...previousState.completedTests,\n\t\t\t\t{\n\t\t\t\t\tstatus: Math.random() < 0.5 ? 'pass' : 'fail',\n\t\t\t\t\tpath,\n\t\t\t\t},\n\t\t\t],\n\t\t}));\n\t}\n}\n\nrender(<Jest />);\n"
  },
  {
    "path": "examples/jest/summary.tsx",
    "content": "import React from 'react';\nimport {Box, Text} from '../../src/index.js';\n\ntype Properties = {\n\treadonly isFinished: boolean;\n\treadonly passed: number;\n\treadonly failed: number;\n\treadonly time: string;\n};\n\nfunction Summary({isFinished, passed, failed, time}: Properties) {\n\treturn (\n\t\t<Box flexDirection=\"column\" marginTop={1}>\n\t\t\t<Box>\n\t\t\t\t<Box width={14}>\n\t\t\t\t\t<Text bold>Test Suites:</Text>\n\t\t\t\t</Box>\n\t\t\t\t{failed > 0 && (\n\t\t\t\t\t<Text bold color=\"red\">\n\t\t\t\t\t\t{failed} failed,{' '}\n\t\t\t\t\t</Text>\n\t\t\t\t)}\n\t\t\t\t{passed > 0 && (\n\t\t\t\t\t<Text bold color=\"green\">\n\t\t\t\t\t\t{passed} passed,{' '}\n\t\t\t\t\t</Text>\n\t\t\t\t)}\n\t\t\t\t<Text>{passed + failed} total</Text>\n\t\t\t</Box>\n\n\t\t\t<Box>\n\t\t\t\t<Box width={14}>\n\t\t\t\t\t<Text bold>Time:</Text>\n\t\t\t\t</Box>\n\n\t\t\t\t<Text>{time}</Text>\n\t\t\t</Box>\n\n\t\t\t{isFinished ? (\n\t\t\t\t<Box>\n\t\t\t\t\t<Text dimColor>Ran all test suites.</Text>\n\t\t\t\t</Box>\n\t\t\t) : null}\n\t\t</Box>\n\t);\n}\n\nexport default Summary;\n"
  },
  {
    "path": "examples/jest/test.tsx",
    "content": "import React from 'react';\nimport {Box, Text} from '../../src/index.js';\n\nconst getBackgroundForStatus = (status: string): string | undefined => {\n\tswitch (status) {\n\t\tcase 'runs': {\n\t\t\treturn 'yellow';\n\t\t}\n\n\t\tcase 'pass': {\n\t\t\treturn 'green';\n\t\t}\n\n\t\tcase 'fail': {\n\t\t\treturn 'red';\n\t\t}\n\n\t\tdefault: {\n\t\t\treturn undefined;\n\t\t}\n\t}\n};\n\ntype Properties = {\n\treadonly status: string;\n\treadonly path: string;\n};\n\nfunction Test({status, path}: Properties) {\n\treturn (\n\t\t<Box>\n\t\t\t<Text color=\"black\" backgroundColor={getBackgroundForStatus(status)}>\n\t\t\t\t{` ${status.toUpperCase()} `}\n\t\t\t</Text>\n\n\t\t\t<Box marginLeft={1}>\n\t\t\t\t<Text dimColor>{path.split('/')[0]}/</Text>\n\n\t\t\t\t<Text bold color=\"white\">\n\t\t\t\t\t{path.split('/')[1]}\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nexport default Test;\n"
  },
  {
    "path": "examples/justify-content/index.ts",
    "content": "import './justify-content.js';\n"
  },
  {
    "path": "examples/justify-content/justify-content.tsx",
    "content": "import React from 'react';\nimport {render, Box, Text} from '../../src/index.js';\n\nfunction JustifyContent() {\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Box>\n\t\t\t\t<Text>[</Text>\n\t\t\t\t<Box justifyContent=\"flex-start\" width={20} height={1}>\n\t\t\t\t\t<Text>X</Text>\n\t\t\t\t\t<Text>Y</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>] flex-start</Text>\n\t\t\t</Box>\n\t\t\t<Box>\n\t\t\t\t<Text>[</Text>\n\t\t\t\t<Box justifyContent=\"flex-end\" width={20} height={1}>\n\t\t\t\t\t<Text>X</Text>\n\t\t\t\t\t<Text>Y</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>] flex-end</Text>\n\t\t\t</Box>\n\t\t\t<Box>\n\t\t\t\t<Text>[</Text>\n\t\t\t\t<Box justifyContent=\"center\" width={20} height={1}>\n\t\t\t\t\t<Text>X</Text>\n\t\t\t\t\t<Text>Y</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>] center</Text>\n\t\t\t</Box>\n\t\t\t<Box>\n\t\t\t\t<Text>[</Text>\n\t\t\t\t<Box justifyContent=\"space-around\" width={20} height={1}>\n\t\t\t\t\t<Text>X</Text>\n\t\t\t\t\t<Text>Y</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>] space-around</Text>\n\t\t\t</Box>\n\t\t\t<Box>\n\t\t\t\t<Text>[</Text>\n\t\t\t\t<Box justifyContent=\"space-between\" width={20} height={1}>\n\t\t\t\t\t<Text>X</Text>\n\t\t\t\t\t<Text>Y</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>] space-between</Text>\n\t\t\t</Box>\n\t\t\t<Box>\n\t\t\t\t<Text>[</Text>\n\t\t\t\t<Box justifyContent=\"space-evenly\" width={20} height={1}>\n\t\t\t\t\t<Text>X</Text>\n\t\t\t\t\t<Text>Y</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>] space-evenly</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nrender(<JustifyContent />);\n"
  },
  {
    "path": "examples/render-throttle/index.tsx",
    "content": "import React, {useState, useEffect} from 'react';\nimport {render, Box, Text} from '../../src/index.js';\n\nfunction App() {\n\tconst [count, setCount] = useState(0);\n\n\tuseEffect(() => {\n\t\tconst interval = setInterval(() => {\n\t\t\tsetCount(c => c + 1);\n\t\t}, 10); // Update every 10ms\n\n\t\treturn () => {\n\t\t\tclearInterval(interval);\n\t\t};\n\t}, []);\n\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<Text>Counter: {count}</Text>\n\t\t\t<Text>This updates every 10ms but renders are throttled</Text>\n\t\t\t<Text>Press Ctrl+C to exit</Text>\n\t\t</Box>\n\t);\n}\n\n// Example with custom maxFps\nrender(<App />, {\n\tmaxFps: 10, // Only render at 10fps (every ~100ms) instead of default 30fps\n});\n"
  },
  {
    "path": "examples/router/index.ts",
    "content": "import './router.js';\n"
  },
  {
    "path": "examples/router/router.tsx",
    "content": "import React from 'react';\nimport {MemoryRouter, Routes, Route, useNavigate} from 'react-router';\nimport {render, useInput, useApp, Box, Text} from '../../src/index.js';\n\nfunction Home() {\n\tconst {exit} = useApp();\n\tconst navigate = useNavigate();\n\n\tuseInput((input, key) => {\n\t\tif (input === 'q') {\n\t\t\texit();\n\t\t}\n\n\t\tif (key.return) {\n\t\t\tvoid navigate('/about');\n\t\t}\n\t});\n\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text bold color=\"green\">\n\t\t\t\tHome\n\t\t\t</Text>\n\t\t\t<Text>Press Enter to go to About, or \"q\" to quit.</Text>\n\t\t</Box>\n\t);\n}\n\nfunction About() {\n\tconst {exit} = useApp();\n\tconst navigate = useNavigate();\n\n\tuseInput((input, key) => {\n\t\tif (input === 'q') {\n\t\t\texit();\n\t\t}\n\n\t\tif (key.return) {\n\t\t\tvoid navigate('/');\n\t\t}\n\t});\n\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text bold color=\"blue\">\n\t\t\t\tAbout\n\t\t\t</Text>\n\t\t\t<Text>Press Enter to go back Home, or \"q\" to quit.</Text>\n\t\t</Box>\n\t);\n}\n\nfunction App() {\n\treturn (\n\t\t<MemoryRouter>\n\t\t\t<Routes>\n\t\t\t\t<Route path=\"/\" element={<Home />} />\n\t\t\t\t<Route path=\"/about\" element={<About />} />\n\t\t\t</Routes>\n\t\t</MemoryRouter>\n\t);\n}\n\nrender(<App />);\n"
  },
  {
    "path": "examples/select-input/index.ts",
    "content": "import './select-input.js';\n"
  },
  {
    "path": "examples/select-input/select-input.tsx",
    "content": "import React, {useState} from 'react';\nimport {\n\trender,\n\tText,\n\tBox,\n\tuseInput,\n\tuseIsScreenReaderEnabled,\n} from '../../src/index.js';\n\nconst items = ['Red', 'Green', 'Blue', 'Yellow', 'Magenta', 'Cyan'];\n\nfunction SelectInput() {\n\tconst [selectedIndex, setSelectedIndex] = useState(0);\n\tconst isScreenReaderEnabled = useIsScreenReaderEnabled();\n\n\tuseInput((input, key) => {\n\t\tif (key.upArrow) {\n\t\t\tsetSelectedIndex(previousIndex =>\n\t\t\t\tpreviousIndex === 0 ? items.length - 1 : previousIndex - 1,\n\t\t\t);\n\t\t}\n\n\t\tif (key.downArrow) {\n\t\t\tsetSelectedIndex(previousIndex =>\n\t\t\t\tpreviousIndex === items.length - 1 ? 0 : previousIndex + 1,\n\t\t\t);\n\t\t}\n\n\t\tif (isScreenReaderEnabled) {\n\t\t\tconst number = Number.parseInt(input, 10);\n\t\t\tif (!Number.isNaN(number) && number > 0 && number <= items.length) {\n\t\t\t\tsetSelectedIndex(number - 1);\n\t\t\t}\n\t\t}\n\t});\n\n\treturn (\n\t\t<Box flexDirection=\"column\" aria-role=\"list\">\n\t\t\t<Text>Select a color:</Text>\n\t\t\t{items.map((item, index) => {\n\t\t\t\tconst isSelected = index === selectedIndex;\n\t\t\t\tconst label = isSelected ? `> ${item}` : `  ${item}`;\n\t\t\t\tconst screenReaderLabel = `${index + 1}. ${item}`;\n\n\t\t\t\treturn (\n\t\t\t\t\t<Box\n\t\t\t\t\t\tkey={item}\n\t\t\t\t\t\taria-role=\"listitem\"\n\t\t\t\t\t\taria-state={{selected: isSelected}}\n\t\t\t\t\t\taria-label={isScreenReaderEnabled ? screenReaderLabel : undefined}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Text color={isSelected ? 'blue' : undefined}>{label}</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t);\n\t\t\t})}\n\t\t</Box>\n\t);\n}\n\nrender(<SelectInput />);\n"
  },
  {
    "path": "examples/static/index.ts",
    "content": "import './static.js';\n"
  },
  {
    "path": "examples/static/static.tsx",
    "content": "import React from 'react';\nimport {Box, Text, render, Static} from '../../src/index.js';\n\nfunction Example() {\n\tconst [tests, setTests] = React.useState<\n\t\tArray<{\n\t\t\tid: number;\n\t\t\ttitle: string;\n\t\t}>\n\t>([]);\n\n\tReact.useEffect(() => {\n\t\tlet completedTests = 0;\n\t\tlet timer: NodeJS.Timeout | undefined;\n\n\t\tconst run = () => {\n\t\t\tif (completedTests++ < 10) {\n\t\t\t\tsetTests(previousTests => [\n\t\t\t\t\t...previousTests,\n\t\t\t\t\t{\n\t\t\t\t\t\tid: previousTests.length,\n\t\t\t\t\t\ttitle: `Test #${previousTests.length + 1}`,\n\t\t\t\t\t},\n\t\t\t\t]);\n\n\t\t\t\ttimer = setTimeout(run, 100);\n\t\t\t}\n\t\t};\n\n\t\trun();\n\n\t\treturn () => {\n\t\t\tclearTimeout(timer);\n\t\t};\n\t}, []);\n\n\treturn (\n\t\t<>\n\t\t\t<Static items={tests}>\n\t\t\t\t{test => (\n\t\t\t\t\t<Box key={test.id}>\n\t\t\t\t\t\t<Text color=\"green\">✔ {test.title}</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t)}\n\t\t\t</Static>\n\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text dimColor>Completed tests: {tests.length}</Text>\n\t\t\t</Box>\n\t\t</>\n\t);\n}\n\nrender(<Example />);\n"
  },
  {
    "path": "examples/subprocess-output/index.ts",
    "content": "import './subprocess-output.js';\n"
  },
  {
    "path": "examples/subprocess-output/subprocess-output.tsx",
    "content": "import childProcess from 'node:child_process';\nimport type {Buffer} from 'node:buffer';\nimport React from 'react';\nimport stripAnsi from 'strip-ansi';\nimport {render, Text, Box} from '../../src/index.js';\n\nfunction SubprocessOutput() {\n\tconst [output, setOutput] = React.useState('');\n\n\tReact.useEffect(() => {\n\t\tconst subProcess = childProcess.spawn('npm', [\n\t\t\t'run',\n\t\t\t'example',\n\t\t\t'examples/jest',\n\t\t]);\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-restricted-types\n\t\tsubProcess.stdout.on('data', (newOutput: Buffer) => {\n\t\t\tconst lines = stripAnsi(newOutput.toString('utf8')).split('\\n');\n\t\t\tsetOutput(lines.slice(-5).join('\\n'));\n\t\t});\n\t}, [setOutput]);\n\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<Text>Сommand output:</Text>\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text>{output}</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nrender(<SubprocessOutput />);\n"
  },
  {
    "path": "examples/suspense/index.ts",
    "content": "import './suspense.js';\n"
  },
  {
    "path": "examples/suspense/suspense.tsx",
    "content": "import React from 'react';\nimport {render, Text} from '../../src/index.js';\n\nlet promise: Promise<void> | undefined;\nlet state: string | undefined;\nlet value: string | undefined;\n\nconst read = () => {\n\tif (!promise) {\n\t\tpromise = new Promise(resolve => {\n\t\t\tsetTimeout(resolve, 500);\n\t\t});\n\n\t\tstate = 'pending';\n\n\t\t(async () => {\n\t\t\tawait promise;\n\t\t\tstate = 'done';\n\t\t\tvalue = 'Hello World';\n\t\t})();\n\t}\n\n\tif (state === 'pending') {\n\t\t// eslint-disable-next-line @typescript-eslint/only-throw-error\n\t\tthrow promise;\n\t}\n\n\tif (state === 'done') {\n\t\treturn value;\n\t}\n};\n\nfunction Example() {\n\tconst message = read();\n\treturn <Text>{message}</Text>;\n}\n\nfunction Fallback() {\n\treturn <Text>Loading...</Text>;\n}\n\nrender(\n\t<React.Suspense fallback={<Fallback />}>\n\t\t<Example />\n\t</React.Suspense>,\n);\n"
  },
  {
    "path": "examples/table/index.ts",
    "content": "import './table.js';\n"
  },
  {
    "path": "examples/table/table.tsx",
    "content": "import React from 'react';\nimport {faker} from '@faker-js/faker';\nimport {Box, Text, render} from '../../src/index.js';\n\nconst users = Array.from({length: 10})\n\t.fill(true)\n\t.map((_, index) => ({\n\t\tid: index,\n\t\tname: faker.internet.username(),\n\t\temail: faker.internet.email(),\n\t}));\n\nfunction Table() {\n\treturn (\n\t\t<Box flexDirection=\"column\" width={80}>\n\t\t\t<Box>\n\t\t\t\t<Box width=\"10%\">\n\t\t\t\t\t<Text>ID</Text>\n\t\t\t\t</Box>\n\n\t\t\t\t<Box width=\"50%\">\n\t\t\t\t\t<Text>Name</Text>\n\t\t\t\t</Box>\n\n\t\t\t\t<Box width=\"40%\">\n\t\t\t\t\t<Text>Email</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\n\t\t\t{users.map(user => (\n\t\t\t\t<Box key={user.id}>\n\t\t\t\t\t<Box width=\"10%\">\n\t\t\t\t\t\t<Text>{user.id}</Text>\n\t\t\t\t\t</Box>\n\n\t\t\t\t\t<Box width=\"50%\">\n\t\t\t\t\t\t<Text>{user.name}</Text>\n\t\t\t\t\t</Box>\n\n\t\t\t\t\t<Box width=\"40%\">\n\t\t\t\t\t\t<Text>{user.email}</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t</Box>\n\t\t\t))}\n\t\t</Box>\n\t);\n}\n\nrender(<Table />);\n"
  },
  {
    "path": "examples/terminal-resize/index.ts",
    "content": "import './terminal-resize.js';\n"
  },
  {
    "path": "examples/terminal-resize/terminal-resize.tsx",
    "content": "import React from 'react';\nimport {render, Box, Text, useWindowSize} from '../../src/index.js';\n\nfunction TerminalResizeExample() {\n\tconst {columns, rows} = useWindowSize();\n\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<Text bold color=\"cyan\">\n\t\t\t\tTerminal Size\n\t\t\t</Text>\n\t\t\t<Text>Columns: {columns}</Text>\n\t\t\t<Text>Rows: {rows}</Text>\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text dimColor>\n\t\t\t\t\tResize your terminal to see the values update. Press Ctrl+C to exit.\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nrender(<TerminalResizeExample />, {\n\tpatchConsole: true,\n\texitOnCtrlC: true,\n});\n"
  },
  {
    "path": "examples/use-focus/index.ts",
    "content": "import './use-focus.js';\n"
  },
  {
    "path": "examples/use-focus/use-focus.tsx",
    "content": "import React from 'react';\nimport {Box, Text, render, useFocus} from '../../src/index.js';\n\nfunction Focus() {\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<Box marginBottom={1}>\n\t\t\t\t<Text>\n\t\t\t\t\tPress Tab to focus next element, Shift+Tab to focus previous element,\n\t\t\t\t\tEsc to reset focus.\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t\t<Item label=\"First\" />\n\t\t\t<Item label=\"Second\" />\n\t\t\t<Item label=\"Third\" />\n\t\t</Box>\n\t);\n}\n\nfunction Item({label}) {\n\tconst {isFocused} = useFocus();\n\treturn (\n\t\t<Text>\n\t\t\t{label} {isFocused ? <Text color=\"green\">(focused)</Text> : null}\n\t\t</Text>\n\t);\n}\n\nrender(<Focus />);\n"
  },
  {
    "path": "examples/use-focus-with-id/index.ts",
    "content": "import './use-focus-with-id.js';\n"
  },
  {
    "path": "examples/use-focus-with-id/use-focus-with-id.tsx",
    "content": "import React from 'react';\nimport {\n\trender,\n\tBox,\n\tText,\n\tuseFocus,\n\tuseInput,\n\tuseFocusManager,\n} from '../../src/index.js';\n\nfunction Focus() {\n\tconst {focus} = useFocusManager();\n\n\tuseInput(input => {\n\t\tif (input === '1') {\n\t\t\tfocus('1');\n\t\t}\n\n\t\tif (input === '2') {\n\t\t\tfocus('2');\n\t\t}\n\n\t\tif (input === '3') {\n\t\t\tfocus('3');\n\t\t}\n\t});\n\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<Box marginBottom={1}>\n\t\t\t\t<Text>\n\t\t\t\t\tPress Tab to focus next element, Shift+Tab to focus previous element,\n\t\t\t\t\tEsc to reset focus.\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t\t<Item id=\"1\" label=\"Press 1 to focus\" />\n\t\t\t<Item id=\"2\" label=\"Press 2 to focus\" />\n\t\t\t<Item id=\"3\" label=\"Press 3 to focus\" />\n\t\t</Box>\n\t);\n}\n\ntype ItemProperties = {\n\treadonly id: number;\n\treadonly label: string;\n};\n\nfunction Item({label, id}: ItemProperties) {\n\tconst {isFocused} = useFocus({id});\n\n\treturn (\n\t\t<Text>\n\t\t\t{label} {isFocused ? <Text color=\"green\">(focused)</Text> : null}\n\t\t</Text>\n\t);\n}\n\nrender(<Focus />);\n"
  },
  {
    "path": "examples/use-input/index.ts",
    "content": "import './use-input.js';\n"
  },
  {
    "path": "examples/use-input/use-input.tsx",
    "content": "import React from 'react';\nimport {render, useInput, useApp, Box, Text} from '../../src/index.js';\n\nfunction Robot() {\n\tconst {exit} = useApp();\n\tconst [x, setX] = React.useState(1);\n\tconst [y, setY] = React.useState(1);\n\n\tuseInput((input, key) => {\n\t\tif (input === 'q') {\n\t\t\texit();\n\t\t}\n\n\t\tif (key.leftArrow) {\n\t\t\tsetX(Math.max(1, x - 1));\n\t\t}\n\n\t\tif (key.rightArrow) {\n\t\t\tsetX(Math.min(20, x + 1));\n\t\t}\n\n\t\tif (key.upArrow) {\n\t\t\tsetY(Math.max(1, y - 1));\n\t\t}\n\n\t\tif (key.downArrow) {\n\t\t\tsetY(Math.min(10, y + 1));\n\t\t}\n\t});\n\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>Use arrow keys to move the face. Press “q” to exit.</Text>\n\t\t\t<Box height={12} paddingLeft={x} paddingTop={y}>\n\t\t\t\t<Text>^_^</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nrender(<Robot />);\n"
  },
  {
    "path": "examples/use-stderr/index.ts",
    "content": "import './use-stderr.js';\n"
  },
  {
    "path": "examples/use-stderr/use-stderr.tsx",
    "content": "import React from 'react';\nimport {render, Text, useStderr} from '../../src/index.js';\n\nfunction Example() {\n\tconst {write} = useStderr();\n\n\tReact.useEffect(() => {\n\t\tconst timer = setInterval(() => {\n\t\t\twrite('Hello from Ink to stderr\\n');\n\t\t}, 1000);\n\n\t\treturn () => {\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, []);\n\n\treturn <Text>Hello World</Text>;\n}\n\nrender(<Example />);\n"
  },
  {
    "path": "examples/use-stdout/index.ts",
    "content": "import './use-stdout.js';\n"
  },
  {
    "path": "examples/use-stdout/use-stdout.tsx",
    "content": "import React from 'react';\nimport {render, Box, Text, useStdout} from '../../src/index.js';\n\nfunction Example() {\n\tconst {stdout, write} = useStdout();\n\n\tReact.useEffect(() => {\n\t\tconst timer = setInterval(() => {\n\t\t\twrite('Hello from Ink to stdout\\n');\n\t\t}, 1000);\n\n\t\treturn () => {\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, []);\n\n\treturn (\n\t\t<Box flexDirection=\"column\" paddingX={2} paddingY={1}>\n\t\t\t<Text bold underline>\n\t\t\t\tTerminal dimensions:\n\t\t\t</Text>\n\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text>\n\t\t\t\t\tWidth: <Text bold>{stdout.columns}</Text>\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t\t<Box>\n\t\t\t\t<Text>\n\t\t\t\t\tHeight: <Text bold>{stdout.rows}</Text>\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nrender(<Example />);\n"
  },
  {
    "path": "examples/use-transition/index.ts",
    "content": "import './use-transition.js';\n"
  },
  {
    "path": "examples/use-transition/use-transition.tsx",
    "content": "import React, {useState, useMemo, useTransition} from 'react';\nimport {render, Box, Text, useInput} from '../../src/index.js';\n\n// Generate a large list of items for demonstration\nfunction generateItems(filter: string): string[] {\n\tconst allItems: string[] = [];\n\tfor (let i = 0; i < 200; i++) {\n\t\tallItems.push(\n\t\t\t`Item ${i + 1}: ${['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'][i % 5]}`,\n\t\t);\n\t}\n\n\tif (!filter) {\n\t\treturn allItems.slice(0, 10);\n\t}\n\n\t// Simulate expensive filtering\n\tconst start = Date.now();\n\twhile (Date.now() - start < 100) {\n\t\t// Artificial delay to simulate expensive computation\n\t}\n\n\treturn allItems\n\t\t.filter(item => item.toLowerCase().includes(filter.toLowerCase()))\n\t\t.slice(0, 10);\n}\n\nfunction SearchApp() {\n\tconst [query, setQuery] = useState('');\n\tconst [isPending, startTransition] = useTransition();\n\n\t// This is the \"deferred\" state that can lag behind\n\tconst [deferredQuery, setDeferredQuery] = useState('');\n\n\t// Filtered items based on deferred query (expensive computation)\n\tconst filteredItems = useMemo(\n\t\t() => generateItems(deferredQuery),\n\t\t[deferredQuery],\n\t);\n\n\t// Handle keyboard input\n\tuseInput((input, key) => {\n\t\tif (key.backspace || key.delete) {\n\t\t\tsetQuery(previousQuery => previousQuery.slice(0, -1));\n\t\t\tstartTransition(() => {\n\t\t\t\tsetDeferredQuery(previousQuery => previousQuery.slice(0, -1));\n\t\t\t});\n\t\t} else if (input && !key.ctrl && !key.meta) {\n\t\t\tsetQuery(previousQuery => previousQuery + input);\n\t\t\t// Wrap the expensive update in a transition\n\t\t\tstartTransition(() => {\n\t\t\t\tsetDeferredQuery(previousQuery => previousQuery + input);\n\t\t\t});\n\t\t}\n\t});\n\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text bold underline>\n\t\t\t\tuseTransition Demo\n\t\t\t</Text>\n\t\t\t<Text dimColor>\n\t\t\t\t(Type to search - input stays responsive while list updates)\n\t\t\t</Text>\n\t\t\t<Box marginTop={1} />\n\n\t\t\t<Box>\n\t\t\t\t<Text>Search: </Text>\n\t\t\t\t<Text color=\"cyan\">{query || '(type something)'}</Text>\n\t\t\t\t{isPending ? <Text color=\"yellow\"> (updating...)</Text> : null}\n\t\t\t</Box>\n\n\t\t\t<Box marginTop={1} flexDirection=\"column\">\n\t\t\t\t<Text bold>\n\t\t\t\t\tResults{' '}\n\t\t\t\t\t{deferredQuery ? `for \"${deferredQuery}\"` : '(showing first 10)'}:\n\t\t\t\t</Text>\n\t\t\t\t{filteredItems.length === 0 ? (\n\t\t\t\t\t<Text dimColor> No items found</Text>\n\t\t\t\t) : (\n\t\t\t\t\tfilteredItems.map(item => (\n\t\t\t\t\t\t<Text key={item} dimColor={isPending}>\n\t\t\t\t\t\t\t{item}\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t))\n\t\t\t\t)}\n\t\t\t</Box>\n\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text dimColor>Press Ctrl+C to exit</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\n// Render with concurrent mode enabled (required for useTransition)\nrender(<SearchApp />, {concurrent: true});\n"
  },
  {
    "path": "license",
    "content": "MIT License\n\nCopyright (c) Vadym Demedes <vadimdemedes@hey.com> (https://github.com/vadimdemedes)\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n"
  },
  {
    "path": "media/demo.js",
    "content": "import process from 'node:process';\nimport React from 'react';\nimport {render, Box, Text} from 'ink';\n\nclass Counter extends React.PureComponent {\n\tconstructor() {\n\t\tsuper();\n\n\t\tthis.state = {\n\t\t\ti: 0,\n\t\t};\n\t}\n\n\trender() {\n\t\treturn React.createElement(\n\t\t\tBox,\n\t\t\t{flexDirection: 'column'},\n\t\t\tReact.createElement(\n\t\t\t\tBox,\n\t\t\t\t{},\n\t\t\t\tReact.createElement(Text, {color: 'blue'}, '~/Projects/ink '),\n\t\t\t),\n\t\t\tReact.createElement(\n\t\t\t\tBox,\n\t\t\t\t{},\n\t\t\t\tReact.createElement(Text, {color: 'magenta'}, '❯ '),\n\t\t\t\tReact.createElement(Text, {color: 'green'}, 'node '),\n\t\t\t\tReact.createElement(Text, {}, 'media/example'),\n\t\t\t),\n\t\t\tReact.createElement(\n\t\t\t\tText,\n\t\t\t\t{color: 'green'},\n\t\t\t\t`${this.state.i} tests passed`,\n\t\t\t),\n\t\t);\n\t}\n\n\tcomponentDidMount() {\n\t\tthis.timer = setInterval(() => {\n\t\t\tif (this.state.i === 50) {\n\t\t\t\tprocess.exit(0); // eslint-disable-line unicorn/no-process-exit\n\t\t\t}\n\n\t\t\tthis.setState(previousState => ({\n\t\t\t\ti: previousState.i + 1,\n\t\t\t}));\n\t\t}, 100);\n\t}\n\n\tcomponentWillUnmount() {\n\t\tclearInterval(this.timer);\n\t}\n}\n\nrender(React.createElement(Counter));\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"ink\",\n\t\"version\": \"6.8.0\",\n\t\"description\": \"React for CLI\",\n\t\"license\": \"MIT\",\n\t\"repository\": \"vadimdemedes/ink\",\n\t\"author\": {\n\t\t\"name\": \"Vadim Demedes\",\n\t\t\"email\": \"vadimdemedes@hey.com\",\n\t\t\"url\": \"https://github.com/vadimdemedes\"\n\t},\n\t\"type\": \"module\",\n\t\"exports\": {\n\t\t\"types\": \"./build/index.d.ts\",\n\t\t\"default\": \"./build/index.js\"\n\t},\n\t\"engines\": {\n\t\t\"node\": \">=20\"\n\t},\n\t\"scripts\": {\n\t\t\"dev\": \"tsc --watch\",\n\t\t\"build\": \"tsc\",\n\t\t\"prepare\": \"npm run build\",\n\t\t\"test\": \"npm run typecheck && npm run lint && FORCE_COLOR=true ava\",\n\t\t\"lint\": \"xo\",\n\t\t\"typecheck\": \"tsc --noEmit\",\n\t\t\"example\": \"NODE_NO_WARNINGS=1 node --import=tsx\",\n\t\t\"benchmark\": \"NODE_NO_WARNINGS=1 node --import=tsx\",\n\t\t\"inspect\": \"react-devtools\"\n\t},\n\t\"files\": [\n\t\t\"build\"\n\t],\n\t\"keywords\": [\n\t\t\"react\",\n\t\t\"cli\",\n\t\t\"jsx\",\n\t\t\"stdout\",\n\t\t\"components\",\n\t\t\"command-line\",\n\t\t\"preact\",\n\t\t\"redux\",\n\t\t\"print\",\n\t\t\"render\",\n\t\t\"colors\",\n\t\t\"text\"\n\t],\n\t\"dependencies\": {\n\t\t\"@alcalzone/ansi-tokenize\": \"^0.3.0\",\n\t\t\"ansi-escapes\": \"^7.3.0\",\n\t\t\"ansi-styles\": \"^6.2.1\",\n\t\t\"auto-bind\": \"^5.0.1\",\n\t\t\"chalk\": \"^5.6.0\",\n\t\t\"cli-boxes\": \"^3.0.0\",\n\t\t\"cli-cursor\": \"^4.0.0\",\n\t\t\"cli-truncate\": \"^5.1.1\",\n\t\t\"code-excerpt\": \"^4.0.0\",\n\t\t\"es-toolkit\": \"^1.39.10\",\n\t\t\"indent-string\": \"^5.0.0\",\n\t\t\"is-in-ci\": \"^2.0.0\",\n\t\t\"patch-console\": \"^2.0.0\",\n\t\t\"react-reconciler\": \"^0.33.0\",\n\t\t\"scheduler\": \"^0.27.0\",\n\t\t\"signal-exit\": \"^3.0.7\",\n\t\t\"slice-ansi\": \"^8.0.0\",\n\t\t\"stack-utils\": \"^2.0.6\",\n\t\t\"string-width\": \"^8.1.1\",\n\t\t\"terminal-size\": \"^4.0.1\",\n\t\t\"type-fest\": \"^5.4.1\",\n\t\t\"widest-line\": \"^6.0.0\",\n\t\t\"wrap-ansi\": \"^10.0.0\",\n\t\t\"ws\": \"^8.18.0\",\n\t\t\"yoga-layout\": \"~3.2.1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@faker-js/faker\": \"^10.3.0\",\n\t\t\"@sindresorhus/tsconfig\": \"^8.1.0\",\n\t\t\"@sinonjs/fake-timers\": \"^15.1.0\",\n\t\t\"@types/ms\": \"^2.1.0\",\n\t\t\"@types/node\": \"^25.0.10\",\n\t\t\"@types/react\": \"^19.2.13\",\n\t\t\"@types/react-reconciler\": \"^0.33.0\",\n\t\t\"@types/scheduler\": \"^0.26.0\",\n\t\t\"@types/signal-exit\": \"^3.0.0\",\n\t\t\"@types/sinon\": \"^21.0.0\",\n\t\t\"@types/stack-utils\": \"^2.0.2\",\n\t\t\"@types/ws\": \"^8.18.1\",\n\t\t\"@vdemedes/prettier-config\": \"^2.0.1\",\n\t\t\"ava\": \"^7.0.0\",\n\t\t\"boxen\": \"^8.0.1\",\n\t\t\"delay\": \"^7.0.0\",\n\t\t\"ms\": \"^2.1.3\",\n\t\t\"node-pty\": \"^1.2.0-beta.10\",\n\t\t\"p-queue\": \"^9.0.0\",\n\t\t\"prettier\": \"^3.8.1\",\n\t\t\"react\": \"^19.2.4\",\n\t\t\"react-devtools-core\": \"^7.0.1\",\n\t\t\"react-devtools\": \"^7.0.1\",\n\t\t\"react-router\": \"^7.13.0\",\n\t\t\"sinon\": \"^21.0.0\",\n\t\t\"strip-ansi\": \"^7.1.0\",\n\t\t\"tsx\": \"^4.21.0\",\n\t\t\"typescript\": \"^5.8.3\",\n\t\t\"xo\": \"^1.2.3\"\n\t},\n\t\"peerDependencies\": {\n\t\t\"@types/react\": \">=19.0.0\",\n\t\t\"react\": \">=19.0.0\",\n\t\t\"react-devtools-core\": \">=6.1.2\"\n\t},\n\t\"peerDependenciesMeta\": {\n\t\t\"@types/react\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"react-devtools-core\": {\n\t\t\t\"optional\": true\n\t\t}\n\t},\n\t\"ava\": {\n\t\t\"workerThreads\": false,\n\t\t\"serial\": true,\n\t\t\"files\": [\n\t\t\t\"test/**/*\",\n\t\t\t\"!test/helpers/**/*\",\n\t\t\t\"!test/fixtures/**/*\"\n\t\t],\n\t\t\"extensions\": {\n\t\t\t\"ts\": \"module\",\n\t\t\t\"tsx\": \"module\"\n\t\t},\n\t\t\"nodeArguments\": [\n\t\t\t\"--import=tsx\"\n\t\t]\n\t},\n\t\"prettier\": \"@vdemedes/prettier-config\"\n}\n"
  },
  {
    "path": "readme.md",
    "content": "[![](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md)\n\n---\n\n<div align=\"center\">\n\t<br>\n\t<br>\n\t<img width=\"240\" alt=\"Ink\" src=\"media/logo.png\">\n\t<br>\n\t<br>\n\t<br>\n</div>\n\n> React for CLIs. Build and test your CLI output using components.\n\n[![Build Status](https://github.com/vadimdemedes/ink/workflows/test/badge.svg)](https://github.com/vadimdemedes/ink/actions)\n[![npm](https://img.shields.io/npm/dm/ink?logo=npm)](https://npmjs.com/package/ink)\n\nInk provides the same component-based UI building experience that React offers in the browser, but for command-line apps.\nIt 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.\nIf you are already familiar with React, you already know Ink.\n\nSince Ink is a React renderer, all features of React are supported.\nHead over to the [React](https://reactjs.org) website for documentation on how to use it.\nOnly Ink's methods are documented in this readme.\n\n---\n\n<div align=\"center\">\n\t<p>\n\t\t<p>\n\t\t\t<sup>\n\t\t\t\t<a href=\"https://opencollective.com/vadimdemedes\">My open source work is supported by the community ❤️</a>\n\t\t\t</sup>\n\t\t</p>\n\t</p>\n</div>\n\n## Install\n\n```sh\nnpm install ink react\n```\n\n> [!NOTE]\n> This readme documents the upcoming version of Ink. For the latest stable release, see [Ink on npm](https://www.npmjs.com/package/ink).\n\n## Usage\n\n```jsx\nimport React, {useState, useEffect} from 'react';\nimport {render, Text} from 'ink';\n\nconst Counter = () => {\n\tconst [counter, setCounter] = useState(0);\n\n\tuseEffect(() => {\n\t\tconst timer = setInterval(() => {\n\t\t\tsetCounter(previousCounter => previousCounter + 1);\n\t\t}, 100);\n\n\t\treturn () => {\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, []);\n\n\treturn <Text color=\"green\">{counter} tests passed</Text>;\n};\n\nrender(<Counter />);\n```\n\n<img src=\"media/demo.svg\" width=\"600\">\n\n## Who's Using Ink?\n\n- [Claude Code](https://github.com/anthropics/claude-code) - An agentic coding tool made by Anthropic.\n- [Gemini CLI](https://github.com/google-gemini/gemini-cli) - An agentic coding tool made by Google.\n- [GitHub Copilot CLI](https://github.com/features/copilot/cli) - Just say what you want the shell to do.\n- [Canva CLI](https://www.canva.dev/docs/apps/canva-cli/) - CLI for creating and managing Canva Apps.\n- [Cloudflare's Wrangler](https://github.com/cloudflare/wrangler2) - The CLI for Cloudflare Workers.\n- [Linear](https://linear.app) - Linear built an internal CLI for managing deployments, configs, and other housekeeping tasks.\n- [Gatsby](https://www.gatsbyjs.org) - Gatsby is a modern web framework for blazing-fast websites.\n- [tap](https://node-tap.org) - A Test-Anything-Protocol library for JavaScript.\n- [Terraform CDK](https://github.com/hashicorp/terraform-cdk) - Cloud Development Kit (CDK) for HashiCorp Terraform.\n- [Specify CLI](https://specifyapp.com) - Automate the distribution of your design tokens.\n- [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).\n- [Typewriter](https://github.com/segmentio/typewriter) - Generates strongly-typed [Segment](https://segment.com) analytics clients from arbitrary JSON Schema.\n- [Prisma](https://www.prisma.io) - The unified data layer for modern applications.\n- [Blitz](https://blitzjs.com) - The Fullstack React Framework.\n- [New York Times](https://github.com/nytimes/kyt) - NYT uses Ink's `kyt` - a toolkit that encapsulates and manages the configuration for web apps.\n- [tink](https://github.com/npm/tink) - A next-generation runtime and package manager.\n- [Inkle](https://github.com/jrr/inkle) - A Wordle game.\n- [loki](https://github.com/oblador/loki) - Visual regression testing tool for Storybook.\n- [Bit](https://github.com/teambit/bit) - Build, distribute, and collaborate on components.\n- [Remirror](https://github.com/remirror/remirror) - Your friendly, world-class editor toolkit.\n- [Prime](https://github.com/birkir/prime) - Open-source GraphQL CMS.\n- [emoj](https://github.com/sindresorhus/emoj) - Find relevant emojis.\n- [emma](https://github.com/maticzav/emma-cli) - Find and install npm packages easily.\n- [npm-check-extras](https://github.com/akgondber/npm-check-extras) - Check for outdated and unused dependencies, and run update/delete actions on selected ones.\n- [swiff](https://github.com/simple-integrated-marketing/swiff) - Multi-environment command-line tools for time-saving web developers.\n- [share](https://github.com/marionebl/share-cli) - Share files quickly.\n- [Kubelive](https://github.com/ameerthehacker/kubelive) - A CLI for Kubernetes that provides live data about the cluster and its resources.\n- [changelog-view](https://github.com/jdeniau/changelog-view) - View changelogs.\n- [cfpush](https://github.com/mamachanko/cfpush) - Interactive Cloud Foundry tutorial.\n- [startd](https://github.com/mgrip/startd) - Turn your React component into a web app.\n- [wiki-cli](https://github.com/hexrcs/wiki-cli) - Search Wikipedia and read article summaries.\n- [garson](https://github.com/goliney/garson) - Build interactive, config-based command-line interfaces.\n- [git-contrib-calendar](https://github.com/giannisp/git-contrib-calendar) - Display a contributions calendar for any Git repository.\n- [gitgud](https://github.com/GitGud-org/GitGud) - Interactive command-line GUI for Git.\n- [Autarky](https://github.com/pranshuchittora/autarky) - Find and delete old `node_modules` directories to free up disk space.\n- [fast-cli](https://github.com/sindresorhus/fast-cli) - Test your download and upload speeds.\n- [tasuku](https://github.com/privatenumber/tasuku) - Minimal task runner.\n- [mnswpr](https://github.com/mordv/mnswpr) - A Minesweeper game.\n- [lrn](https://github.com/krychu/lrn) - Learning by repetition.\n- [turdle](https://github.com/mynameisankit/turdle) - A Wordle game.\n- [Shopify CLI](https://github.com/Shopify/cli) - Build apps, themes, and storefronts for the Shopify platform.\n- [ToDesktop CLI](https://www.todesktop.com/electron) - All-in-one platform for building Electron apps.\n- [Walle](https://github.com/Pobepto/walle) - A full-featured crypto wallet for EVM networks.\n- [Sudoku](https://github.com/mrozio13pl/sudoku-in-terminal) - A Sudoku game.\n- [Sea Trader](https://github.com/zyishai/sea-trader) - A Taipan!-inspired trading simulator game.\n- [srtd](https://github.com/t1mmen/srtd) - Live-reloading SQL templates for Supabase projects.\n- [tweakcc](https://github.com/Piebald-AI/tweakcc) - Customize your Claude Code styling.\n- [argonaut](https://github.com/darksworm/argonaut) - Manage Argo CD resources.\n- [Qodo Command](https://github.com/qodo-ai/command) - Build, run, and manage AI agents.\n- [Nanocoder](https://github.com/nano-collective/nanocoder) - A community-built, local-first AI coding agent with multi-provider support.\n- [dev3000](https://github.com/vercel-labs/dev3000) - An AI agent MCP orchestrator and developer browser.\n- [Neovate Code](https://github.com/neovateai/neovate-code) - An agentic coding tool made by AntGroup.\n- [instagram-cli](https://github.com/supreme-gg-gg/instagram-cli) - Instagram client.\n- [ElevenLabs CLI](https://github.com/elevenlabs/cli) - ElevenLabs agents client.\n- [SSH AI Chat](https://github.com/miantiao-me/ssh-ai-chat) - Chat with AI over SSH.\n\n*(PRs welcome. Append new entries at the end. Repos must have 100+ stars and showcase Ink beyond a basic list picker.)*\n\n## Contents\n\n- [Getting Started](#getting-started)\n- [App Lifecycle](#app-lifecycle)\n- [Components](#components)\n  - [`<Text>`](#text)\n  - [`<Box>`](#box)\n  - [`<Newline>`](#newline)\n  - [`<Spacer>`](#spacer)\n  - [`<Static>`](#static)\n  - [`<Transform>`](#transform)\n- [Hooks](#hooks)\n  - [`useInput`](#useinputinputhandler-options)\n  - [`usePaste`](#usepastehandler-options)\n  - [`useApp`](#useapp)\n  - [`useStdin`](#usestdin)\n  - [`useStdout`](#usestdout)\n  - [`useBoxMetrics`](#useboxmetricsref)\n  - [`useStderr`](#usestderr)\n  - [`useWindowSize`](#usewindowsize)\n  - [`useFocus`](#usefocusoptions)\n  - [`useFocusManager`](#usefocusmanager)\n  - [`useCursor`](#usecursor)\n- [API](#api)\n- [Testing](#testing)\n- [Using React Devtools](#using-react-devtools)\n- [Screen Reader Support](#screen-reader-support)\n- [Useful Components](#useful-components)\n- [Useful Hooks](#useful-hooks)\n- [Recipes](#recipes)\n- [Examples](#examples)\n- [Continuous Integration](#continuous-integration)\n\n## Getting Started\n\nUse [create-ink-app](https://github.com/vadimdemedes/create-ink-app) to quickly scaffold a new Ink-based CLI.\n\n```sh\nnpx create-ink-app my-ink-cli\n```\n\nAlternatively, create a TypeScript project:\n\n```sh\nnpx create-ink-app --typescript my-ink-cli\n```\n\n<details><summary>Manual JavaScript setup</summary>\n<p>\nInk requires the same Babel setup as you would do for regular React-based apps in the browser.\n\nSet up Babel with a React preset to ensure all examples in this readme work as expected.\nAfter [installing Babel](https://babeljs.io/docs/en/usage), install `@babel/preset-react` and insert the following configuration in `babel.config.json`:\n\n```sh\nnpm install --save-dev @babel/preset-react\n```\n\n```json\n{\n\t\"presets\": [\"@babel/preset-react\"]\n}\n```\n\nNext, create a file `source.js`, where you'll type code that uses Ink:\n\n```jsx\nimport React from 'react';\nimport {render, Text} from 'ink';\n\nconst Demo = () => <Text>Hello World</Text>;\n\nrender(<Demo />);\n```\n\nThen, transpile this file with Babel:\n\n```sh\nnpx babel source.js -o cli.js\n```\n\nNow you can run `cli.js` with Node.js:\n\n```sh\nnode cli\n```\n\nIf 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.\n\n</p>\n</details>\n\nInk 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.\nIt's important to remember that each element is a Flexbox container.\nThink of it as if every `<div>` in the browser had `display: flex`.\nSee [`<Box>`](#box) built-in component below for documentation on how to use Flexbox layouts in Ink.\nNote that all text must be wrapped in a [`<Text>`](#text) component.\n\n## App Lifecycle\n\nAn 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.\n\nTo 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).\n\nUse [`waitUntilExit()`](#waituntilexit) to run code after the app is unmounted:\n\n```jsx\nconst {waitUntilExit} = render(<MyApp />);\n\nawait waitUntilExit();\n\nconsole.log('App exited');\n```\n\n## Components\n\n### `<Text>`\n\nThis component can display text and change its style to make it bold, underlined, italic, or strikethrough.\n\n```jsx\nimport {render, Text} from 'ink';\n\nconst Example = () => (\n\t<>\n\t\t<Text color=\"green\">I am green</Text>\n\t\t<Text color=\"black\" backgroundColor=\"white\">\n\t\t\tI am black on white\n\t\t</Text>\n\t\t<Text color=\"#ffffff\">I am white</Text>\n\t\t<Text bold>I am bold</Text>\n\t\t<Text italic>I am italic</Text>\n\t\t<Text underline>I am underline</Text>\n\t\t<Text strikethrough>I am strikethrough</Text>\n\t\t<Text inverse>I am inversed</Text>\n\t</>\n);\n\nrender(<Example />);\n```\n\n> [!NOTE]\n> `<Text>` allows only text nodes and nested `<Text>` components inside of it. For example, `<Box>` component can't be used inside `<Text>`.\n\n#### color\n\nType: `string`\n\nChange text color.\nInk uses [chalk](https://github.com/chalk/chalk) under the hood, so all its functionality is supported.\n\n```jsx\n<Text color=\"green\">Green</Text>\n<Text color=\"#005cc5\">Blue</Text>\n<Text color=\"rgb(232, 131, 136)\">Red</Text>\n```\n\n<img src=\"media/text-color.jpg\" width=\"247\">\n\n#### backgroundColor\n\nType: `string`\n\nSame as `color` above, but for background.\n\n```jsx\n<Text backgroundColor=\"green\" color=\"white\">Green</Text>\n<Text backgroundColor=\"#005cc5\" color=\"white\">Blue</Text>\n<Text backgroundColor=\"rgb(232, 131, 136)\" color=\"white\">Red</Text>\n```\n\n<img src=\"media/text-backgroundColor.jpg\" width=\"226\">\n\n#### dimColor\n\nType: `boolean`\\\nDefault: `false`\n\nDim the color (make it less bright).\n\n```jsx\n<Text color=\"red\" dimColor>\n\tDimmed Red\n</Text>\n```\n\n<img src=\"media/text-dimColor.jpg\" width=\"138\">\n\n#### bold\n\nType: `boolean`\\\nDefault: `false`\n\nMake the text bold.\n\n#### italic\n\nType: `boolean`\\\nDefault: `false`\n\nMake the text italic.\n\n#### underline\n\nType: `boolean`\\\nDefault: `false`\n\nMake the text underlined.\n\n#### strikethrough\n\nType: `boolean`\\\nDefault: `false`\n\nMake the text crossed with a line.\n\n#### inverse\n\nType: `boolean`\\\nDefault: `false`\n\nInvert background and foreground colors.\n\n```jsx\n<Text inverse color=\"yellow\">\n\tInversed Yellow\n</Text>\n```\n\n<img src=\"media/text-inverse.jpg\" width=\"138\">\n\n#### wrap\n\nType: `string`\\\nAllowed values: `wrap` `truncate` `truncate-start` `truncate-middle` `truncate-end`\\\nDefault: `wrap`\n\nThis property tells Ink to wrap or truncate text if its width is larger than the container.\nIf `wrap` is passed (the default), Ink will wrap text and split it into multiple lines.\nIf `truncate-*` is passed, Ink will truncate text instead, resulting in one line of text with the rest cut off.\n\n```jsx\n<Box width={7}>\n\t<Text>Hello World</Text>\n</Box>\n//=> 'Hello\\nWorld'\n\n// `truncate` is an alias to `truncate-end`\n<Box width={7}>\n\t<Text wrap=\"truncate\">Hello World</Text>\n</Box>\n//=> 'Hello…'\n\n<Box width={7}>\n\t<Text wrap=\"truncate-middle\">Hello World</Text>\n</Box>\n//=> 'He…ld'\n\n<Box width={7}>\n\t<Text wrap=\"truncate-start\">Hello World</Text>\n</Box>\n//=> '…World'\n```\n\n### `<Box>`\n\n`<Box>` is an essential Ink component to build your layout.\nIt's like `<div style=\"display: flex\">` in the browser.\n\n```jsx\nimport {render, Box, Text} from 'ink';\n\nconst Example = () => (\n\t<Box margin={2}>\n\t\t<Text>This is a box with margin</Text>\n\t</Box>\n);\n\nrender(<Example />);\n```\n\n#### Dimensions\n\n##### width\n\nType: `number` `string`\n\nWidth of the element in spaces.\nYou can also set it as a percentage, which will calculate the width based on the width of the parent element.\n\n```jsx\n<Box width={4}>\n\t<Text>X</Text>\n</Box>\n//=> 'X   '\n```\n\n```jsx\n<Box width={10}>\n\t<Box width=\"50%\">\n\t\t<Text>X</Text>\n\t</Box>\n\t<Text>Y</Text>\n</Box>\n//=> 'X    Y'\n```\n\n##### height\n\nType: `number` `string`\n\nHeight of the element in lines (rows).\nYou can also set it as a percentage, which will calculate the height based on the height of the parent element.\n\n```jsx\n<Box height={4}>\n\t<Text>X</Text>\n</Box>\n//=> 'X\\n\\n\\n'\n```\n\n```jsx\n<Box height={6} flexDirection=\"column\">\n\t<Box height=\"50%\">\n\t\t<Text>X</Text>\n\t</Box>\n\t<Text>Y</Text>\n</Box>\n//=> 'X\\n\\n\\nY\\n\\n'\n```\n\n##### minWidth\n\nType: `number`\n\nSets a minimum width of the element.\nPercentages aren't supported yet; see https://github.com/facebook/yoga/issues/872.\n\n##### minHeight\n\nType: `number` `string`\n\nSets a minimum height of the element in lines (rows).\nYou can also set it as a percentage, which will calculate the minimum height based on the height of the parent element.\n\n##### maxWidth\n\nType: `number`\n\nSets a maximum width of the element.\nPercentages aren't supported yet; see https://github.com/facebook/yoga/issues/872.\n\n##### maxHeight\n\nType: `number` `string`\n\nSets a maximum height of the element in lines (rows).\nYou can also set it as a percentage, which will calculate the maximum height based on the height of the parent element.\n\n##### aspectRatio\n\nType: `number`\n\nDefines the aspect ratio (width/height) for the element.\n\nUse it with at least one size constraint (`width`, `height`, `minHeight`, or `maxHeight`) so Ink can derive the missing dimension.\n\n#### Padding\n\n##### paddingTop\n\nType: `number`\\\nDefault: `0`\n\nTop padding.\n\n##### paddingBottom\n\nType: `number`\\\nDefault: `0`\n\nBottom padding.\n\n##### paddingLeft\n\nType: `number`\\\nDefault: `0`\n\nLeft padding.\n\n##### paddingRight\n\nType: `number`\\\nDefault: `0`\n\nRight padding.\n\n##### paddingX\n\nType: `number`\\\nDefault: `0`\n\nHorizontal padding. Equivalent to setting `paddingLeft` and `paddingRight`.\n\n##### paddingY\n\nType: `number`\\\nDefault: `0`\n\nVertical padding. Equivalent to setting `paddingTop` and `paddingBottom`.\n\n##### padding\n\nType: `number`\\\nDefault: `0`\n\nPadding on all sides. Equivalent to setting `paddingTop`, `paddingBottom`, `paddingLeft` and `paddingRight`.\n\n```jsx\n<Box paddingTop={2}><Text>Top</Text></Box>\n<Box paddingBottom={2}><Text>Bottom</Text></Box>\n<Box paddingLeft={2}><Text>Left</Text></Box>\n<Box paddingRight={2}><Text>Right</Text></Box>\n<Box paddingX={2}><Text>Left and right</Text></Box>\n<Box paddingY={2}><Text>Top and bottom</Text></Box>\n<Box padding={2}><Text>Top, bottom, left and right</Text></Box>\n```\n\n#### Margin\n\n##### marginTop\n\nType: `number`\\\nDefault: `0`\n\nTop margin.\n\n##### marginBottom\n\nType: `number`\\\nDefault: `0`\n\nBottom margin.\n\n##### marginLeft\n\nType: `number`\\\nDefault: `0`\n\nLeft margin.\n\n##### marginRight\n\nType: `number`\\\nDefault: `0`\n\nRight margin.\n\n##### marginX\n\nType: `number`\\\nDefault: `0`\n\nHorizontal margin. Equivalent to setting `marginLeft` and `marginRight`.\n\n##### marginY\n\nType: `number`\\\nDefault: `0`\n\nVertical margin. Equivalent to setting `marginTop` and `marginBottom`.\n\n##### margin\n\nType: `number`\\\nDefault: `0`\n\nMargin on all sides. Equivalent to setting `marginTop`, `marginBottom`, `marginLeft` and `marginRight`.\n\n```jsx\n<Box marginTop={2}><Text>Top</Text></Box>\n<Box marginBottom={2}><Text>Bottom</Text></Box>\n<Box marginLeft={2}><Text>Left</Text></Box>\n<Box marginRight={2}><Text>Right</Text></Box>\n<Box marginX={2}><Text>Left and right</Text></Box>\n<Box marginY={2}><Text>Top and bottom</Text></Box>\n<Box margin={2}><Text>Top, bottom, left and right</Text></Box>\n```\n\n#### Gap\n\n#### gap\n\nType: `number`\\\nDefault: `0`\n\nSize of the gap between an element's columns and rows. A shorthand for `columnGap` and `rowGap`.\n\n```jsx\n<Box gap={1} width={3} flexWrap=\"wrap\">\n\t<Text>A</Text>\n\t<Text>B</Text>\n\t<Text>C</Text>\n</Box>\n// A B\n//\n// C\n```\n\n#### columnGap\n\nType: `number`\\\nDefault: `0`\n\nSize of the gap between an element's columns.\n\n```jsx\n<Box columnGap={1}>\n\t<Text>A</Text>\n\t<Text>B</Text>\n</Box>\n// A B\n```\n\n#### rowGap\n\nType: `number`\\\nDefault: `0`\n\nSize of the gap between an element's rows.\n\n```jsx\n<Box flexDirection=\"column\" rowGap={1}>\n\t<Text>A</Text>\n\t<Text>B</Text>\n</Box>\n// A\n//\n// B\n```\n\n#### Flex\n\n##### flexGrow\n\nType: `number`\\\nDefault: `0`\n\nSee [flex-grow](https://css-tricks.com/almanac/properties/f/flex-grow/).\n\n```jsx\n<Box>\n\t<Text>Label:</Text>\n\t<Box flexGrow={1}>\n\t\t<Text>Fills all remaining space</Text>\n\t</Box>\n</Box>\n```\n\n##### flexShrink\n\nType: `number`\\\nDefault: `1`\n\nSee [flex-shrink](https://css-tricks.com/almanac/properties/f/flex-shrink/).\n\n```jsx\n<Box width={20}>\n\t<Box flexShrink={2} width={10}>\n\t\t<Text>Will be 1/4</Text>\n\t</Box>\n\t<Box width={10}>\n\t\t<Text>Will be 3/4</Text>\n\t</Box>\n</Box>\n```\n\n##### flexBasis\n\nType: `number` `string`\n\nSee [flex-basis](https://css-tricks.com/almanac/properties/f/flex-basis/).\n\n```jsx\n<Box width={6}>\n\t<Box flexBasis={3}>\n\t\t<Text>X</Text>\n\t</Box>\n\t<Text>Y</Text>\n</Box>\n//=> 'X  Y'\n```\n\n```jsx\n<Box width={6}>\n\t<Box flexBasis=\"50%\">\n\t\t<Text>X</Text>\n\t</Box>\n\t<Text>Y</Text>\n</Box>\n//=> 'X  Y'\n```\n\n##### flexDirection\n\nType: `string`\\\nAllowed values: `row` `row-reverse` `column` `column-reverse`\n\nSee [flex-direction](https://css-tricks.com/almanac/properties/f/flex-direction/).\n\n```jsx\n<Box>\n\t<Box marginRight={1}>\n\t\t<Text>X</Text>\n\t</Box>\n\t<Text>Y</Text>\n</Box>\n// X Y\n\n<Box flexDirection=\"row-reverse\">\n\t<Text>X</Text>\n\t<Box marginRight={1}>\n\t\t<Text>Y</Text>\n\t</Box>\n</Box>\n// Y X\n\n<Box flexDirection=\"column\">\n\t<Text>X</Text>\n\t<Text>Y</Text>\n</Box>\n// X\n// Y\n\n<Box flexDirection=\"column-reverse\">\n\t<Text>X</Text>\n\t<Text>Y</Text>\n</Box>\n// Y\n// X\n```\n\n##### flexWrap\n\nType: `string`\\\nAllowed values: `nowrap` `wrap` `wrap-reverse`\n\nSee [flex-wrap](https://css-tricks.com/almanac/properties/f/flex-wrap/).\n\n```jsx\n<Box width={2} flexWrap=\"wrap\">\n\t<Text>A</Text>\n\t<Text>BC</Text>\n</Box>\n// A\n// B C\n```\n\n```jsx\n<Box flexDirection=\"column\" height={2} flexWrap=\"wrap\">\n\t<Text>A</Text>\n\t<Text>B</Text>\n\t<Text>C</Text>\n</Box>\n// A C\n// B\n```\n\n##### alignItems\n\nType: `string`\\\nAllowed values: `flex-start` `center` `flex-end` `stretch` `baseline`\n\nSee [align-items](https://css-tricks.com/almanac/properties/a/align-items/).\n\n```jsx\n<Box alignItems=\"flex-start\">\n\t<Box marginRight={1}>\n\t\t<Text>X</Text>\n\t</Box>\n\t<Text>\n\t\tA\n\t\t<Newline/>\n\t\tB\n\t\t<Newline/>\n\t\tC\n\t</Text>\n</Box>\n// X A\n//   B\n//   C\n\n<Box alignItems=\"center\">\n\t<Box marginRight={1}>\n\t\t<Text>X</Text>\n\t</Box>\n\t<Text>\n\t\tA\n\t\t<Newline/>\n\t\tB\n\t\t<Newline/>\n\t\tC\n\t</Text>\n</Box>\n//   A\n// X B\n//   C\n\n<Box alignItems=\"flex-end\">\n\t<Box marginRight={1}>\n\t\t<Text>X</Text>\n\t</Box>\n\t<Text>\n\t\tA\n\t\t<Newline/>\n\t\tB\n\t\t<Newline/>\n\t\tC\n\t</Text>\n</Box>\n//   A\n//   B\n// X C\n```\n\n##### alignSelf\n\nType: `string`\\\nDefault: `auto`\\\nAllowed values: `auto` `flex-start` `center` `flex-end` `stretch` `baseline`\n\nSee [align-self](https://css-tricks.com/almanac/properties/a/align-self/).\n\n```jsx\n<Box height={3}>\n\t<Box alignSelf=\"flex-start\">\n\t\t<Text>X</Text>\n\t</Box>\n</Box>\n// X\n//\n//\n\n<Box height={3}>\n\t<Box alignSelf=\"center\">\n\t\t<Text>X</Text>\n\t</Box>\n</Box>\n//\n// X\n//\n\n<Box height={3}>\n\t<Box alignSelf=\"flex-end\">\n\t\t<Text>X</Text>\n\t</Box>\n</Box>\n//\n//\n// X\n```\n\n##### alignContent\n\nType: `string`\\\nDefault: `flex-start`\\\nAllowed values: `flex-start` `flex-end` `center` `stretch` `space-between` `space-around` `space-evenly`\n\nDefines alignment between flex lines on the cross axis when `flexWrap` creates multiple lines.\nSee [align-content](https://css-tricks.com/almanac/properties/a/align-content/).\nUnlike 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.\n\n##### justifyContent\n\nType: `string`\\\nAllowed values: `flex-start` `center` `flex-end` `space-between` `space-around` `space-evenly`\n\nSee [justify-content](https://css-tricks.com/almanac/properties/j/justify-content/).\n\n```jsx\n<Box justifyContent=\"flex-start\">\n\t<Text>X</Text>\n</Box>\n// [X      ]\n\n<Box justifyContent=\"center\">\n\t<Text>X</Text>\n</Box>\n// [   X   ]\n\n<Box justifyContent=\"flex-end\">\n\t<Text>X</Text>\n</Box>\n// [      X]\n\n<Box justifyContent=\"space-between\">\n\t<Text>X</Text>\n\t<Text>Y</Text>\n</Box>\n// [X      Y]\n\n<Box justifyContent=\"space-around\">\n\t<Text>X</Text>\n\t<Text>Y</Text>\n</Box>\n// [  X   Y  ]\n\n<Box justifyContent=\"space-evenly\">\n\t<Text>X</Text>\n\t<Text>Y</Text>\n</Box>\n// [   X   Y   ]\n```\n\n#### Position\n\n##### position\n\nType: `string`\\\nAllowed values: `relative` `absolute` `static`\\\nDefault: `relative`\n\nControls how the element is positioned.\n\nWhen `position` is `static`, `top`, `right`, `bottom`, and `left` are ignored.\n\n##### top\n\nType: `number` `string`\n\nTop offset for positioned elements.\nYou can also set it as a percentage of the parent size.\n\n##### right\n\nType: `number` `string`\n\nRight offset for positioned elements.\nYou can also set it as a percentage of the parent size.\n\n##### bottom\n\nType: `number` `string`\n\nBottom offset for positioned elements.\nYou can also set it as a percentage of the parent size.\n\n##### left\n\nType: `number` `string`\n\nLeft offset for positioned elements.\nYou can also set it as a percentage of the parent size.\n\n#### Visibility\n\n##### display\n\nType: `string`\\\nAllowed values: `flex` `none`\\\nDefault: `flex`\n\nSet this property to `none` to hide the element.\n\n##### overflowX\n\nType: `string`\\\nAllowed values: `visible` `hidden`\\\nDefault: `visible`\n\nBehavior for an element's overflow in the horizontal direction.\n\n##### overflowY\n\nType: `string`\\\nAllowed values: `visible` `hidden`\\\nDefault: `visible`\n\nBehavior for an element's overflow in the vertical direction.\n\n##### overflow\n\nType: `string`\\\nAllowed values: `visible` `hidden`\\\nDefault: `visible`\n\nA shortcut for setting `overflowX` and `overflowY` at the same time.\n\n#### Borders\n\n##### borderStyle\n\nType: `string`\\\nAllowed values: `single` `double` `round` `bold` `singleDouble` `doubleSingle` `classic` | `BoxStyle`\n\nAdd a border with a specified style.\nIf `borderStyle` is `undefined` (the default), no border will be added.\nInk uses border styles from the [`cli-boxes`](https://github.com/sindresorhus/cli-boxes) module.\n\n```jsx\n<Box flexDirection=\"column\">\n\t<Box>\n\t\t<Box borderStyle=\"single\" marginRight={2}>\n\t\t\t<Text>single</Text>\n\t\t</Box>\n\n\t\t<Box borderStyle=\"double\" marginRight={2}>\n\t\t\t<Text>double</Text>\n\t\t</Box>\n\n\t\t<Box borderStyle=\"round\" marginRight={2}>\n\t\t\t<Text>round</Text>\n\t\t</Box>\n\n\t\t<Box borderStyle=\"bold\">\n\t\t\t<Text>bold</Text>\n\t\t</Box>\n\t</Box>\n\n\t<Box marginTop={1}>\n\t\t<Box borderStyle=\"singleDouble\" marginRight={2}>\n\t\t\t<Text>singleDouble</Text>\n\t\t</Box>\n\n\t\t<Box borderStyle=\"doubleSingle\" marginRight={2}>\n\t\t\t<Text>doubleSingle</Text>\n\t\t</Box>\n\n\t\t<Box borderStyle=\"classic\">\n\t\t\t<Text>classic</Text>\n\t\t</Box>\n\t</Box>\n</Box>\n```\n\n<img src=\"media/box-borderStyle.jpg\" width=\"521\">\n\nAlternatively, pass a custom border style like so:\n\n```jsx\n<Box\n\tborderStyle={{\n\t\ttopLeft: '↘',\n\t\ttop: '↓',\n\t\ttopRight: '↙',\n\t\tleft: '→',\n\t\tbottomLeft: '↗',\n\t\tbottom: '↑',\n\t\tbottomRight: '↖',\n\t\tright: '←'\n\t}}\n>\n\t<Text>Custom</Text>\n</Box>\n```\n\nSee example in [examples/borders](examples/borders/borders.tsx).\n\n##### borderColor\n\nType: `string`\n\nChange border color.\nA shorthand for setting `borderTopColor`, `borderRightColor`, `borderBottomColor`, and `borderLeftColor`.\n\n```jsx\n<Box borderStyle=\"round\" borderColor=\"green\">\n\t<Text>Green Rounded Box</Text>\n</Box>\n```\n\n<img src=\"media/box-borderColor.jpg\" width=\"228\">\n\n##### borderTopColor\n\nType: `string`\n\nChange top border color.\nAccepts the same values as [`color`](#color) in `<Text>` component.\n\n```jsx\n<Box borderStyle=\"round\" borderTopColor=\"green\">\n\t<Text>Hello world</Text>\n</Box>\n```\n\n##### borderRightColor\n\nType: `string`\n\nChange the right border color.\nAccepts the same values as [`color`](#color) in `<Text>` component.\n\n```jsx\n<Box borderStyle=\"round\" borderRightColor=\"green\">\n\t<Text>Hello world</Text>\n</Box>\n```\n\n##### borderBottomColor\n\nType: `string`\n\nChange the bottom border color.\nAccepts the same values as [`color`](#color) in `<Text>` component.\n\n```jsx\n<Box borderStyle=\"round\" borderBottomColor=\"green\">\n\t<Text>Hello world</Text>\n</Box>\n```\n\n##### borderLeftColor\n\nType: `string`\n\nChange the left border color.\nAccepts the same values as [`color`](#color) in `<Text>` component.\n\n```jsx\n<Box borderStyle=\"round\" borderLeftColor=\"green\">\n\t<Text>Hello world</Text>\n</Box>\n```\n\n##### borderDimColor\n\nType: `boolean`\\\nDefault: `false`\n\nDim the border color.\nA shorthand for setting `borderTopDimColor`, `borderBottomDimColor`, `borderLeftDimColor`, and `borderRightDimColor`.\n\n```jsx\n<Box borderStyle=\"round\" borderDimColor>\n\t<Text>Hello world</Text>\n</Box>\n```\n\n##### borderTopDimColor\n\nType: `boolean`\\\nDefault: `false`\n\nDim the top border color.\n\n```jsx\n<Box borderStyle=\"round\" borderTopDimColor>\n\t<Text>Hello world</Text>\n</Box>\n```\n\n##### borderBottomDimColor\n\nType: `boolean`\\\nDefault: `false`\n\nDim the bottom border color.\n\n```jsx\n<Box borderStyle=\"round\" borderBottomDimColor>\n\t<Text>Hello world</Text>\n</Box>\n```\n\n##### borderLeftDimColor\n\nType: `boolean`\\\nDefault: `false`\n\nDim the left border color.\n\n```jsx\n<Box borderStyle=\"round\" borderLeftDimColor>\n\t<Text>Hello world</Text>\n</Box>\n```\n\n##### borderRightDimColor\n\nType: `boolean`\\\nDefault: `false`\n\nDim the right border color.\n\n```jsx\n<Box borderStyle=\"round\" borderRightDimColor>\n\t<Text>Hello world</Text>\n</Box>\n```\n\n##### borderTop\n\nType: `boolean`\\\nDefault: `true`\n\nDetermines whether the top border is visible.\n\n##### borderRight\n\nType: `boolean`\\\nDefault: `true`\n\nDetermines whether the right border is visible.\n\n##### borderBottom\n\nType: `boolean`\\\nDefault: `true`\n\nDetermines whether the bottom border is visible.\n\n##### borderLeft\n\nType: `boolean`\\\nDefault: `true`\n\nDetermines whether the left border is visible.\n\n#### Background\n\n##### backgroundColor\n\nType: `string`\n\nBackground color for the element.\n\nAccepts the same values as [`color`](#color) in the `<Text>` component.\n\n```jsx\n<Box flexDirection=\"column\">\n\t<Box backgroundColor=\"red\" width={20} height={5} alignSelf=\"flex-start\">\n\t\t<Text>Red background</Text>\n\t</Box>\n\n\t<Box backgroundColor=\"#FF8800\" width={20} height={3} marginTop={1} alignSelf=\"flex-start\">\n\t\t<Text>Orange background</Text>\n\t</Box>\n\n\t<Box backgroundColor=\"rgb(0, 255, 0)\" width={20} height={3} marginTop={1} alignSelf=\"flex-start\">\n\t\t<Text>Green background</Text>\n\t</Box>\n</Box>\n```\n\nThe background color fills the entire `<Box>` area and is inherited by child `<Text>` components unless they specify their own `backgroundColor`.\n\n```jsx\n<Box backgroundColor=\"blue\" alignSelf=\"flex-start\">\n\t<Text>Blue inherited </Text>\n\t<Text backgroundColor=\"yellow\">Yellow override </Text>\n\t<Text>Blue inherited again</Text>\n</Box>\n```\n\nBackground colors work with borders and padding:\n\n```jsx\n<Box backgroundColor=\"cyan\" borderStyle=\"round\" padding={1} alignSelf=\"flex-start\">\n\t<Text>Background with border and padding</Text>\n</Box>\n```\n\nSee example in [examples/box-backgrounds](examples/box-backgrounds/box-backgrounds.tsx).\n\n### `<Newline>`\n\nAdds one or more newline (`\\n`) characters.\nMust be used within `<Text>` components.\n\n#### count\n\nType: `number`\\\nDefault: `1`\n\nNumber of newlines to insert.\n\n```jsx\nimport {render, Text, Newline} from 'ink';\n\nconst Example = () => (\n\t<Text>\n\t\t<Text color=\"green\">Hello</Text>\n\t\t<Newline />\n\t\t<Text color=\"red\">World</Text>\n\t</Text>\n);\n\nrender(<Example />);\n```\n\nOutput:\n\n```\nHello\nWorld\n```\n\n### `<Spacer>`\n\nA flexible space that expands along the major axis of its containing layout.\nIt's useful as a shortcut for filling all the available space between elements.\n\nFor 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.\n\n```jsx\nimport {render, Box, Text, Spacer} from 'ink';\n\nconst Example = () => (\n\t<Box>\n\t\t<Text>Left</Text>\n\t\t<Spacer />\n\t\t<Text>Right</Text>\n\t</Box>\n);\n\nrender(<Example />);\n```\n\nIn a vertical flex direction (`column`), it will position \"Top\" at the top of the container and push \"Bottom\" to the bottom.\nNote that the container needs to be tall enough to see this in effect.\n\n```jsx\nimport {render, Box, Text, Spacer} from 'ink';\n\nconst Example = () => (\n\t<Box flexDirection=\"column\" height={10}>\n\t\t<Text>Top</Text>\n\t\t<Spacer />\n\t\t<Text>Bottom</Text>\n\t</Box>\n);\n\nrender(<Example />);\n```\n\n### `<Static>`\n\n`<Static>` component permanently renders its output above everything else.\nIt's useful for displaying activity like completed tasks or logs - things that\ndon't change after they're rendered (hence the name \"Static\").\n\nIt's preferred to use `<Static>` for use cases like these when you can't know\nor control the number of items that need to be rendered.\n\nFor example, [Tap](https://github.com/tapjs/node-tap) uses `<Static>` to display\na list of completed tests. [Gatsby](https://github.com/gatsbyjs/gatsby) uses it\nto display a list of generated pages while still displaying a live progress bar.\n\n```jsx\nimport React, {useState, useEffect} from 'react';\nimport {render, Static, Box, Text} from 'ink';\n\nconst Example = () => {\n\tconst [tests, setTests] = useState([]);\n\n\tuseEffect(() => {\n\t\tlet completedTests = 0;\n\t\tlet timer;\n\n\t\tconst run = () => {\n\t\t\t// Fake 10 completed tests\n\t\t\tif (completedTests++ < 10) {\n\t\t\t\tsetTests(previousTests => [\n\t\t\t\t\t...previousTests,\n\t\t\t\t\t{\n\t\t\t\t\t\tid: previousTests.length,\n\t\t\t\t\t\ttitle: `Test #${previousTests.length + 1}`\n\t\t\t\t\t}\n\t\t\t\t]);\n\n\t\t\t\ttimer = setTimeout(run, 100);\n\t\t\t}\n\t\t};\n\n\t\trun();\n\n\t\treturn () => {\n\t\t\tclearTimeout(timer);\n\t\t};\n\t}, []);\n\n\treturn (\n\t\t<>\n\t\t\t{/* This part will be rendered once to the terminal */}\n\t\t\t<Static items={tests}>\n\t\t\t\t{test => (\n\t\t\t\t\t<Box key={test.id}>\n\t\t\t\t\t\t<Text color=\"green\">✔ {test.title}</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t)}\n\t\t\t</Static>\n\n\t\t\t{/* This part keeps updating as state changes */}\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text dimColor>Completed tests: {tests.length}</Text>\n\t\t\t</Box>\n\t\t</>\n\t);\n};\n\nrender(<Example />);\n```\n\n> [!NOTE]\n> `<Static>` only renders new items in the `items` prop and ignores items\nthat were previously rendered. This means that when you add new items to the `items`\narray, changes you make to previous items will not trigger a rerender.\n\nSee [examples/static](examples/static/static.tsx) for an example usage of `<Static>` component.\n\n#### items\n\nType: `Array`\n\nArray of items of any type to render using the function you pass as a component child.\n\n#### style\n\nType: `object`\n\nStyles to apply to a container of child elements.\nSee [`<Box>`](#box) for supported properties.\n\n```jsx\n<Static items={...} style={{padding: 1}}>\n\t{...}\n</Static>\n```\n\n#### children(item)\n\nType: `Function`\n\nFunction that is called to render every item in the `items` array.\nThe first argument is the item itself, and the second argument is the index of that item in the\n`items` array.\n\nNote that a `key` must be assigned to the root component.\n\n```jsx\n<Static items={['a', 'b', 'c']}>\n\t{(item, index) => {\n\t\t// This function is called for every item in ['a', 'b', 'c']\n\t\t// `item` is 'a', 'b', 'c'\n\t\t// `index` is 0, 1, 2\n\t\treturn (\n\t\t\t<Box key={index}>\n\t\t\t\t<Text>Item: {item}</Text>\n\t\t\t</Box>\n\t\t);\n\t}}\n</Static>\n```\n\n### `<Transform>`\n\nTransform a string representation of React components before they're written to output.\nFor 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).\nThese use cases can't accept React nodes as input; they expect a string.\nThat's what the `<Transform>` component does: it gives you an output string of its child components and lets you transform it in any way.\n\n> [!NOTE]\n> `<Transform>` must be applied only to `<Text>` children components and shouldn't change the dimensions of the output; otherwise, the layout will be incorrect.\n\n> [!IMPORTANT]\n> 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)).\n\n```jsx\nimport {render, Transform} from 'ink';\n\nconst Example = () => (\n\t<Transform transform={output => output.toUpperCase()}>\n\t\t<Text>Hello World</Text>\n\t</Transform>\n);\n\nrender(<Example />);\n```\n\nSince the `transform` function converts all characters to uppercase, the final output rendered to the terminal will be \"HELLO WORLD\", not \"Hello World\".\n\nWhen the output wraps to multiple lines, it can be helpful to know which line is being processed.\n\nFor example, to implement a hanging indent component, you can indent all the lines except for the first.\n\n```jsx\nimport {render, Transform} from 'ink';\n\nconst HangingIndent = ({indent = 4, children}) => (\n\t<Transform\n\t\ttransform={(line, index) =>\n\t\t\tindex === 0 ? line : ' '.repeat(indent) + line\n\t\t}\n\t>\n\t\t{children}\n\t</Transform>\n);\n\nconst text =\n\t'WHEN I WROTE the following pages, or rather the bulk of them, ' +\n\t'I lived alone, in the woods, a mile from any neighbor, in a ' +\n\t'house which I had built myself, on the shore of Walden Pond, ' +\n\t'in Concord, Massachusetts, and earned my living by the labor ' +\n\t'of my hands only. I lived there two years and two months. At ' +\n\t'present I am a sojourner in civilized life again.';\n\nrender(\n\t<HangingIndent indent={4}>\n\t\t{text}\n\t</HangingIndent>\n);\n```\n\n#### transform(outputLine, index)\n\nType: `Function`\n\nFunction that transforms children output.\nIt accepts children and must return transformed children as well.\n\n##### children\n\nType: `string`\n\nOutput of child components.\n\n##### index\n\nType: `number`\n\nThe zero-indexed line number of the line that's currently being transformed.\n\n## Hooks\n\n### useInput(inputHandler, options?)\n\nA React hook that returns `void` and handles user input.\nIt's a more convenient alternative to using `useStdin` and listening for `data` events.\nThe callback you pass to `useInput` is called for each character when the user enters any input.\nHowever, 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`.\nYou can find a full example of using `useInput` at [examples/use-input](examples/use-input/use-input.tsx).\n\n```jsx\nimport {useInput} from 'ink';\n\nconst UserInput = () => {\n\tuseInput((input, key) => {\n\t\tif (input === 'q') {\n\t\t\t// Exit program\n\t\t}\n\n\t\tif (key.leftArrow) {\n\t\t\t// Left arrow key pressed\n\t\t}\n\t});\n\n\treturn …\n};\n```\n\n#### inputHandler(input, key)\n\nType: `Function`\n\nThe handler function that you pass to `useInput` receives two arguments:\n\n##### input\n\nType: `string`\n\nThe input that the program received.\n\n##### key\n\nType: `object`\n\nHandy information about a key that was pressed.\n\n###### key.leftArrow\n\n###### key.rightArrow\n\n###### key.upArrow\n\n###### key.downArrow\n\nType: `boolean`\\\nDefault: `false`\n\nIf an arrow key was pressed, the corresponding property will be `true`.\nFor example, if the user presses the left arrow key, `key.leftArrow` equals `true`.\n\n###### key.return\n\nType: `boolean`\\\nDefault: `false`\n\nReturn (Enter) key was pressed.\n\n###### key.escape\n\nType: `boolean`\\\nDefault: `false`\n\nEscape key was pressed.\n\n###### key.ctrl\n\nType: `boolean`\\\nDefault: `false`\n\nCtrl key was pressed.\n\n###### key.shift\n\nType: `boolean`\\\nDefault: `false`\n\nShift key was pressed.\n\n###### key.tab\n\nType: `boolean`\\\nDefault: `false`\n\nTab key was pressed.\n\n###### key.backspace\n\nType: `boolean`\\\nDefault: `false`\n\nBackspace key was pressed.\n\n###### key.delete\n\nType: `boolean`\\\nDefault: `false`\n\nDelete key was pressed.\n\n###### key.pageDown\n\n###### key.pageUp\n\nType: `boolean`\\\nDefault: `false`\n\nIf the Page Up or Page Down key was pressed, the corresponding property will be `true`.\nFor example, if the user presses Page Down, `key.pageDown` equals `true`.\n\n###### key.home\n\n###### key.end\n\nType: `boolean`\\\nDefault: `false`\n\nIf the Home or End key was pressed, the corresponding property will be `true`.\nFor example, if the user presses End, `key.end` equals `true`.\n\n###### key.meta\n\nType: `boolean`\\\nDefault: `false`\n\n[Meta key](https://en.wikipedia.org/wiki/Meta_key) was pressed.\n\n###### key.super\n\nType: `boolean`\\\nDefault: `false`\n\nSuper key (Cmd on macOS, Win on Windows) was pressed. Requires [kitty keyboard protocol](#kittykeyboard).\n\n###### key.hyper\n\nType: `boolean`\\\nDefault: `false`\n\nHyper key was pressed. Requires [kitty keyboard protocol](#kittykeyboard).\n\n###### key.capsLock\n\nType: `boolean`\\\nDefault: `false`\n\nCaps Lock was active. Requires [kitty keyboard protocol](#kittykeyboard).\n\n###### key.numLock\n\nType: `boolean`\\\nDefault: `false`\n\nNum Lock was active. Requires [kitty keyboard protocol](#kittykeyboard).\n\n###### key.eventType\n\nType: `'press' | 'repeat' | 'release'`\\\nDefault: `undefined`\n\nThe type of key event. Only available with [kitty keyboard protocol](#kittykeyboard). Without the protocol, this property is `undefined`.\n\n#### options\n\nType: `object`\n\n##### isActive\n\nType: `boolean`\\\nDefault: `true`\n\nEnable or disable capturing of user input.\nUseful when there are multiple `useInput` hooks used at once to avoid handling the same input several times.\n\n### usePaste(handler, options?)\n\nA 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.\n\n`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.\n\n```jsx\nimport {useInput, usePaste} from 'ink';\n\nconst MyInput = () => {\n\tuseInput((input, key) => {\n\t\t// Only receives typed characters and key events, not pasted text.\n\t\tif (key.return) {\n\t\t\t// Submit\n\t\t}\n\t});\n\n\tusePaste((text) => {\n\t\t// Receives the full pasted string, including newlines.\n\t\tconsole.log('Pasted:', text);\n\t});\n\n\treturn …\n};\n```\n\n#### handler(text)\n\nType: `Function`\n\nCalled 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.\n\n##### text\n\nType: `string`\n\nThe pasted text.\n\n#### options\n\nType: `object`\n\n##### isActive\n\nType: `boolean`\\\nDefault: `true`\n\nEnable or disable the paste handler. Useful when multiple components use `usePaste` and only one should be active at a time.\n\n### useApp()\n\nA React hook that returns app lifecycle methods.\n\n#### exit(errorOrResult?)\n\nType: `Function`\n\nExit (unmount) the whole Ink app.\n\n##### errorOrResult\n\nType: `Error | unknown`\n\nOptional value that controls how [`waitUntilExit`](#waituntilexit) settles:\n- `exit()` resolves with `undefined`.\n- `exit(error)` rejects when `error` is an `Error`.\n- `exit(value)` resolves with `value`.\n\n```js\nimport {useEffect} from 'react';\nimport {useApp} from 'ink';\n\nconst Example = () => {\n\tconst {exit} = useApp();\n\n\t// Exit the app after 5 seconds\n\tuseEffect(() => {\n\t\tsetTimeout(() => {\n\t\t\texit();\n\t\t}, 5000);\n\t}, [exit]);\n\n\treturn …\n};\n```\n\n#### waitUntilRenderFlush()\n\nType: `Function`\n\nReturns a promise that settles after pending render output is flushed to stdout.\n\n```js\nimport {useEffect} from 'react';\nimport {useApp} from 'ink';\n\nconst Example = () => {\n\tconst {waitUntilRenderFlush} = useApp();\n\n\tuseEffect(() => {\n\t\tvoid (async () => {\n\t\t\tawait waitUntilRenderFlush();\n\t\t\trunNextCommand();\n\t\t})();\n\t}, [waitUntilRenderFlush]);\n\n\treturn …;\n};\n```\n\n### useStdin()\n\nA React hook that returns the stdin stream and stdin-related utilities.\n\n#### stdin\n\nType: `stream.Readable`\\\nDefault: `process.stdin`\n\nThe stdin stream passed to `render()` in `options.stdin`, or `process.stdin` by default.\nUseful if your app needs to handle user input.\n\n```js\nimport {useStdin} from 'ink';\n\nconst Example = () => {\n\tconst {stdin} = useStdin();\n\n\treturn …\n};\n```\n\n#### isRawModeSupported\n\nType: `boolean`\n\nA boolean flag determining if the current `stdin` supports `setRawMode`.\nA component using `setRawMode` might want to use `isRawModeSupported` to nicely fall back in environments where raw mode is not supported.\n\n```jsx\nimport {useStdin} from 'ink';\n\nconst Example = () => {\n\tconst {isRawModeSupported} = useStdin();\n\n\treturn isRawModeSupported ? (\n\t\t<MyInputComponent />\n\t) : (\n\t\t<MyComponentThatDoesntUseInput />\n\t);\n};\n```\n\n#### setRawMode(isRawModeEnabled)\n\nType: `function`\n\n##### isRawModeEnabled\n\nType: `boolean`\n\nSee [`setRawMode`](https://nodejs.org/api/tty.html#tty_readstream_setrawmode_mode).\nInk 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`.\n\n**Warning:** This function will throw unless the current `stdin` supports `setRawMode`. Use [`isRawModeSupported`](#israwmodesupported) to detect `setRawMode` support.\n\n```js\nimport {useStdin} from 'ink';\n\nconst Example = () => {\n\tconst {setRawMode} = useStdin();\n\n\tuseEffect(() => {\n\t\tsetRawMode(true);\n\n\t\treturn () => {\n\t\t\tsetRawMode(false);\n\t\t};\n\t});\n\n\treturn …\n};\n```\n\n### useStdout()\n\nA React hook that returns the stdout stream where Ink renders your app and stdout-related utilities.\n\n#### stdout\n\nType: `stream.Writable`\\\nDefault: `process.stdout`\n\n```js\nimport {useStdout} from 'ink';\n\nconst Example = () => {\n\tconst {stdout} = useStdout();\n\n\treturn …\n};\n```\n\n#### write(data)\n\nWrite any string to stdout while preserving Ink's output.\nIt's useful when you want to display external information outside of Ink's rendering and ensure there's no conflict between the two.\nIt's similar to `<Static>`, except it can't accept components; it only works with strings.\n\n##### data\n\nType: `string`\n\nData to write to stdout.\n\n```js\nimport {useStdout} from 'ink';\n\nconst Example = () => {\n\tconst {write} = useStdout();\n\n\tuseEffect(() => {\n\t\t// Write a single message to stdout, above Ink's output\n\t\twrite('Hello from Ink to stdout\\n');\n\t}, []);\n\n\treturn …\n};\n```\n\nSee additional usage example in [examples/use-stdout](examples/use-stdout/use-stdout.tsx).\n\n### useBoxMetrics(ref)\n\nA React hook that returns the current layout metrics for a tracked box element.\nIt updates when layout changes (for example terminal resize, sibling/content changes, or position changes).\n\nUse `hasMeasured` to detect when the currently tracked element has been measured.\n\n#### ref\n\nType: `React.RefObject<DOMElement>`\n\nA ref to the `<Box>` element to track.\n\n```jsx\nimport {useRef} from 'react';\nimport {Box, Text, useBoxMetrics} from 'ink';\n\nconst Example = () => {\n\tconst ref = useRef(null);\n\tconst {width, height, left, top, hasMeasured} = useBoxMetrics(ref);\n\n\treturn (\n\t\t<Box ref={ref}>\n\t\t\t<Text>\n\t\t\t\t{hasMeasured ? `${width}x${height} at ${left},${top}` : 'Measuring...'}\n\t\t\t</Text>\n\t\t</Box>\n\t);\n};\n```\n\n#### width\n\nType: `number`\n\nElement width.\n\n#### height\n\nType: `number`\n\nElement height.\n\n#### left\n\nType: `number`\n\nDistance from the left edge of the parent.\n\n#### top\n\nType: `number`\n\nDistance from the top edge of the parent.\n\n#### hasMeasured\n\nType: `boolean`\n\nWhether the currently tracked element has been measured.\n\n> [!NOTE]\n> 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.\n\n### useStderr()\n\nA React hook that returns the stderr stream and stderr-related utilities.\n\n#### stderr\n\nType: `stream.Writable`\\\nDefault: `process.stderr`\n\nStderr stream.\n\n```js\nimport {useStderr} from 'ink';\n\nconst Example = () => {\n\tconst {stderr} = useStderr();\n\n\treturn …\n};\n```\n\n#### write(data)\n\nWrite any string to stderr while preserving Ink's output.\n\nIt's useful when you want to display external information outside of Ink's rendering and ensure there's no conflict between the two.\nIt's similar to `<Static>`, except it can't accept components; it only works with strings.\n\n##### data\n\nType: `string`\n\nData to write to stderr.\n\n```js\nimport {useStderr} from 'ink';\n\nconst Example = () => {\n\tconst {write} = useStderr();\n\n\tuseEffect(() => {\n\t\t// Write a single message to stderr, above Ink's output\n\t\twrite('Hello from Ink to stderr\\n');\n\t}, []);\n\n\treturn …\n};\n```\n\n### useWindowSize()\n\nA React hook that returns the current terminal dimensions and re-renders the component whenever the terminal is resized.\n\n```js\nimport {Text, useWindowSize} from 'ink';\n\nconst Example = () => {\n\tconst {columns, rows} = useWindowSize();\n\n\treturn <Text>{columns}x{rows}</Text>;\n};\n```\n\n#### columns\n\nType: `number`\n\nNumber of columns (horizontal character cells).\n\n#### rows\n\nType: `number`\n\nNumber of rows (vertical character cells).\n\n### useFocus(options?)\n\nA React hook that returns focus state and focus controls for the current component.\nA 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.\nIf there are multiple components that execute the `useFocus` hook, focus will be given to them in the order in which these components are rendered.\nThis hook returns an object with an `isFocused` boolean property, which determines whether this component is focused.\n\n#### options\n\n##### autoFocus\n\nType: `boolean`\\\nDefault: `false`\n\nAuto-focus this component if there's no active (focused) component right now.\n\n##### isActive\n\nType: `boolean`\\\nDefault: `true`\n\nEnable or disable this component's focus, while still maintaining its position in the list of focusable components.\nThis is useful for inputs that are temporarily disabled.\n\n##### id\n\nType: `string`\\\nRequired: `false`\n\nSet 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.\n\n```jsx\nimport {render, useFocus, Text} from 'ink';\n\nconst Example = () => {\n\tconst {isFocused} = useFocus();\n\n\treturn <Text>{isFocused ? 'I am focused' : 'I am not focused'}</Text>;\n};\n\nrender(<Example />);\n```\n\nSee 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).\n\n### useFocusManager()\n\nA React hook that returns methods to manage focus across focusable components.\n\n#### enableFocus()\n\nEnable focus management for all components.\n\n> [!NOTE]\n> You don't need to call this method manually unless you've disabled focus management. Focus management is enabled by default.\n\n```js\nimport {useFocusManager} from 'ink';\n\nconst Example = () => {\n\tconst {enableFocus} = useFocusManager();\n\n\tuseEffect(() => {\n\t\tenableFocus();\n\t}, []);\n\n\treturn …\n};\n```\n\n#### disableFocus()\n\nDisable focus management for all components.\nThe currently active component (if there's one) will lose its focus.\n\n```js\nimport {useFocusManager} from 'ink';\n\nconst Example = () => {\n\tconst {disableFocus} = useFocusManager();\n\n\tuseEffect(() => {\n\t\tdisableFocus();\n\t}, []);\n\n\treturn …\n};\n```\n\n#### focusNext()\n\nSwitch focus to the next focusable component.\nIf there's no active component right now, focus will be given to the first focusable component.\nIf the active component is the last in the list of focusable components, focus will be switched to the first focusable component.\n\n> [!NOTE]\n> Ink calls this method when user presses <kbd>Tab</kbd>.\n\n```js\nimport {useFocusManager} from 'ink';\n\nconst Example = () => {\n\tconst {focusNext} = useFocusManager();\n\n\tuseEffect(() => {\n\t\tfocusNext();\n\t}, []);\n\n\treturn …\n};\n```\n\n#### focusPrevious()\n\nSwitch focus to the previous focusable component.\nIf there's no active component right now, focus will be given to the first focusable component.\nIf the active component is the first in the list of focusable components, focus will be switched to the last focusable component.\n\n> [!NOTE]\n> Ink calls this method when user presses <kbd>Shift</kbd>+<kbd>Tab</kbd>.\n\n```js\nimport {useFocusManager} from 'ink';\n\nconst Example = () => {\n\tconst {focusPrevious} = useFocusManager();\n\n\tuseEffect(() => {\n\t\tfocusPrevious();\n\t}, []);\n\n\treturn …\n};\n```\n\n#### focus(id)\n\n##### id\n\nType: `string`\n\nSwitch focus to the component with the given [`id`](#id).\nIf there's no component with that ID, focus is not changed.\n\n```js\nimport {useFocusManager, useInput} from 'ink';\n\nconst Example = () => {\n\tconst {focus} = useFocusManager();\n\n\tuseInput(input => {\n\t\tif (input === 's') {\n\t\t\t// Focus the component with focus ID 'someId'\n\t\t\tfocus('someId');\n\t\t}\n\t});\n\n\treturn …\n};\n```\n\n#### activeId\n\nType: `string | undefined`\n\nThe ID of the currently focused component, or `undefined` if no component is focused.\n\n```js\nimport {Text, useFocusManager} from 'ink';\n\nconst Example = () => {\n\tconst {activeId} = useFocusManager();\n\n\treturn <Text>Focused: {activeId ?? 'none'}</Text>;\n};\n```\n\n### useCursor()\n\nA React hook that returns methods to control the terminal cursor position after each render.\nThis is essential for IME (Input Method Editor) support, where the composing character is displayed at the cursor location.\n\n```jsx\nimport {useState} from 'react';\nimport {Box, Text, useCursor} from 'ink';\nimport stringWidth from 'string-width';\n\nconst TextInput = () => {\n\tconst [text, setText] = useState('');\n\tconst {setCursorPosition} = useCursor();\n\n\tconst prompt = '> ';\n\tsetCursorPosition({x: stringWidth(prompt + text), y: 1});\n\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>Type here:</Text>\n\t\t\t<Text>{prompt}{text}</Text>\n\t\t</Box>\n\t);\n};\n```\n\n#### setCursorPosition(position)\n\nSet the cursor position relative to the Ink output. Pass `undefined` to hide the cursor.\n\n##### position\n\nType: `object | undefined`\n\nUse [`string-width`](https://github.com/sindresorhus/string-width) to calculate `x` for strings containing wide characters (CJK, emoji).\n\nSee a full example at [examples/cursor-ime](examples/cursor-ime/cursor-ime.tsx).\n\n###### x\n\nType: `number`\n\nColumn position (0-based).\n\n###### y\n\nType: `number`\n\nRow position from the top of the Ink output (0 = first line).\n\n### useIsScreenReaderEnabled()\n\nA React hook that returns whether a screen reader is enabled.\nThis is useful when you want to render different output for screen readers.\n\n```jsx\nimport {useIsScreenReaderEnabled, Text} from 'ink';\n\nconst Example = () => {\n\tconst isScreenReaderEnabled = useIsScreenReaderEnabled();\n\n\treturn (\n\t\t<Text>\n\t\t\t{isScreenReaderEnabled\n\t\t\t\t? 'Screen reader is enabled'\n\t\t\t\t: 'Screen reader is disabled'}\n\t\t</Text>\n\t);\n};\n```\n\n## API\n\n#### render(tree, options?)\n\nReturns: [`Instance`](#instance)\n\nMount a component and render the output.\n\n##### tree\n\nType: `ReactNode`\n\n##### options\n\nType: `object`\n\n###### stdout\n\nType: `stream.Writable`\\\nDefault: `process.stdout`\n\nOutput stream where the app will be rendered.\n\n###### stdin\n\nType: `stream.Readable`\\\nDefault: `process.stdin`\n\nInput stream where app will listen for input.\n\n###### stderr\n\nType: `stream.Writable`\\\nDefault: `process.stderr`\n\nError stream.\n\n###### exitOnCtrlC\n\nType: `boolean`\\\nDefault: `true`\n\nConfigure whether Ink should listen for Ctrl+C keyboard input and exit the app.\nThis 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.\n\n###### patchConsole\n\nType: `boolean`\\\nDefault: `true`\n\nPatch console methods to ensure console output doesn't mix with Ink's output.\nWhen 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.\nThat way, both are visible and don't overlap each other.\n\nOnce 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.\n\nThis 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.\n\n###### onRender\n\nType: `({renderTime: number}) => void`\\\nDefault: `undefined`\n\nRuns the given callback after each render and re-render with render metrics.\nThis callback runs after Ink commits a frame, but it does not wait for `stdout`/`stderr` stream callbacks.\nTo run code after output is flushed, use [`waitUntilRenderFlush()`](#waituntilrenderflush).\n\n###### isScreenReaderEnabled\n\nType: `boolean`\\\nDefault: `process.env['INK_SCREEN_READER'] === 'true'`\n\nEnable screen reader support. See [Screen Reader Support](#screen-reader-support).\n\n###### debug\n\nType: `boolean`\\\nDefault: `false`\n\nIf `true`, each update will be rendered as separate output, without replacing the previous one.\n\n###### maxFps\n\nType: `number`\\\nDefault: `30`\n\nMaximum frames per second for render updates.\nThis controls how frequently the UI can update to prevent excessive re-rendering.\nHigher values allow more frequent updates but may impact performance.\nSetting it to a lower value may be useful for components that update very frequently, to reduce CPU usage.\n\n###### incrementalRendering\n\nType: `boolean`\\\nDefault: `false`\n\nEnable incremental rendering mode which only updates changed lines instead of redrawing the entire output.\nThis can reduce flickering and improve performance for frequently updating UIs.\n\n###### concurrent\n\nType: `boolean`\\\nDefault: `false`\n\nEnable React Concurrent Rendering mode.\n\nWhen enabled:\n- Suspense boundaries work correctly with async data fetching\n- `useTransition` and `useDeferredValue` hooks are fully functional\n- Updates can be interrupted for higher priority work\n\n```jsx\nrender(<MyApp />, {concurrent: true});\n```\n\n> [!NOTE]\n> 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.\n\n###### interactive\n\nType: `boolean`\\\nDefault: `true` (`false` if in CI (detected via [`is-in-ci`](https://github.com/sindresorhus/is-in-ci)) or `stdout.isTTY` is falsy)\n\nOverride automatic interactive mode detection.\n\nBy 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.\n\nMost 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.\n\n> [!NOTE]\n> 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.\n\n```jsx\n// Use your own detection logic\nconst isInteractive = myCustomDetection();\nrender(<MyApp />, {interactive: isInteractive});\n```\n\n###### alternateScreen\n\nType: `boolean`\\\nDefault: `false`\n\nRender 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.\n\nNote: 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.\n\nInk 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.\n\nOnly works in interactive mode. Ignored when `interactive` is `false` or in a non-interactive environment (CI, piped stdout).\n\n> [!NOTE]\n> 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.\n\n```jsx\nrender(<MyApp />, {alternateScreen: true});\n```\n\n###### kittyKeyboard\n\nType: `object`\\\nDefault: `undefined`\n\nEnable 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).\n\n```jsx\nimport {render} from 'ink';\n\nrender(<MyApp />, {kittyKeyboard: {mode: 'auto'}});\n```\n\n```jsx\nimport {render} from 'ink';\n\nrender(<MyApp />, {\n\tkittyKeyboard: {\n\t\tmode: 'enabled',\n\t\tflags: ['disambiguateEscapeCodes', 'reportEventTypes'],\n\t},\n});\n```\n\n**kittyKeyboard.mode**\n\nType: `'auto' | 'enabled' | 'disabled'`\\\nDefault: `'auto'`\n\n- `'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.\n- `'enabled'`: Force enable the protocol. Both stdin and stdout must be TTYs.\n- `'disabled'`: Never enable the protocol.\n\n**kittyKeyboard.flags**\n\nType: `string[]`\\\nDefault: `['disambiguateEscapeCodes']`\n\nProtocol flags to request from the terminal. Pass an array of flag name strings.\n\nAvailable flags:\n- `'disambiguateEscapeCodes'` - Disambiguate escape codes\n- `'reportEventTypes'` - Report key press, repeat, and release events\n- `'reportAlternateKeys'` - Report alternate key encodings\n- `'reportAllKeysAsEscapeCodes'` - Report all keys as escape codes\n- `'reportAssociatedText'` - Report associated text with key events\n\n**Behavior notes**\n\nWhen the kitty keyboard protocol is enabled, input handling changes in several ways:\n\n- **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.\n- **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.\n- **Key disambiguation.** The protocol allows the terminal to distinguish between keys that normally produce the same escape sequence. For example:\n  - `Ctrl+I` vs `Tab` - without the protocol, both produce the same byte (`\\x09`). With the protocol, they are reported as distinct keys.\n  - `Shift+Enter` vs `Enter` - the shift modifier is correctly reported.\n  - `Escape` key vs `Ctrl+[` - these are disambiguated.\n- **Event types.** With the `reportEventTypes` flag, key press, repeat, and release events are distinguished via `key.eventType`.\n\n#### renderToString(tree, options?)\n\nReturns: `string`\n\nRender 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.\n\nUseful 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.\n\n```jsx\nimport {renderToString, Text, Box} from 'ink';\n\nconst output = renderToString(\n\t<Box padding={1}>\n\t\t<Text color=\"green\">Hello World</Text>\n\t</Box>,\n);\n\nconsole.log(output);\n```\n\n**Notes:**\n\n- 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.\n- `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.\n- `useLayoutEffect` callbacks fire synchronously during commit, so state updates they trigger **will** be reflected in the output.\n- The `<Static>` component is supported — its output is prepended to the dynamic output.\n- If a component throws during rendering, the error is propagated to the caller after cleanup.\n\n##### tree\n\nType: `ReactNode`\n\n##### options\n\nType: `object`\n\n###### columns\n\nType: `number`\\\nDefault: `80`\n\nWidth of the virtual terminal in columns. Controls where text wrapping occurs.\n\n```jsx\nconst output = renderToString(<Text>{'A'.repeat(100)}</Text>, {\n\tcolumns: 40,\n});\n// Text wraps at 40 columns\n```\n\n#### Instance\n\nThis is the object that `render()` returns.\n\n##### rerender(tree)\n\nReplace the previous root node with a new one or update the props of the current root node.\n\n###### tree\n\nType: `ReactNode`\n\n```jsx\n// Update props of the root node\nconst {rerender} = render(<Counter count={1} />);\nrerender(<Counter count={2} />);\n\n// Replace root node\nconst {rerender} = render(<OldCounter />);\nrerender(<NewCounter />);\n```\n\n##### unmount()\n\nManually unmount the whole Ink app.\n\n```jsx\nconst {unmount} = render(<MyApp />);\nunmount();\n```\n\n##### waitUntilExit()\n\nReturns a promise that settles when the app is unmounted.\n\nIt resolves with the value passed to `exit(value)` and rejects with the error passed to `exit(error)`.\nWhen `unmount()` is called manually, it settles after unmount-related stdout writes complete.\n\n```jsx\nconst {unmount, waitUntilExit} = render(<MyApp />);\n\nsetTimeout(unmount, 1000);\n\nawait waitUntilExit(); // resolves after `unmount()` is called\n```\n\n##### waitUntilRenderFlush()\n\nReturns a promise that settles after pending render output is flushed to stdout.\n\nUseful when you need to run code only after a frame is written:\n\n```jsx\nconst {rerender, waitUntilRenderFlush} = render(<MyApp step=\"loading\" />);\n\nrerender(<MyApp step=\"ready\" />);\nawait waitUntilRenderFlush(); // output for \"ready\" is flushed\n\nrunNextCommand();\n```\n\n##### cleanup()\n\nUnmount the current app and delete the internal Ink instance associated with the current `stdout`.\nThis is mostly useful for advanced cases (for example, tests) where you need `render()` to create a fresh instance for the same stream.\nUnlike deleting the internal instance directly, this also tears down terminal state such as the alternate screen.\n\n##### clear()\n\nClear output.\n\n```jsx\nconst {clear} = render(<MyApp />);\nclear();\n```\n\n#### measureElement(ref)\n\nMeasure the dimensions of a particular `<Box>` element.\nReturns an object with `width` and `height` properties.\nThis 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.\n\n> [!NOTE]\n> `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.\n\n##### ref\n\nType: `MutableRef`\n\nA reference to a `<Box>` element captured with the `ref` property.\nSee [Refs](https://reactjs.org/docs/refs-and-the-dom.html) for more information on how to capture references.\n\n```jsx\nimport {render, measureElement, Box, Text} from 'ink';\n\nconst Example = () => {\n\tconst ref = useRef();\n\n\tuseEffect(() => {\n\t\tconst {width, height} = measureElement(ref.current);\n\t\t// width = 100, height = 1\n\t}, []);\n\n\treturn (\n\t\t<Box width={100}>\n\t\t\t<Box ref={ref}>\n\t\t\t\t<Text>This box will stretch to 100 width</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n};\n\nrender(<Example />);\n```\n\n## Testing\n\nInk components are simple to test with [ink-testing-library](https://github.com/vadimdemedes/ink-testing-library).\nHere's a simple example that checks how the component is rendered:\n\n```jsx\nimport React from 'react';\nimport {Text} from 'ink';\nimport {render} from 'ink-testing-library';\n\nconst Test = () => <Text>Hello World</Text>;\nconst {lastFrame} = render(<Test />);\n\nlastFrame() === 'Hello World'; //=> true\n```\n\nCheck out [ink-testing-library](https://github.com/vadimdemedes/ink-testing-library) for more examples and full documentation.\n\n## Using React Devtools\n\n![](media/devtools.jpg)\n\nInk 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:\n\n```sh\nDEV=true my-cli\n```\n\nThen, start React Devtools itself:\n\n```sh\nnpx react-devtools\n```\n\nAfter it starts, you should see the component tree of your CLI.\nYou can even inspect and change the props of components, and see the results immediately in the CLI, without restarting it.\n\n> [!NOTE]\n> You must manually quit your CLI via <kbd>Ctrl</kbd>+<kbd>C</kbd> after you're done testing.\n\n## Screen Reader Support\n\nInk has basic support for screen readers.\n\nTo enable it, you can either pass the `isScreenReaderEnabled` option to the `render` function or set the `INK_SCREEN_READER` environment variable to `true`.\n\nInk implements a small subset of functionality from the [ARIA specification](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA).\n\n```jsx\nrender(<MyApp />, {isScreenReaderEnabled: true});\n```\n\nWhen screen reader support is enabled, Ink will try its best to generate a screen-reader-friendly output.\n\nFor example, for this code:\n\n```jsx\n<Box aria-role=\"checkbox\" aria-state={{checked: true}}>\n\t<Text>Accept terms and conditions</Text>\n</Box>\n```\n\nInk will generate the following output for screen readers:\n\n```\n(checked) checkbox: Accept terms and conditions\n```\n\nYou can also provide a custom label for screen readers if you want to render something different for them.\n\nFor example, if you are building a progress bar, you can use `aria-label` to provide a more descriptive label for screen readers.\n\n```jsx\n<Box>\n\t<Box width=\"50%\" height={1} backgroundColor=\"green\" />\n\t<Text aria-label=\"Progress: 50%\">50%</Text>\n</Box>\n```\n\nIn the example above, the screen reader will read \"Progress: 50%\" instead of \"50%\".\n\n### `aria-label`\n\nType: `string`\n\nA label for the element for screen readers.\n\n### `aria-hidden`\n\nType: `boolean`\\\nDefault: `false`\n\nHide the element from screen readers.\n\n##### aria-role\n\nType: `string`\n\nThe role of the element.\n\nSupported values:\n- `button`\n- `checkbox`\n- `radio`\n- `radiogroup`\n- `list`\n- `listitem`\n- `menu`\n- `menuitem`\n- `progressbar`\n- `tab`\n- `tablist`\n- `timer`\n- `toolbar`\n- `table`\n\n##### aria-state\n\nType: `object`\n\nThe state of the element.\n\nSupported values:\n- `checked` (boolean)\n- `disabled` (boolean)\n- `expanded` (boolean)\n- `selected` (boolean)\n\n## Creating Components\n\nWhen 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.\n\n### General Principles\n\n- **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.\n- **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.\n\nFor a practical example of building an accessible component, see the [ARIA example](/examples/aria/aria.tsx).\n\n## Useful Components\n\n- [ink-text-input](https://github.com/vadimdemedes/ink-text-input) - Text input.\n- [ink-spinner](https://github.com/vadimdemedes/ink-spinner) - Spinner.\n- [ink-select-input](https://github.com/vadimdemedes/ink-select-input) - Select (dropdown) input.\n- [ink-link](https://github.com/sindresorhus/ink-link) - Link.\n- [ink-gradient](https://github.com/sindresorhus/ink-gradient) - Gradient color.\n- [ink-big-text](https://github.com/sindresorhus/ink-big-text) - Awesome text.\n- [ink-picture](https://github.com/endernoke/ink-picture) - Display images.\n- [ink-tab](https://github.com/jdeniau/ink-tab) - Tab.\n- [ink-color-pipe](https://github.com/LitoMore/ink-color-pipe) - Create color text with simpler style strings.\n- [ink-multi-select](https://github.com/karaggeorge/ink-multi-select) - Select one or more values from a list\n- [ink-divider](https://github.com/JureSotosek/ink-divider) - A divider.\n- [ink-progress-bar](https://github.com/brigand/ink-progress-bar) - Progress bar.\n- [ink-table](https://github.com/maticzav/ink-table) - Table.\n- [ink-ascii](https://github.com/hexrcs/ink-ascii) - Awesome text component with more font choices, based on Figlet.\n- [ink-markdown](https://github.com/cameronhunter/ink-markdown) - Render syntax highlighted Markdown.\n- [ink-quicksearch-input](https://github.com/Eximchain/ink-quicksearch-input) - Select component with fast, quicksearch-like navigation.\n- [ink-confirm-input](https://github.com/kevva/ink-confirm-input) - Yes/No confirmation input.\n- [ink-syntax-highlight](https://github.com/vsashyn/ink-syntax-highlight) - Code syntax highlighting.\n- [ink-form](https://github.com/lukasbach/ink-form) - Form.\n- [ink-task-list](https://github.com/privatenumber/ink-task-list) - Task list.\n- [ink-spawn](https://github.com/kraenhansen/ink-spawn) - Spawn child processes.\n- [ink-titled-box](https://github.com/mishieck/ink-titled-box) - Box with a title.\n- [ink-chart](https://github.com/pppp606/ink-chart) - Sparkline and bar chart.\n- [ink-scroll-view](https://github.com/ByteLandTechnology/ink-scroll-view) - Scroll container.\n- [ink-scroll-list](https://github.com/ByteLandTechnology/ink-scroll-list) - Scrollable list.\n- [ink-stepper](https://github.com/archcorsair/ink-stepper) - Step-by-step wizard.\n- [ink-virtual-list](https://github.com/archcorsair/ink-virtual-list) - Virtualized list that renders only visible items for performance.\n- [ink-color-picker](https://github.com/sina-byn/ink-color-picker) - Color picker.\n\n## Useful Hooks\n\n- [ink-use-stdout-dimensions](https://github.com/cameronhunter/ink-monorepo/tree/master/packages/ink-use-stdout-dimensions) - Subscribe to stdout dimensions.\n\n## Recipes\n\n- [Routing with React Router](recipes/routing.md) - Navigate between routes using `MemoryRouter`.\n\n## Examples\n\nThe [`examples`](/examples) directory contains a set of real examples. You can run them with:\n\n```bash\nnpm run example examples/[example name]\n# e.g. npm run example examples/borders\n```\n\n- [Jest](examples/jest/jest.tsx) - Implementation of basic Jest UI.\n- [Counter](examples/counter/counter.tsx) - A simple counter that increments every 100ms.\n- [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).\n- [Borders](examples/borders/borders.tsx) - Add borders to the `<Box>` component.\n- [Suspense](examples/suspense/suspense.tsx) - Use React Suspense.\n- [Table](examples/table/table.tsx) - Renders a table with multiple columns and rows.\n- [Focus management](examples/use-focus/use-focus.tsx) - Use the `useFocus` hook to manage focus between components.\n- [User input](examples/use-input/use-input.tsx) - Listen for user input.\n- [Write to stdout](examples/use-stdout/use-stdout.tsx) - Write to stdout, bypassing main Ink output.\n- [Write to stderr](examples/use-stderr/use-stderr.tsx) - Write to stderr, bypassing main Ink output.\n- [Static](examples/static/static.tsx) - Use the `<Static>` component to render permanent output.\n- [Child process](examples/subprocess-output) - Renders output from a child process.\n- [Router](examples/router/router.tsx) - Navigate between routes using React Router's `MemoryRouter`.\n\n## Continuous Integration\n\nWhen running on CI (detected via the `CI` environment variable), Ink adapts its rendering:\n\n- 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.\n- Terminal resize events are not listened to.\n\nIf your CI environment supports full terminal rendering and you want to opt out of this behavior, set `CI=false`:\n\n```sh\nCI=false node my-cli.js\n```\n\n## Maintainers\n\n- [Vadim Demedes](https://github.com/vadimdemedes)\n- [Sindre Sorhus](https://github.com/sindresorhus)\n"
  },
  {
    "path": "recipes/routing.md",
    "content": "# Routing with React Router\n\n[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.\n\n```tsx\nimport React from 'react';\nimport {MemoryRouter, Routes, Route, useNavigate} from 'react-router';\nimport {render, useInput, Text} from 'ink';\n\nfunction Home() {\n\tconst navigate = useNavigate();\n\n\tuseInput((input, key) => {\n\t\tif (key.return) {\n\t\t\tnavigate('/about');\n\t\t}\n\t});\n\n\treturn <Text>Home. Press Enter to go to About.</Text>;\n}\n\nfunction About() {\n\tconst navigate = useNavigate();\n\n\tuseInput((input, key) => {\n\t\tif (key.return) {\n\t\t\tnavigate('/');\n\t\t}\n\t});\n\n\treturn <Text>About. Press Enter to go back Home.</Text>;\n}\n\nfunction App() {\n\treturn (\n\t\t<MemoryRouter>\n\t\t\t<Routes>\n\t\t\t\t<Route path=\"/\" element={<Home />} />\n\t\t\t\t<Route path=\"/about\" element={<About />} />\n\t\t\t</Routes>\n\t\t</MemoryRouter>\n\t);\n}\n\nrender(<App />);\n```\n\nThings to keep in mind:\n\n- `<Link>` can't be used in Ink since it renders an `<a>` tag. Use the `useNavigate` hook for all navigation instead.\n- `MemoryRouter` starts at `\"/\"` by default. Set the `initialEntries` prop to start at a different route.\n- Terminal routing is an abstraction for conditional rendering — routes aren't URLs, they're just screen states.\n\nSee [`examples/router`](/examples/router) for a working example.\n"
  },
  {
    "path": "src/ansi-tokenizer.ts",
    "content": "const bellCharacter = '\\u0007';\nconst escapeCharacter = '\\u001B';\nconst stringTerminatorCharacter = '\\u009C';\nconst csiCharacter = '\\u009B';\nconst oscCharacter = '\\u009D';\nconst dcsCharacter = '\\u0090';\nconst pmCharacter = '\\u009E';\nconst apcCharacter = '\\u009F';\nconst sosCharacter = '\\u0098';\n\ntype ControlStringType = 'osc' | 'dcs' | 'pm' | 'apc' | 'sos';\n\ntype CsiToken = {\n\treadonly type: 'csi';\n\treadonly value: string;\n\treadonly parameterString: string;\n\treadonly intermediateString: string;\n\treadonly finalCharacter: string;\n};\n\ntype EscToken = {\n\treadonly type: 'esc';\n\treadonly value: string;\n\treadonly intermediateString: string;\n\treadonly finalCharacter: string;\n};\n\ntype ControlStringToken = {\n\treadonly type: ControlStringType;\n\treadonly value: string;\n};\n\ntype TextToken = {\n\treadonly type: 'text';\n\treadonly value: string;\n};\n\ntype StToken = {\n\treadonly type: 'st';\n\treadonly value: string;\n};\n\ntype C1Token = {\n\treadonly type: 'c1';\n\treadonly value: string;\n};\n\ntype InvalidToken = {\n\treadonly type: 'invalid';\n\treadonly value: string;\n};\n\nexport type AnsiToken =\n\t| TextToken\n\t| CsiToken\n\t| EscToken\n\t| ControlStringToken\n\t| StToken\n\t| C1Token\n\t| InvalidToken;\n\nconst isCsiParameterCharacter = (character: string): boolean => {\n\tconst codePoint = character.codePointAt(0);\n\n\treturn codePoint !== undefined && codePoint >= 0x30 && codePoint <= 0x3f;\n};\n\nconst isCsiIntermediateCharacter = (character: string): boolean => {\n\tconst codePoint = character.codePointAt(0);\n\n\treturn codePoint !== undefined && codePoint >= 0x20 && codePoint <= 0x2f;\n};\n\nconst isCsiFinalCharacter = (character: string): boolean => {\n\tconst codePoint = character.codePointAt(0);\n\n\treturn codePoint !== undefined && codePoint >= 0x40 && codePoint <= 0x7e;\n};\n\nconst isEscapeIntermediateCharacter = (character: string): boolean => {\n\tconst codePoint = character.codePointAt(0);\n\n\treturn codePoint !== undefined && codePoint >= 0x20 && codePoint <= 0x2f;\n};\n\nconst isEscapeFinalCharacter = (character: string): boolean => {\n\tconst codePoint = character.codePointAt(0);\n\n\treturn codePoint !== undefined && codePoint >= 0x30 && codePoint <= 0x7e;\n};\n\nconst isC1ControlCharacter = (character: string): boolean => {\n\tconst codePoint = character.codePointAt(0);\n\n\treturn codePoint !== undefined && codePoint >= 0x80 && codePoint <= 0x9f;\n};\n\n// Standards references:\n// ECMA-48 control functions and CSI byte classes: https://ecma-international.org/publications-and-standards/standards/ecma-48/\n// xterm CSI parameter/intermediate/final format notes: https://invisible-island.net/xterm/ecma-48-parameter-format.html\n// xterm/OSC BEL termination behavior: https://davidrg.github.io/ckwin/dev/ctlseqs.html\nconst readCsiSequence = (\n\ttext: string,\n\tfromIndex: number,\n):\n\t| {\n\t\t\treadonly endIndex: number;\n\t\t\treadonly parameterString: string;\n\t\t\treadonly intermediateString: string;\n\t\t\treadonly finalCharacter: string;\n\t  }\n\t| undefined => {\n\tlet index = fromIndex;\n\n\twhile (index < text.length) {\n\t\tconst character = text[index]!;\n\n\t\tif (!isCsiParameterCharacter(character)) {\n\t\t\tbreak;\n\t\t}\n\n\t\tindex++;\n\t}\n\n\tconst parameterString = text.slice(fromIndex, index);\n\tconst intermediateStartIndex = index;\n\n\twhile (index < text.length) {\n\t\tconst character = text[index]!;\n\n\t\tif (!isCsiIntermediateCharacter(character)) {\n\t\t\tbreak;\n\t\t}\n\n\t\tindex++;\n\t}\n\n\tconst intermediateString = text.slice(intermediateStartIndex, index);\n\tconst finalCharacter = text[index];\n\n\tif (finalCharacter === undefined || !isCsiFinalCharacter(finalCharacter)) {\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\tendIndex: index + 1,\n\t\tparameterString,\n\t\tintermediateString,\n\t\tfinalCharacter,\n\t};\n};\n\nconst findControlStringTerminatorIndex = (\n\ttext: string,\n\tfromIndex: number,\n\tallowBellTerminator: boolean,\n): number | undefined => {\n\tfor (let index = fromIndex; index < text.length; index++) {\n\t\tconst character = text[index];\n\n\t\tif (allowBellTerminator && character === bellCharacter) {\n\t\t\treturn index + 1;\n\t\t}\n\n\t\tif (character === stringTerminatorCharacter) {\n\t\t\treturn index + 1;\n\t\t}\n\n\t\tif (character === escapeCharacter) {\n\t\t\tconst followingCharacter = text[index + 1];\n\n\t\t\t// Tmux escapes ESC bytes in payload as ESC ESC.\n\t\t\tif (followingCharacter === escapeCharacter) {\n\t\t\t\tindex++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (followingCharacter === '\\\\') {\n\t\t\t\treturn index + 2;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n};\n\nconst readEscapeSequence = (\n\ttext: string,\n\tfromIndex: number,\n):\n\t| {\n\t\t\treadonly endIndex: number;\n\t\t\treadonly intermediateString: string;\n\t\t\treadonly finalCharacter: string;\n\t  }\n\t| undefined => {\n\tlet index = fromIndex;\n\n\twhile (index < text.length) {\n\t\tconst character = text[index]!;\n\n\t\tif (!isEscapeIntermediateCharacter(character)) {\n\t\t\tbreak;\n\t\t}\n\n\t\tindex++;\n\t}\n\n\tconst intermediateString = text.slice(fromIndex, index);\n\tconst finalCharacter = text[index];\n\n\tif (finalCharacter === undefined || !isEscapeFinalCharacter(finalCharacter)) {\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\tendIndex: index + 1,\n\t\tintermediateString,\n\t\tfinalCharacter,\n\t};\n};\n\n// Centralize control-string rules so ESC and C1 paths do not diverge.\nconst getControlStringFromEscapeIntroducer = (\n\tcharacter: string,\n):\n\t| {\n\t\t\treadonly type: ControlStringType;\n\t\t\treadonly allowBellTerminator: boolean;\n\t  }\n\t| undefined => {\n\tswitch (character) {\n\t\tcase ']': {\n\t\t\treturn {type: 'osc', allowBellTerminator: true};\n\t\t}\n\n\t\tcase 'P': {\n\t\t\treturn {type: 'dcs', allowBellTerminator: false};\n\t\t}\n\n\t\tcase '^': {\n\t\t\treturn {type: 'pm', allowBellTerminator: false};\n\t\t}\n\n\t\tcase '_': {\n\t\t\treturn {type: 'apc', allowBellTerminator: false};\n\t\t}\n\n\t\tcase 'X': {\n\t\t\treturn {type: 'sos', allowBellTerminator: false};\n\t\t}\n\n\t\tdefault: {\n\t\t\treturn undefined;\n\t\t}\n\t}\n};\n\nconst getControlStringFromC1Introducer = (\n\tcharacter: string,\n):\n\t| {\n\t\t\treadonly type: ControlStringType;\n\t\t\treadonly allowBellTerminator: boolean;\n\t  }\n\t| undefined => {\n\tswitch (character) {\n\t\tcase oscCharacter: {\n\t\t\treturn {type: 'osc', allowBellTerminator: true};\n\t\t}\n\n\t\tcase dcsCharacter: {\n\t\t\treturn {type: 'dcs', allowBellTerminator: false};\n\t\t}\n\n\t\tcase pmCharacter: {\n\t\t\treturn {type: 'pm', allowBellTerminator: false};\n\t\t}\n\n\t\tcase apcCharacter: {\n\t\t\treturn {type: 'apc', allowBellTerminator: false};\n\t\t}\n\n\t\tcase sosCharacter: {\n\t\t\treturn {type: 'sos', allowBellTerminator: false};\n\t\t}\n\n\t\tdefault: {\n\t\t\treturn undefined;\n\t\t}\n\t}\n};\n\nexport const hasAnsiControlCharacters = (text: string): boolean => {\n\tif (text.includes(escapeCharacter)) {\n\t\treturn true;\n\t}\n\n\tfor (const character of text) {\n\t\tif (isC1ControlCharacter(character)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n};\n\nconst malformedFromIndex = (\n\ttokens: AnsiToken[],\n\ttext: string,\n\ttextStartIndex: number,\n\tfromIndex: number,\n): AnsiToken[] => {\n\tif (fromIndex > textStartIndex) {\n\t\ttokens.push({type: 'text', value: text.slice(textStartIndex, fromIndex)});\n\t}\n\n\t// Treat the remainder as invalid so callers can drop it as one unsafe unit.\n\ttokens.push({type: 'invalid', value: text.slice(fromIndex)});\n\n\treturn tokens;\n};\n\nexport const tokenizeAnsi = (text: string): AnsiToken[] => {\n\tif (!hasAnsiControlCharacters(text)) {\n\t\treturn [{type: 'text', value: text}];\n\t}\n\n\tconst tokens: AnsiToken[] = [];\n\tlet textStartIndex = 0;\n\n\tfor (let index = 0; index < text.length; ) {\n\t\tconst character = text[index];\n\n\t\tif (character === undefined) {\n\t\t\tbreak;\n\t\t}\n\n\t\tif (character === escapeCharacter) {\n\t\t\tconst followingCharacter = text[index + 1];\n\n\t\t\tif (followingCharacter === undefined) {\n\t\t\t\treturn malformedFromIndex(tokens, text, textStartIndex, index);\n\t\t\t}\n\n\t\t\tif (followingCharacter === '[') {\n\t\t\t\tconst csiSequence = readCsiSequence(text, index + 2);\n\n\t\t\t\tif (csiSequence === undefined) {\n\t\t\t\t\treturn malformedFromIndex(tokens, text, textStartIndex, index);\n\t\t\t\t}\n\n\t\t\t\tif (index > textStartIndex) {\n\t\t\t\t\ttokens.push({type: 'text', value: text.slice(textStartIndex, index)});\n\t\t\t\t}\n\n\t\t\t\ttokens.push({\n\t\t\t\t\ttype: 'csi',\n\t\t\t\t\tvalue: text.slice(index, csiSequence.endIndex),\n\t\t\t\t\tparameterString: csiSequence.parameterString,\n\t\t\t\t\tintermediateString: csiSequence.intermediateString,\n\t\t\t\t\tfinalCharacter: csiSequence.finalCharacter,\n\t\t\t\t});\n\t\t\t\tindex = csiSequence.endIndex;\n\t\t\t\ttextStartIndex = index;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst escapeControlString =\n\t\t\t\tgetControlStringFromEscapeIntroducer(followingCharacter);\n\n\t\t\tif (escapeControlString !== undefined) {\n\t\t\t\tconst controlStringTerminatorIndex = findControlStringTerminatorIndex(\n\t\t\t\t\ttext,\n\t\t\t\t\tindex + 2,\n\t\t\t\t\tescapeControlString.allowBellTerminator,\n\t\t\t\t);\n\n\t\t\t\tif (controlStringTerminatorIndex === undefined) {\n\t\t\t\t\treturn malformedFromIndex(tokens, text, textStartIndex, index);\n\t\t\t\t}\n\n\t\t\t\tif (index > textStartIndex) {\n\t\t\t\t\ttokens.push({type: 'text', value: text.slice(textStartIndex, index)});\n\t\t\t\t}\n\n\t\t\t\ttokens.push({\n\t\t\t\t\ttype: escapeControlString.type,\n\t\t\t\t\tvalue: text.slice(index, controlStringTerminatorIndex),\n\t\t\t\t});\n\t\t\t\tindex = controlStringTerminatorIndex;\n\t\t\t\ttextStartIndex = index;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst escapeSequence = readEscapeSequence(text, index + 1);\n\n\t\t\tif (escapeSequence === undefined) {\n\t\t\t\t// Incomplete escape sequences with intermediates are malformed control strings.\n\t\t\t\tif (isEscapeIntermediateCharacter(followingCharacter)) {\n\t\t\t\t\treturn malformedFromIndex(tokens, text, textStartIndex, index);\n\t\t\t\t}\n\n\t\t\t\tif (index > textStartIndex) {\n\t\t\t\t\ttokens.push({type: 'text', value: text.slice(textStartIndex, index)});\n\t\t\t\t}\n\n\t\t\t\t// Ignore lone ESC and continue tokenizing the rest.\n\t\t\t\tindex++;\n\t\t\t\ttextStartIndex = index;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (index > textStartIndex) {\n\t\t\t\ttokens.push({type: 'text', value: text.slice(textStartIndex, index)});\n\t\t\t}\n\n\t\t\ttokens.push({\n\t\t\t\ttype: 'esc',\n\t\t\t\tvalue: text.slice(index, escapeSequence.endIndex),\n\t\t\t\tintermediateString: escapeSequence.intermediateString,\n\t\t\t\tfinalCharacter: escapeSequence.finalCharacter,\n\t\t\t});\n\t\t\tindex = escapeSequence.endIndex;\n\t\t\ttextStartIndex = index;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (character === csiCharacter) {\n\t\t\tconst csiSequence = readCsiSequence(text, index + 1);\n\n\t\t\tif (csiSequence === undefined) {\n\t\t\t\treturn malformedFromIndex(tokens, text, textStartIndex, index);\n\t\t\t}\n\n\t\t\tif (index > textStartIndex) {\n\t\t\t\ttokens.push({type: 'text', value: text.slice(textStartIndex, index)});\n\t\t\t}\n\n\t\t\ttokens.push({\n\t\t\t\ttype: 'csi',\n\t\t\t\tvalue: text.slice(index, csiSequence.endIndex),\n\t\t\t\tparameterString: csiSequence.parameterString,\n\t\t\t\tintermediateString: csiSequence.intermediateString,\n\t\t\t\tfinalCharacter: csiSequence.finalCharacter,\n\t\t\t});\n\t\t\tindex = csiSequence.endIndex;\n\t\t\ttextStartIndex = index;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst c1ControlString = getControlStringFromC1Introducer(character);\n\n\t\tif (c1ControlString !== undefined) {\n\t\t\tconst controlStringTerminatorIndex = findControlStringTerminatorIndex(\n\t\t\t\ttext,\n\t\t\t\tindex + 1,\n\t\t\t\tc1ControlString.allowBellTerminator,\n\t\t\t);\n\n\t\t\tif (controlStringTerminatorIndex === undefined) {\n\t\t\t\treturn malformedFromIndex(tokens, text, textStartIndex, index);\n\t\t\t}\n\n\t\t\tif (index > textStartIndex) {\n\t\t\t\ttokens.push({type: 'text', value: text.slice(textStartIndex, index)});\n\t\t\t}\n\n\t\t\ttokens.push({\n\t\t\t\ttype: c1ControlString.type,\n\t\t\t\tvalue: text.slice(index, controlStringTerminatorIndex),\n\t\t\t});\n\t\t\tindex = controlStringTerminatorIndex;\n\t\t\ttextStartIndex = index;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (character === stringTerminatorCharacter) {\n\t\t\tif (index > textStartIndex) {\n\t\t\t\ttokens.push({type: 'text', value: text.slice(textStartIndex, index)});\n\t\t\t}\n\n\t\t\ttokens.push({type: 'st', value: character});\n\t\t\tindex++;\n\t\t\ttextStartIndex = index;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Strip remaining C1 controls as standalone functions.\n\t\tif (isC1ControlCharacter(character)) {\n\t\t\tif (index > textStartIndex) {\n\t\t\t\ttokens.push({type: 'text', value: text.slice(textStartIndex, index)});\n\t\t\t}\n\n\t\t\ttokens.push({type: 'c1', value: character});\n\t\t\tindex++;\n\t\t\ttextStartIndex = index;\n\t\t\tcontinue;\n\t\t}\n\n\t\tindex++;\n\t}\n\n\tif (textStartIndex < text.length) {\n\t\ttokens.push({type: 'text', value: text.slice(textStartIndex)});\n\t}\n\n\treturn tokens;\n};\n"
  },
  {
    "path": "src/colorize.ts",
    "content": "import chalk, {type ForegroundColorName, type BackgroundColorName} from 'chalk';\n\ntype ColorType = 'foreground' | 'background';\n\nconst rgbRegex = /^rgb\\(\\s?(\\d+),\\s?(\\d+),\\s?(\\d+)\\s?\\)$/;\nconst ansiRegex = /^ansi256\\(\\s?(\\d+)\\s?\\)$/;\n\nconst isNamedColor = (color: string): color is ForegroundColorName => {\n\treturn color in chalk;\n};\n\nconst colorize = (\n\tstr: string,\n\tcolor: string | undefined,\n\ttype: ColorType,\n): string => {\n\tif (!color) {\n\t\treturn str;\n\t}\n\n\tif (isNamedColor(color)) {\n\t\tif (type === 'foreground') {\n\t\t\treturn chalk[color](str);\n\t\t}\n\n\t\tconst methodName = `bg${\n\t\t\tcolor[0]!.toUpperCase() + color.slice(1)\n\t\t}` as BackgroundColorName;\n\n\t\treturn chalk[methodName](str);\n\t}\n\n\tif (color.startsWith('#')) {\n\t\treturn type === 'foreground'\n\t\t\t? chalk.hex(color)(str)\n\t\t\t: chalk.bgHex(color)(str);\n\t}\n\n\tif (color.startsWith('ansi256')) {\n\t\tconst matches = ansiRegex.exec(color);\n\n\t\tif (!matches) {\n\t\t\treturn str;\n\t\t}\n\n\t\tconst value = Number(matches[1]);\n\n\t\treturn type === 'foreground'\n\t\t\t? chalk.ansi256(value)(str)\n\t\t\t: chalk.bgAnsi256(value)(str);\n\t}\n\n\tif (color.startsWith('rgb')) {\n\t\tconst matches = rgbRegex.exec(color);\n\n\t\tif (!matches) {\n\t\t\treturn str;\n\t\t}\n\n\t\tconst firstValue = Number(matches[1]);\n\t\tconst secondValue = Number(matches[2]);\n\t\tconst thirdValue = Number(matches[3]);\n\n\t\treturn type === 'foreground'\n\t\t\t? chalk.rgb(firstValue, secondValue, thirdValue)(str)\n\t\t\t: chalk.bgRgb(firstValue, secondValue, thirdValue)(str);\n\t}\n\n\treturn str;\n};\n\nexport default colorize;\n"
  },
  {
    "path": "src/components/AccessibilityContext.ts",
    "content": "import {createContext} from 'react';\n\nexport const accessibilityContext = createContext({\n\tisScreenReaderEnabled: false,\n});\n"
  },
  {
    "path": "src/components/App.tsx",
    "content": "import {EventEmitter} from 'node:events';\nimport process from 'node:process';\nimport React, {\n\ttype ReactNode,\n\tuseState,\n\tuseRef,\n\tuseCallback,\n\tuseMemo,\n\tuseEffect,\n} from 'react';\nimport cliCursor from 'cli-cursor';\nimport {type CursorPosition} from '../log-update.js';\nimport {createInputParser} from '../input-parser.js';\nimport AppContext from './AppContext.js';\nimport StdinContext from './StdinContext.js';\nimport StdoutContext from './StdoutContext.js';\nimport StderrContext from './StderrContext.js';\nimport FocusContext from './FocusContext.js';\nimport CursorContext from './CursorContext.js';\nimport ErrorBoundary from './ErrorBoundary.js';\n\nconst tab = '\\t';\nconst shiftTab = '\\u001B[Z';\nconst escape = '\\u001B';\n\ntype Props = {\n\treadonly children: ReactNode;\n\treadonly stdin: NodeJS.ReadStream;\n\treadonly stdout: NodeJS.WriteStream;\n\treadonly stderr: NodeJS.WriteStream;\n\treadonly writeToStdout: (data: string) => void;\n\treadonly writeToStderr: (data: string) => void;\n\treadonly exitOnCtrlC: boolean;\n\treadonly onExit: (errorOrResult?: unknown) => void;\n\treadonly onWaitUntilRenderFlush: () => Promise<void>;\n\treadonly setCursorPosition: (position: CursorPosition | undefined) => void;\n\treadonly interactive: boolean;\n};\n\ntype Focusable = {\n\treadonly id: string;\n\treadonly isActive: boolean;\n};\n\n// Root component for all Ink apps\n// It renders stdin and stdout contexts, so that children can access them if needed\n// It also handles Ctrl+C exiting and cursor visibility\nfunction App({\n\tchildren,\n\tstdin,\n\tstdout,\n\tstderr,\n\twriteToStdout,\n\twriteToStderr,\n\texitOnCtrlC,\n\tonExit,\n\tonWaitUntilRenderFlush,\n\tsetCursorPosition,\n\tinteractive,\n}: Props): React.ReactNode {\n\tconst [isFocusEnabled, setIsFocusEnabled] = useState(true);\n\tconst [activeFocusId, setActiveFocusId] = useState<string | undefined>(\n\t\tundefined,\n\t);\n\t// Focusables array is managed internally via setFocusables callback pattern\n\t// eslint-disable-next-line react/hook-use-state\n\tconst [, setFocusables] = useState<Focusable[]>([]);\n\t// Track focusables count for tab navigation check (avoids stale closure)\n\tconst focusablesCountRef = useRef(0);\n\n\t// Count how many components enabled raw mode to avoid disabling\n\t// raw mode until all components don't need it anymore\n\tconst rawModeEnabledCount = useRef(0);\n\t// Count how many components enabled bracketed paste mode\n\tconst bracketedPasteModeEnabledCount = useRef(0);\n\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\tconst internal_eventEmitter = useRef(new EventEmitter());\n\t// Each useInput hook adds a listener, so the count can legitimately exceed the default limit of 10.\n\tinternal_eventEmitter.current.setMaxListeners(Infinity);\n\t// Store the currently attached readable listener to avoid stale closure issues\n\tconst readableListenerRef = useRef<(() => void) | undefined>(undefined);\n\tconst inputParserRef = useRef(createInputParser());\n\tconst pendingInputFlushRef = useRef<NodeJS.Timeout | undefined>(undefined);\n\t// Small delay to let chunked escape sequences complete before flushing as literal input.\n\tconst pendingInputFlushDelayMilliseconds = 20;\n\n\tconst clearPendingInputFlush = useCallback((): void => {\n\t\tif (!pendingInputFlushRef.current) {\n\t\t\treturn;\n\t\t}\n\n\t\tclearTimeout(pendingInputFlushRef.current);\n\t\tpendingInputFlushRef.current = undefined;\n\t}, []);\n\n\t// Determines if TTY is supported on the provided stdin\n\tconst isRawModeSupported = stdin.isTTY;\n\n\tconst detachReadableListener = useCallback((): void => {\n\t\tif (!readableListenerRef.current) {\n\t\t\treturn;\n\t\t}\n\n\t\tstdin.removeListener('readable', readableListenerRef.current);\n\t\treadableListenerRef.current = undefined;\n\t}, [stdin]);\n\n\tconst disableRawMode = useCallback((): void => {\n\t\tstdin.setRawMode(false);\n\t\tdetachReadableListener();\n\t\tstdin.unref();\n\t\trawModeEnabledCount.current = 0;\n\t\tinputParserRef.current.reset();\n\t\tclearPendingInputFlush();\n\t}, [stdin, detachReadableListener, clearPendingInputFlush]);\n\n\tconst handleExit = useCallback(\n\t\t(errorOrResult?: unknown): void => {\n\t\t\tif (isRawModeSupported && rawModeEnabledCount.current > 0) {\n\t\t\t\tdisableRawMode();\n\t\t\t}\n\n\t\t\tonExit(errorOrResult);\n\t\t},\n\t\t[isRawModeSupported, disableRawMode, onExit],\n\t);\n\n\tconst handleInput = useCallback(\n\t\t(input: string): void => {\n\t\t\t// Exit on Ctrl+C\n\t\t\t// eslint-disable-next-line unicorn/no-hex-escape\n\t\t\tif (input === '\\x03' && exitOnCtrlC) {\n\t\t\t\thandleExit();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Reset focus when there's an active focused component on Esc\n\t\t\tif (input === escape) {\n\t\t\t\tsetActiveFocusId(currentActiveFocusId => {\n\t\t\t\t\tif (currentActiveFocusId) {\n\t\t\t\t\t\treturn undefined;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn currentActiveFocusId;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\t[exitOnCtrlC, handleExit],\n\t);\n\n\tconst emitInput = useCallback(\n\t\t(input: string): void => {\n\t\t\thandleInput(input);\n\t\t\tinternal_eventEmitter.current.emit('input', input);\n\t\t},\n\t\t[handleInput],\n\t);\n\n\tconst schedulePendingInputFlush = useCallback((): void => {\n\t\tclearPendingInputFlush();\n\t\tpendingInputFlushRef.current = setTimeout(() => {\n\t\t\tpendingInputFlushRef.current = undefined;\n\t\t\tconst pendingEscape = inputParserRef.current.flushPendingEscape();\n\t\t\tif (!pendingEscape) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\temitInput(pendingEscape);\n\t\t}, pendingInputFlushDelayMilliseconds);\n\t}, [clearPendingInputFlush, emitInput]);\n\n\tconst handleReadable = useCallback((): void => {\n\t\tclearPendingInputFlush();\n\t\tlet chunk;\n\t\t// eslint-disable-next-line @typescript-eslint/no-restricted-types\n\t\twhile ((chunk = stdin.read() as string | null) !== null) {\n\t\t\tconst inputEvents = inputParserRef.current.push(chunk);\n\t\t\tfor (const event of inputEvents) {\n\t\t\t\tif (typeof event === 'string') {\n\t\t\t\t\temitInput(event);\n\t\t\t\t} else {\n\t\t\t\t\t// Keep paste on a separate channel from `useInput` so key handlers\n\t\t\t\t\t// don't need to branch on mixed key-vs-paste event shapes.\n\t\t\t\t\tif (internal_eventEmitter.current.listenerCount('paste') === 0) {\n\t\t\t\t\t\temitInput(event.paste);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tinternal_eventEmitter.current.emit('paste', event.paste);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (inputParserRef.current.hasPendingEscape()) {\n\t\t\tschedulePendingInputFlush();\n\t\t}\n\t}, [stdin, emitInput, clearPendingInputFlush, schedulePendingInputFlush]);\n\n\tconst handleSetRawMode = useCallback(\n\t\t(isEnabled: boolean): void => {\n\t\t\tif (!isRawModeSupported) {\n\t\t\t\tif (stdin === process.stdin) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t'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',\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t'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',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tstdin.setEncoding('utf8');\n\n\t\t\tif (isEnabled) {\n\t\t\t\t// Ensure raw mode is enabled only once\n\t\t\t\tif (rawModeEnabledCount.current === 0) {\n\t\t\t\t\tstdin.ref();\n\t\t\t\t\tstdin.setRawMode(true);\n\t\t\t\t\t// Store the listener reference to avoid stale closure when removing\n\t\t\t\t\treadableListenerRef.current = handleReadable;\n\t\t\t\t\tstdin.addListener('readable', handleReadable);\n\t\t\t\t}\n\n\t\t\t\trawModeEnabledCount.current++;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Disable raw mode only when no components left that are using it\n\t\t\tif (rawModeEnabledCount.current === 0) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (--rawModeEnabledCount.current === 0) {\n\t\t\t\tdisableRawMode();\n\t\t\t}\n\t\t},\n\t\t[isRawModeSupported, stdin, handleReadable, disableRawMode],\n\t);\n\n\tconst handleSetBracketedPasteMode = useCallback(\n\t\t(isEnabled: boolean): void => {\n\t\t\tif (!stdout.isTTY) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (isEnabled) {\n\t\t\t\tif (bracketedPasteModeEnabledCount.current === 0) {\n\t\t\t\t\tstdout.write('\\u001B[?2004h');\n\t\t\t\t}\n\n\t\t\t\tbracketedPasteModeEnabledCount.current++;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (bracketedPasteModeEnabledCount.current === 0) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (--bracketedPasteModeEnabledCount.current === 0) {\n\t\t\t\tstdout.write('\\u001B[?2004l');\n\t\t\t}\n\t\t},\n\t\t[stdout],\n\t);\n\n\t// Focus navigation helpers\n\tconst findNextFocusable = useCallback(\n\t\t(\n\t\t\tcurrentFocusables: Focusable[],\n\t\t\tcurrentActiveFocusId: string | undefined,\n\t\t): string | undefined => {\n\t\t\tconst activeIndex = currentFocusables.findIndex(focusable => {\n\t\t\t\treturn focusable.id === currentActiveFocusId;\n\t\t\t});\n\n\t\t\tfor (\n\t\t\t\tlet index = activeIndex + 1;\n\t\t\t\tindex < currentFocusables.length;\n\t\t\t\tindex++\n\t\t\t) {\n\t\t\t\tconst focusable = currentFocusables[index];\n\n\t\t\t\tif (focusable?.isActive) {\n\t\t\t\t\treturn focusable.id;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn undefined;\n\t\t},\n\t\t[],\n\t);\n\n\tconst findPreviousFocusable = useCallback(\n\t\t(\n\t\t\tcurrentFocusables: Focusable[],\n\t\t\tcurrentActiveFocusId: string | undefined,\n\t\t): string | undefined => {\n\t\t\tconst activeIndex = currentFocusables.findIndex(focusable => {\n\t\t\t\treturn focusable.id === currentActiveFocusId;\n\t\t\t});\n\n\t\t\tfor (let index = activeIndex - 1; index >= 0; index--) {\n\t\t\t\tconst focusable = currentFocusables[index];\n\n\t\t\t\tif (focusable?.isActive) {\n\t\t\t\t\treturn focusable.id;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn undefined;\n\t\t},\n\t\t[],\n\t);\n\n\tconst focusNext = useCallback((): void => {\n\t\tsetFocusables(currentFocusables => {\n\t\t\tsetActiveFocusId(currentActiveFocusId => {\n\t\t\t\tconst firstFocusableId = currentFocusables.find(\n\t\t\t\t\tfocusable => focusable.isActive,\n\t\t\t\t)?.id;\n\t\t\t\tconst nextFocusableId = findNextFocusable(\n\t\t\t\t\tcurrentFocusables,\n\t\t\t\t\tcurrentActiveFocusId,\n\t\t\t\t);\n\n\t\t\t\treturn nextFocusableId ?? firstFocusableId;\n\t\t\t});\n\t\t\treturn currentFocusables;\n\t\t});\n\t}, [findNextFocusable]);\n\n\tconst focusPrevious = useCallback((): void => {\n\t\tsetFocusables(currentFocusables => {\n\t\t\tsetActiveFocusId(currentActiveFocusId => {\n\t\t\t\tconst lastFocusableId = currentFocusables.findLast(\n\t\t\t\t\tfocusable => focusable.isActive,\n\t\t\t\t)?.id;\n\t\t\t\tconst previousFocusableId = findPreviousFocusable(\n\t\t\t\t\tcurrentFocusables,\n\t\t\t\t\tcurrentActiveFocusId,\n\t\t\t\t);\n\n\t\t\t\treturn previousFocusableId ?? lastFocusableId;\n\t\t\t});\n\t\t\treturn currentFocusables;\n\t\t});\n\t}, [findPreviousFocusable]);\n\n\t// Handle tab navigation via effect that subscribes to input events\n\tuseEffect(() => {\n\t\tconst handleTabNavigation = (input: string): void => {\n\t\t\tif (!isFocusEnabled || focusablesCountRef.current === 0) return;\n\n\t\t\tif (input === tab) {\n\t\t\t\tfocusNext();\n\t\t\t}\n\n\t\t\tif (input === shiftTab) {\n\t\t\t\tfocusPrevious();\n\t\t\t}\n\t\t};\n\n\t\tinternal_eventEmitter.current.on('input', handleTabNavigation);\n\t\tconst emitter = internal_eventEmitter.current;\n\n\t\treturn () => {\n\t\t\temitter.off('input', handleTabNavigation);\n\t\t};\n\t}, [isFocusEnabled, focusNext, focusPrevious]);\n\n\tconst enableFocus = useCallback((): void => {\n\t\tsetIsFocusEnabled(true);\n\t}, []);\n\n\tconst disableFocus = useCallback((): void => {\n\t\tsetIsFocusEnabled(false);\n\t}, []);\n\n\tconst focus = useCallback((id: string): void => {\n\t\tsetFocusables(currentFocusables => {\n\t\t\tconst hasFocusableId = currentFocusables.some(\n\t\t\t\tfocusable => focusable?.id === id,\n\t\t\t);\n\n\t\t\tif (hasFocusableId) {\n\t\t\t\tsetActiveFocusId(id);\n\t\t\t}\n\n\t\t\treturn currentFocusables;\n\t\t});\n\t}, []);\n\n\tconst addFocusable = useCallback(\n\t\t(id: string, {autoFocus}: {autoFocus: boolean}): void => {\n\t\t\tsetFocusables(currentFocusables => {\n\t\t\t\tfocusablesCountRef.current = currentFocusables.length + 1;\n\n\t\t\t\treturn [\n\t\t\t\t\t...currentFocusables,\n\t\t\t\t\t{\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tisActive: true,\n\t\t\t\t\t},\n\t\t\t\t];\n\t\t\t});\n\n\t\t\tif (autoFocus) {\n\t\t\t\tsetActiveFocusId(currentActiveFocusId => {\n\t\t\t\t\tif (!currentActiveFocusId) {\n\t\t\t\t\t\treturn id;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn currentActiveFocusId;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\t[],\n\t);\n\n\tconst removeFocusable = useCallback((id: string): void => {\n\t\tsetActiveFocusId(currentActiveFocusId => {\n\t\t\tif (currentActiveFocusId === id) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\treturn currentActiveFocusId;\n\t\t});\n\n\t\tsetFocusables(currentFocusables => {\n\t\t\tconst filtered = currentFocusables.filter(focusable => {\n\t\t\t\treturn focusable.id !== id;\n\t\t\t});\n\t\t\tfocusablesCountRef.current = filtered.length;\n\n\t\t\treturn filtered;\n\t\t});\n\t}, []);\n\n\tconst activateFocusable = useCallback((id: string): void => {\n\t\tsetFocusables(currentFocusables =>\n\t\t\tcurrentFocusables.map(focusable => {\n\t\t\t\tif (focusable.id !== id) {\n\t\t\t\t\treturn focusable;\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tisActive: true,\n\t\t\t\t};\n\t\t\t}),\n\t\t);\n\t}, []);\n\n\tconst deactivateFocusable = useCallback((id: string): void => {\n\t\tsetActiveFocusId(currentActiveFocusId => {\n\t\t\tif (currentActiveFocusId === id) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\treturn currentActiveFocusId;\n\t\t});\n\n\t\tsetFocusables(currentFocusables =>\n\t\t\tcurrentFocusables.map(focusable => {\n\t\t\t\tif (focusable.id !== id) {\n\t\t\t\t\treturn focusable;\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tisActive: false,\n\t\t\t\t};\n\t\t\t}),\n\t\t);\n\t}, []);\n\n\t// Handle cursor visibility, raw mode, and bracketed paste mode cleanup on unmount\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tconst canWriteToStdout = !stdout.destroyed && !stdout.writableEnded;\n\n\t\t\tif (interactive && canWriteToStdout) {\n\t\t\t\tcliCursor.show(stdout);\n\t\t\t}\n\n\t\t\tif (isRawModeSupported && rawModeEnabledCount.current > 0) {\n\t\t\t\tdisableRawMode();\n\t\t\t}\n\n\t\t\tif (bracketedPasteModeEnabledCount.current > 0) {\n\t\t\t\tif (stdout.isTTY && canWriteToStdout) {\n\t\t\t\t\tstdout.write('\\u001B[?2004l');\n\t\t\t\t}\n\n\t\t\t\tbracketedPasteModeEnabledCount.current = 0;\n\t\t\t}\n\t\t};\n\t}, [stdout, isRawModeSupported, disableRawMode, interactive]);\n\n\t// Memoize context values to prevent unnecessary re-renders\n\tconst appContextValue = useMemo(\n\t\t() => ({\n\t\t\texit: handleExit,\n\t\t\twaitUntilRenderFlush: onWaitUntilRenderFlush,\n\t\t}),\n\t\t[handleExit, onWaitUntilRenderFlush],\n\t);\n\n\tconst stdinContextValue = useMemo(\n\t\t() => ({\n\t\t\tstdin,\n\t\t\tsetRawMode: handleSetRawMode,\n\t\t\tsetBracketedPasteMode: handleSetBracketedPasteMode,\n\t\t\tisRawModeSupported,\n\t\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\t\tinternal_exitOnCtrlC: exitOnCtrlC,\n\t\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\t\tinternal_eventEmitter: internal_eventEmitter.current,\n\t\t}),\n\t\t[\n\t\t\tstdin,\n\t\t\thandleSetRawMode,\n\t\t\thandleSetBracketedPasteMode,\n\t\t\tisRawModeSupported,\n\t\t\texitOnCtrlC,\n\t\t],\n\t);\n\n\tconst stdoutContextValue = useMemo(\n\t\t() => ({\n\t\t\tstdout,\n\t\t\twrite: writeToStdout,\n\t\t}),\n\t\t[stdout, writeToStdout],\n\t);\n\n\tconst stderrContextValue = useMemo(\n\t\t() => ({\n\t\t\tstderr,\n\t\t\twrite: writeToStderr,\n\t\t}),\n\t\t[stderr, writeToStderr],\n\t);\n\n\tconst cursorContextValue = useMemo(\n\t\t() => ({\n\t\t\tsetCursorPosition,\n\t\t}),\n\t\t[setCursorPosition],\n\t);\n\n\tconst focusContextValue = useMemo(\n\t\t() => ({\n\t\t\tactiveId: activeFocusId,\n\t\t\tadd: addFocusable,\n\t\t\tremove: removeFocusable,\n\t\t\tactivate: activateFocusable,\n\t\t\tdeactivate: deactivateFocusable,\n\t\t\tenableFocus,\n\t\t\tdisableFocus,\n\t\t\tfocusNext,\n\t\t\tfocusPrevious,\n\t\t\tfocus,\n\t\t}),\n\t\t[\n\t\t\tactiveFocusId,\n\t\t\taddFocusable,\n\t\t\tremoveFocusable,\n\t\t\tactivateFocusable,\n\t\t\tdeactivateFocusable,\n\t\t\tenableFocus,\n\t\t\tdisableFocus,\n\t\t\tfocusNext,\n\t\t\tfocusPrevious,\n\t\t\tfocus,\n\t\t],\n\t);\n\n\treturn (\n\t\t<AppContext.Provider value={appContextValue}>\n\t\t\t<StdinContext.Provider value={stdinContextValue}>\n\t\t\t\t<StdoutContext.Provider value={stdoutContextValue}>\n\t\t\t\t\t<StderrContext.Provider value={stderrContextValue}>\n\t\t\t\t\t\t<FocusContext.Provider value={focusContextValue}>\n\t\t\t\t\t\t\t<CursorContext.Provider value={cursorContextValue}>\n\t\t\t\t\t\t\t\t<ErrorBoundary onError={handleExit}>{children}</ErrorBoundary>\n\t\t\t\t\t\t\t</CursorContext.Provider>\n\t\t\t\t\t\t</FocusContext.Provider>\n\t\t\t\t\t</StderrContext.Provider>\n\t\t\t\t</StdoutContext.Provider>\n\t\t\t</StdinContext.Provider>\n\t\t</AppContext.Provider>\n\t);\n}\n\nApp.displayName = 'InternalApp';\n\nexport default App;\n"
  },
  {
    "path": "src/components/AppContext.ts",
    "content": "import {createContext} from 'react';\n\nexport type Props = {\n\t/**\n\tExit (unmount) the whole Ink app.\n\n\t- `exit()` — resolves `waitUntilExit()` with `undefined`.\n\t- `exit(new Error('…'))` — rejects `waitUntilExit()` with the error.\n\t- `exit(value)` — resolves `waitUntilExit()` with `value`.\n\t*/\n\treadonly exit: (errorOrResult?: Error | unknown) => void;\n\n\t/**\n\tReturns a promise that settles after pending render output is flushed to stdout.\n\n\t@example\n\t```jsx\n\timport {useEffect} from 'react';\n\timport {useApp} from 'ink';\n\n\tconst Example = () => {\n\t\tconst {waitUntilRenderFlush} = useApp();\n\n\t\tuseEffect(() => {\n\t\t\tvoid (async () => {\n\t\t\t\tawait waitUntilRenderFlush();\n\t\t\t\trunNextCommand();\n\t\t\t})();\n\t\t}, [waitUntilRenderFlush]);\n\n\t\treturn …;\n\t};\n\t```\n\t*/\n\treadonly waitUntilRenderFlush: () => Promise<void>;\n};\n\n/**\n`AppContext` is a React context that exposes lifecycle methods for the app.\n*/\n// eslint-disable-next-line @typescript-eslint/naming-convention\nconst AppContext = createContext<Props>({\n\texit() {},\n\tasync waitUntilRenderFlush() {},\n});\n\nAppContext.displayName = 'InternalAppContext';\n\nexport default AppContext;\n"
  },
  {
    "path": "src/components/BackgroundContext.ts",
    "content": "import {createContext} from 'react';\nimport {type LiteralUnion} from 'type-fest';\nimport {type ForegroundColorName} from 'ansi-styles';\n\nexport type BackgroundColor = LiteralUnion<ForegroundColorName, string>;\n\nexport const backgroundContext = createContext<BackgroundColor | undefined>(\n\tundefined,\n);\n"
  },
  {
    "path": "src/components/Box.tsx",
    "content": "import React, {forwardRef, useContext, type PropsWithChildren} from 'react';\nimport {type Except} from 'type-fest';\nimport {type Styles} from '../styles.js';\nimport {type DOMElement} from '../dom.js';\nimport {accessibilityContext} from './AccessibilityContext.js';\nimport {backgroundContext} from './BackgroundContext.js';\n\nexport type Props = Except<Styles, 'textWrap'> & {\n\t/**\n\tA label for the element for screen readers.\n\t*/\n\treadonly 'aria-label'?: string;\n\n\t/**\n\tHide the element from screen readers.\n\t*/\n\treadonly 'aria-hidden'?: boolean;\n\n\t/**\n\tThe role of the element.\n\t*/\n\treadonly 'aria-role'?:\n\t\t| 'button'\n\t\t| 'checkbox'\n\t\t| 'combobox'\n\t\t| 'list'\n\t\t| 'listbox'\n\t\t| 'listitem'\n\t\t| 'menu'\n\t\t| 'menuitem'\n\t\t| 'option'\n\t\t| 'progressbar'\n\t\t| 'radio'\n\t\t| 'radiogroup'\n\t\t| 'tab'\n\t\t| 'tablist'\n\t\t| 'table'\n\t\t| 'textbox'\n\t\t| 'timer'\n\t\t| 'toolbar';\n\n\t/**\n\tThe state of the element.\n\t*/\n\treadonly 'aria-state'?: {\n\t\treadonly busy?: boolean;\n\t\treadonly checked?: boolean;\n\t\treadonly disabled?: boolean;\n\t\treadonly expanded?: boolean;\n\t\treadonly multiline?: boolean;\n\t\treadonly multiselectable?: boolean;\n\t\treadonly readonly?: boolean;\n\t\treadonly required?: boolean;\n\t\treadonly selected?: boolean;\n\t};\n};\n\n/**\n`<Box>` is an essential Ink component to build your layout. It's like `<div style=\"display: flex\">` in the browser.\n*/\nconst Box = forwardRef<DOMElement, PropsWithChildren<Props>>(\n\t(\n\t\t{\n\t\t\tchildren,\n\t\t\tbackgroundColor,\n\t\t\t'aria-label': ariaLabel,\n\t\t\t'aria-hidden': ariaHidden,\n\t\t\t'aria-role': role,\n\t\t\t'aria-state': ariaState,\n\t\t\t...style\n\t\t},\n\t\tref,\n\t) => {\n\t\tconst {isScreenReaderEnabled} = useContext(accessibilityContext);\n\t\tconst label = ariaLabel ? <ink-text>{ariaLabel}</ink-text> : undefined;\n\t\tif (isScreenReaderEnabled && ariaHidden) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst boxElement = (\n\t\t\t<ink-box\n\t\t\t\tref={ref}\n\t\t\t\tstyle={{\n\t\t\t\t\tflexWrap: 'nowrap',\n\t\t\t\t\tflexDirection: 'row',\n\t\t\t\t\tflexGrow: 0,\n\t\t\t\t\tflexShrink: 1,\n\t\t\t\t\t...style,\n\t\t\t\t\tbackgroundColor,\n\t\t\t\t\toverflowX: style.overflowX ?? style.overflow ?? 'visible',\n\t\t\t\t\toverflowY: style.overflowY ?? style.overflow ?? 'visible',\n\t\t\t\t}}\n\t\t\t\tinternal_accessibility={{\n\t\t\t\t\trole,\n\t\t\t\t\tstate: ariaState,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{isScreenReaderEnabled && label ? label : children}\n\t\t\t</ink-box>\n\t\t);\n\n\t\t// If this Box has a background color, provide it to children via context\n\t\tif (backgroundColor) {\n\t\t\treturn (\n\t\t\t\t<backgroundContext.Provider value={backgroundColor}>\n\t\t\t\t\t{boxElement}\n\t\t\t\t</backgroundContext.Provider>\n\t\t\t);\n\t\t}\n\n\t\treturn boxElement;\n\t},\n);\n\nBox.displayName = 'Box';\n\nexport default Box;\n"
  },
  {
    "path": "src/components/CursorContext.ts",
    "content": "import {createContext} from 'react';\nimport {type CursorPosition} from '../log-update.js';\n\nexport type Props = {\n\t/**\n\tSet the cursor position relative to the Ink output.\n\n\tPass `undefined` to hide the cursor.\n\t*/\n\treadonly setCursorPosition: (position: CursorPosition | undefined) => void;\n};\n\n// eslint-disable-next-line @typescript-eslint/naming-convention\nconst CursorContext = createContext<Props>({\n\tsetCursorPosition() {},\n});\n\nCursorContext.displayName = 'InternalCursorContext';\n\nexport default CursorContext;\n"
  },
  {
    "path": "src/components/ErrorBoundary.tsx",
    "content": "import React, {PureComponent, type ReactNode} from 'react';\nimport ErrorOverview from './ErrorOverview.js';\n\ntype Props = {\n\treadonly children: ReactNode;\n\treadonly onError: (error: Error) => void;\n};\n\ntype State = {\n\treadonly error?: Error;\n};\n\n// Error boundary must be a class component since getDerivedStateFromError\n// and componentDidCatch are not available as hooks\nexport default class ErrorBoundary extends PureComponent<Props, State> {\n\tstatic displayName = 'InternalErrorBoundary';\n\n\tstatic getDerivedStateFromError(error: Error) {\n\t\treturn {error};\n\t}\n\n\toverride state: State = {\n\t\terror: undefined,\n\t};\n\n\toverride componentDidCatch(error: Error): void {\n\t\tthis.props.onError(error);\n\t}\n\n\toverride render(): ReactNode {\n\t\tif (this.state.error) {\n\t\t\treturn <ErrorOverview error={this.state.error} />;\n\t\t}\n\n\t\treturn this.props.children;\n\t}\n}\n"
  },
  {
    "path": "src/components/ErrorOverview.tsx",
    "content": "import * as fs from 'node:fs';\nimport {cwd} from 'node:process';\nimport React from 'react';\nimport StackUtils from 'stack-utils';\nimport codeExcerpt, {type CodeExcerpt} from 'code-excerpt';\nimport Box from './Box.js';\nimport Text from './Text.js';\n\n// Error's source file is reported as file:///home/user/file.js\n// This function removes the file://[cwd] part\nconst cleanupPath = (path: string | undefined): string | undefined => {\n\treturn path?.replace(`file://${cwd()}/`, '');\n};\n\nconst stackUtils = new StackUtils({\n\tcwd: cwd(),\n\tinternals: StackUtils.nodeInternals(),\n});\n\ntype Props = {\n\treadonly error: Error;\n};\n\nexport default function ErrorOverview({error}: Props) {\n\tconst stack = error.stack ? error.stack.split('\\n').slice(1) : undefined;\n\tconst origin = stack ? stackUtils.parseLine(stack[0]!) : undefined;\n\tconst filePath = cleanupPath(origin?.file);\n\tlet excerpt: CodeExcerpt[] | undefined;\n\tlet lineWidth = 0;\n\n\tif (filePath && origin?.line && fs.existsSync(filePath)) {\n\t\tconst sourceCode = fs.readFileSync(filePath, 'utf8');\n\t\texcerpt = codeExcerpt(sourceCode, origin.line);\n\n\t\tif (excerpt) {\n\t\t\tfor (const {line} of excerpt) {\n\t\t\t\tlineWidth = Math.max(lineWidth, String(line).length);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<Box>\n\t\t\t\t<Text backgroundColor=\"red\" color=\"white\">\n\t\t\t\t\t{' '}\n\t\t\t\t\tERROR{' '}\n\t\t\t\t</Text>\n\n\t\t\t\t<Text> {error.message}</Text>\n\t\t\t</Box>\n\n\t\t\t{origin && filePath ? (\n\t\t\t\t<Box marginTop={1}>\n\t\t\t\t\t<Text dimColor>\n\t\t\t\t\t\t{filePath}:{origin.line}:{origin.column}\n\t\t\t\t\t</Text>\n\t\t\t\t</Box>\n\t\t\t) : null}\n\n\t\t\t{origin && excerpt ? (\n\t\t\t\t<Box marginTop={1} flexDirection=\"column\">\n\t\t\t\t\t{excerpt.map(({line, value}) => (\n\t\t\t\t\t\t<Box key={line}>\n\t\t\t\t\t\t\t<Box width={lineWidth + 1}>\n\t\t\t\t\t\t\t\t<Text\n\t\t\t\t\t\t\t\t\tdimColor={line !== origin.line}\n\t\t\t\t\t\t\t\t\tbackgroundColor={line === origin.line ? 'red' : undefined}\n\t\t\t\t\t\t\t\t\tcolor={line === origin.line ? 'white' : undefined}\n\t\t\t\t\t\t\t\t\taria-label={\n\t\t\t\t\t\t\t\t\t\tline === origin.line\n\t\t\t\t\t\t\t\t\t\t\t? `Line ${line}, error`\n\t\t\t\t\t\t\t\t\t\t\t: `Line ${line}`\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{String(line).padStart(lineWidth, ' ')}:\n\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t</Box>\n\n\t\t\t\t\t\t\t<Text\n\t\t\t\t\t\t\t\tkey={line}\n\t\t\t\t\t\t\t\tbackgroundColor={line === origin.line ? 'red' : undefined}\n\t\t\t\t\t\t\t\tcolor={line === origin.line ? 'white' : undefined}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{' ' + value}\n\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t</Box>\n\t\t\t\t\t))}\n\t\t\t\t</Box>\n\t\t\t) : null}\n\n\t\t\t{error.stack ? (\n\t\t\t\t<Box marginTop={1} flexDirection=\"column\">\n\t\t\t\t\t{error.stack\n\t\t\t\t\t\t.split('\\n')\n\t\t\t\t\t\t.slice(1)\n\t\t\t\t\t\t.map(line => {\n\t\t\t\t\t\t\tconst parsedLine = stackUtils.parseLine(line);\n\n\t\t\t\t\t\t\t// If the line from the stack cannot be parsed, we print out the unparsed line.\n\t\t\t\t\t\t\tif (!parsedLine) {\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<Box key={line}>\n\t\t\t\t\t\t\t\t\t\t<Text dimColor>- </Text>\n\t\t\t\t\t\t\t\t\t\t<Text dimColor bold>\n\t\t\t\t\t\t\t\t\t\t\t{line}\n\t\t\t\t\t\t\t\t\t\t\t\\t{' '}\n\t\t\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t\t\t</Box>\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Box key={line}>\n\t\t\t\t\t\t\t\t\t<Text dimColor>- </Text>\n\t\t\t\t\t\t\t\t\t<Text dimColor bold>\n\t\t\t\t\t\t\t\t\t\t{parsedLine.function}\n\t\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t\t\t<Text\n\t\t\t\t\t\t\t\t\t\tdimColor\n\t\t\t\t\t\t\t\t\t\tcolor=\"gray\"\n\t\t\t\t\t\t\t\t\t\taria-label={`at ${\n\t\t\t\t\t\t\t\t\t\t\tcleanupPath(parsedLine.file) ?? ''\n\t\t\t\t\t\t\t\t\t\t} line ${parsedLine.line} column ${parsedLine.column}`}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{' '}\n\t\t\t\t\t\t\t\t\t\t({cleanupPath(parsedLine.file) ?? ''}:{parsedLine.line}:\n\t\t\t\t\t\t\t\t\t\t{parsedLine.column})\n\t\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t\t</Box>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t</Box>\n\t\t\t) : null}\n\t\t</Box>\n\t);\n}\n"
  },
  {
    "path": "src/components/FocusContext.ts",
    "content": "import {createContext} from 'react';\n\nexport type Props = {\n\treadonly activeId?: string;\n\treadonly add: (id: string, options: {autoFocus: boolean}) => void;\n\treadonly remove: (id: string) => void;\n\treadonly activate: (id: string) => void;\n\treadonly deactivate: (id: string) => void;\n\treadonly enableFocus: () => void;\n\treadonly disableFocus: () => void;\n\treadonly focusNext: () => void;\n\treadonly focusPrevious: () => void;\n\treadonly focus: (id: string) => void;\n};\n\n// eslint-disable-next-line @typescript-eslint/naming-convention\nconst FocusContext = createContext<Props>({\n\tactiveId: undefined,\n\tadd() {},\n\tremove() {},\n\tactivate() {},\n\tdeactivate() {},\n\tenableFocus() {},\n\tdisableFocus() {},\n\tfocusNext() {},\n\tfocusPrevious() {},\n\tfocus() {},\n});\n\nFocusContext.displayName = 'InternalFocusContext';\n\nexport default FocusContext;\n"
  },
  {
    "path": "src/components/Newline.tsx",
    "content": "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?: number;\n};\n\n/**\nAdds one or more newline (`\\n`) characters. Must be used within `<Text>` components.\n*/\nexport default function Newline({count = 1}: Props) {\n\treturn <ink-text>{'\\n'.repeat(count)}</ink-text>;\n}\n"
  },
  {
    "path": "src/components/Spacer.tsx",
    "content": "import React from 'react';\nimport Box from './Box.js';\n\n/**\nA flexible space that expands along the major axis of its containing layout.\n\nIt's useful as a shortcut for filling all the available space between elements.\n*/\nexport default function Spacer() {\n\treturn <Box flexGrow={1} />;\n}\n"
  },
  {
    "path": "src/components/Static.tsx",
    "content": "import React, {useMemo, useState, useLayoutEffect, type ReactNode} from 'react';\nimport {type Styles} from '../styles.js';\n\nexport type Props<T> = {\n\t/**\n\tArray of items of any type to render using the function you pass as a component child.\n\t*/\n\treadonly items: T[];\n\n\t/**\n\tStyles to apply to a container of child elements. See <Box> for supported properties.\n\t*/\n\treadonly style?: Styles;\n\n\t/**\n\tFunction 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.\n\t*/\n\treadonly children: (item: T, index: number) => ReactNode;\n};\n\n/**\n`<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\").\n\nIt'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.\n\nFor 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.\n*/\nexport default function Static<T>(props: Props<T>) {\n\tconst {items, children: render, style: customStyle} = props;\n\tconst [index, setIndex] = useState(0);\n\n\tconst itemsToRender: T[] = useMemo(() => {\n\t\treturn items.slice(index);\n\t}, [items, index]);\n\n\tuseLayoutEffect(() => {\n\t\tsetIndex(items.length);\n\t}, [items.length]);\n\n\tconst children = itemsToRender.map((item, itemIndex): ReactNode => {\n\t\treturn render(item, index + itemIndex);\n\t});\n\n\tconst style: Styles = useMemo(\n\t\t() => ({\n\t\t\tposition: 'absolute',\n\t\t\tflexDirection: 'column',\n\t\t\t...customStyle,\n\t\t}),\n\t\t[customStyle],\n\t);\n\n\treturn (\n\t\t<ink-box internal_static style={style}>\n\t\t\t{children}\n\t\t</ink-box>\n\t);\n}\n"
  },
  {
    "path": "src/components/StderrContext.ts",
    "content": "import process from 'node:process';\nimport {createContext} from 'react';\n\nexport type Props = {\n\t/**\n\tStderr stream passed to `render()` in `options.stderr` or `process.stderr` by default.\n\t*/\n\treadonly stderr: NodeJS.WriteStream;\n\n\t/**\n\tWrite 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.\n\t*/\n\treadonly write: (data: string) => void;\n};\n\n/**\n`StderrContext` is a React context that exposes the stderr stream.\n*/\n// eslint-disable-next-line @typescript-eslint/naming-convention\nconst StderrContext = createContext<Props>({\n\tstderr: process.stderr,\n\twrite() {},\n});\n\nStderrContext.displayName = 'InternalStderrContext';\n\nexport default StderrContext;\n"
  },
  {
    "path": "src/components/StdinContext.ts",
    "content": "import {EventEmitter} from 'node:events';\nimport process from 'node:process';\nimport {createContext} from 'react';\n\nexport type PublicProps = {\n\t/**\n\tThe stdin stream passed to `render()` in `options.stdin`, or `process.stdin` by default. Useful if your app needs to handle user input.\n\t*/\n\treadonly stdin: NodeJS.ReadStream;\n\n\t/**\n\tInk 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.\n\t*/\n\treadonly setRawMode: (value: boolean) => void;\n\n\t/**\n\tA 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.\n\t*/\n\treadonly isRawModeSupported: boolean;\n};\n\nexport type Props = PublicProps & {\n\t/**\n\tEnable 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.\n\t*/\n\treadonly setBracketedPasteMode: (value: boolean) => void;\n\n\treadonly internal_exitOnCtrlC: boolean;\n\n\treadonly internal_eventEmitter: EventEmitter;\n};\n\n/**\n`StdinContext` is a React context that exposes the input stream.\n*/\n// eslint-disable-next-line @typescript-eslint/naming-convention\nconst StdinContext = createContext<Props>({\n\tstdin: process.stdin,\n\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\tinternal_eventEmitter: new EventEmitter(),\n\tsetRawMode() {},\n\tsetBracketedPasteMode() {},\n\tisRawModeSupported: false,\n\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\tinternal_exitOnCtrlC: true,\n});\n\nStdinContext.displayName = 'InternalStdinContext';\n\nexport default StdinContext;\n"
  },
  {
    "path": "src/components/StdoutContext.ts",
    "content": "import process from 'node:process';\nimport {createContext} from 'react';\n\nexport type Props = {\n\t/**\n\tStdout stream passed to `render()` in `options.stdout` or `process.stdout` by default.\n\t*/\n\treadonly stdout: NodeJS.WriteStream;\n\n\t/**\n\tWrite 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.\n\t*/\n\treadonly write: (data: string) => void;\n};\n\n/**\n`StdoutContext` is a React context that exposes the stdout stream where Ink renders your app.\n*/\n// eslint-disable-next-line @typescript-eslint/naming-convention\nconst StdoutContext = createContext<Props>({\n\tstdout: process.stdout,\n\twrite() {},\n});\n\nStdoutContext.displayName = 'InternalStdoutContext';\n\nexport default StdoutContext;\n"
  },
  {
    "path": "src/components/Text.tsx",
    "content": "import React, {useContext, type ReactNode} from 'react';\nimport chalk, {type ForegroundColorName} from 'chalk';\nimport {type LiteralUnion} from 'type-fest';\nimport colorize from '../colorize.js';\nimport {type Styles} from '../styles.js';\nimport {accessibilityContext} from './AccessibilityContext.js';\nimport {backgroundContext} from './BackgroundContext.js';\n\nexport type Props = {\n\t/**\n\tA label for the element for screen readers.\n\t*/\n\treadonly 'aria-label'?: string;\n\n\t/**\n\tHide the element from screen readers.\n\t*/\n\treadonly 'aria-hidden'?: boolean;\n\n\t/**\n\tChange text color. Ink uses Chalk under the hood, so all its functionality is supported.\n\t*/\n\treadonly color?: LiteralUnion<ForegroundColorName, string>;\n\n\t/**\n\tSame as `color`, but for the background.\n\t*/\n\treadonly backgroundColor?: LiteralUnion<ForegroundColorName, string>;\n\n\t/**\n\tDim the color (make it less bright).\n\t*/\n\treadonly dimColor?: boolean;\n\n\t/**\n\tMake the text bold.\n\t*/\n\treadonly bold?: boolean;\n\n\t/**\n\tMake the text italic.\n\t*/\n\treadonly italic?: boolean;\n\n\t/**\n\tMake the text underlined.\n\t*/\n\treadonly underline?: boolean;\n\n\t/**\n\tMake the text crossed out with a line.\n\t*/\n\treadonly strikethrough?: boolean;\n\n\t/**\n\tInverse background and foreground colors.\n\t*/\n\treadonly inverse?: boolean;\n\n\t/**\n\tThis 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.\n\t*/\n\treadonly wrap?: Styles['textWrap'];\n\n\treadonly children?: ReactNode;\n};\n\n/**\nThis component can display text and change its style to make it bold, underlined, italic, or strikethrough.\n*/\nexport default function Text({\n\tcolor,\n\tbackgroundColor,\n\tdimColor = false,\n\tbold = false,\n\titalic = false,\n\tunderline = false,\n\tstrikethrough = false,\n\tinverse = false,\n\twrap = 'wrap',\n\tchildren,\n\t'aria-label': ariaLabel,\n\t'aria-hidden': ariaHidden = false,\n}: Props) {\n\tconst {isScreenReaderEnabled} = useContext(accessibilityContext);\n\tconst inheritedBackgroundColor = useContext(backgroundContext);\n\tconst childrenOrAriaLabel =\n\t\tisScreenReaderEnabled && ariaLabel ? ariaLabel : children;\n\n\tif (childrenOrAriaLabel === undefined || childrenOrAriaLabel === null) {\n\t\treturn null;\n\t}\n\n\tconst transform = (children: string): string => {\n\t\tif (dimColor) {\n\t\t\tchildren = chalk.dim(children);\n\t\t}\n\n\t\tif (color) {\n\t\t\tchildren = colorize(children, color, 'foreground');\n\t\t}\n\n\t\t// Use explicit backgroundColor if provided, otherwise use inherited from parent Box\n\t\tconst effectiveBackgroundColor =\n\t\t\tbackgroundColor ?? inheritedBackgroundColor;\n\t\tif (effectiveBackgroundColor) {\n\t\t\tchildren = colorize(children, effectiveBackgroundColor, 'background');\n\t\t}\n\n\t\tif (bold) {\n\t\t\tchildren = chalk.bold(children);\n\t\t}\n\n\t\tif (italic) {\n\t\t\tchildren = chalk.italic(children);\n\t\t}\n\n\t\tif (underline) {\n\t\t\tchildren = chalk.underline(children);\n\t\t}\n\n\t\tif (strikethrough) {\n\t\t\tchildren = chalk.strikethrough(children);\n\t\t}\n\n\t\tif (inverse) {\n\t\t\tchildren = chalk.inverse(children);\n\t\t}\n\n\t\treturn children;\n\t};\n\n\tif (isScreenReaderEnabled && ariaHidden) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t<ink-text\n\t\t\tstyle={{flexGrow: 0, flexShrink: 1, flexDirection: 'row', textWrap: wrap}}\n\t\t\tinternal_transform={transform}\n\t\t>\n\t\t\t{isScreenReaderEnabled && ariaLabel ? ariaLabel : children}\n\t\t</ink-text>\n\t);\n}\n"
  },
  {
    "path": "src/components/Transform.tsx",
    "content": "import React, {useContext, type ReactNode} from 'react';\nimport {accessibilityContext} from './AccessibilityContext.js';\n\nexport type Props = {\n\t/**\n\tScreen-reader-specific text to output. If this is set, all children will be ignored.\n\t*/\n\treadonly accessibilityLabel?: string;\n\n\t/**\n\tFunction 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.\n\t*/\n\treadonly transform: (children: string, index: number) => string;\n\n\treadonly children?: ReactNode;\n};\n\n/**\nTransform 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.\n*/\nexport default function Transform({\n\tchildren,\n\ttransform,\n\taccessibilityLabel,\n}: Props) {\n\tconst {isScreenReaderEnabled} = useContext(accessibilityContext);\n\n\tif (children === undefined || children === null) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t<ink-text\n\t\t\tstyle={{flexGrow: 0, flexShrink: 1, flexDirection: 'row'}}\n\t\t\tinternal_transform={transform}\n\t\t>\n\t\t\t{isScreenReaderEnabled && accessibilityLabel\n\t\t\t\t? accessibilityLabel\n\t\t\t\t: children}\n\t\t</ink-text>\n\t);\n}\n"
  },
  {
    "path": "src/cursor-helpers.ts",
    "content": "import ansiEscapes from 'ansi-escapes';\n\nexport type CursorPosition = {\n\tx: number;\n\ty: number;\n};\n\nconst showCursorEscape = '\\u001B[?25h';\nconst hideCursorEscape = '\\u001B[?25l';\n\nexport {showCursorEscape, hideCursorEscape};\n\n/**\nCompare two cursor positions. Returns true if they differ.\n*/\nexport const cursorPositionChanged = (\n\ta: CursorPosition | undefined,\n\tb: CursorPosition | undefined,\n): boolean => a?.x !== b?.x || a?.y !== b?.y;\n\n/**\nBuild escape sequence to move cursor from bottom of output to the target position and show it.\nAssumes cursor is at (col 0, line visibleLineCount) — i.e. just after the last output line.\n*/\nexport const buildCursorSuffix = (\n\tvisibleLineCount: number,\n\tcursorPosition: CursorPosition | undefined,\n): string => {\n\tif (!cursorPosition) {\n\t\treturn '';\n\t}\n\n\tconst moveUp = visibleLineCount - cursorPosition.y;\n\treturn (\n\t\t(moveUp > 0 ? ansiEscapes.cursorUp(moveUp) : '') +\n\t\tansiEscapes.cursorTo(cursorPosition.x) +\n\t\tshowCursorEscape\n\t);\n};\n\n/**\nBuild escape sequence to move cursor from previousCursorPosition back to the bottom of output.\nThis must be done before eraseLines or any operation that assumes cursor is at the bottom.\n*/\nexport const buildReturnToBottom = (\n\tpreviousLineCount: number,\n\tpreviousCursorPosition: CursorPosition | undefined,\n): string => {\n\tif (!previousCursorPosition) {\n\t\treturn '';\n\t}\n\n\t// PreviousLineCount includes trailing newline, so visible lines = previousLineCount - 1\n\t// cursor is at previousCursorPosition.y, need to go to line (previousLineCount - 1)\n\tconst down = previousLineCount - 1 - previousCursorPosition.y;\n\treturn (\n\t\t(down > 0 ? ansiEscapes.cursorDown(down) : '') + ansiEscapes.cursorTo(0)\n\t);\n};\n\nexport type CursorOnlyInput = {\n\tcursorWasShown: boolean;\n\tpreviousLineCount: number;\n\tpreviousCursorPosition: CursorPosition | undefined;\n\tvisibleLineCount: number;\n\tcursorPosition: CursorPosition | undefined;\n};\n\n/**\nBuild the escape sequence for cursor-only updates (output unchanged, cursor moved).\nHides cursor if it was previously shown, returns to bottom, then repositions.\n*/\nexport const buildCursorOnlySequence = (input: CursorOnlyInput): string => {\n\tconst hidePrefix = input.cursorWasShown ? hideCursorEscape : '';\n\tconst returnToBottom = buildReturnToBottom(\n\t\tinput.previousLineCount,\n\t\tinput.previousCursorPosition,\n\t);\n\tconst cursorSuffix = buildCursorSuffix(\n\t\tinput.visibleLineCount,\n\t\tinput.cursorPosition,\n\t);\n\treturn hidePrefix + returnToBottom + cursorSuffix;\n};\n\n/**\nBuild the prefix that hides cursor and returns to bottom before erasing or rewriting.\nReturns empty string if cursor was not shown.\n*/\nexport const buildReturnToBottomPrefix = (\n\tcursorWasShown: boolean,\n\tpreviousLineCount: number,\n\tpreviousCursorPosition: CursorPosition | undefined,\n): string => {\n\tif (!cursorWasShown) {\n\t\treturn '';\n\t}\n\n\treturn (\n\t\thideCursorEscape +\n\t\tbuildReturnToBottom(previousLineCount, previousCursorPosition)\n\t);\n};\n"
  },
  {
    "path": "src/devtools-window-polyfill.ts",
    "content": "// Ignoring missing types error to avoid adding another dependency for this hack to work\nimport ws from 'ws';\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\nconst customGlobal = globalThis as any;\n\n// These things must exist before importing `react-devtools-core`\n// Using ||= intentionally to set falsy values, not just null/undefined\n\n// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing\ncustomGlobal.WebSocket ||= ws;\n\n// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing\ncustomGlobal.window ||= globalThis;\n\n// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing\ncustomGlobal.self ||= globalThis;\n\n// Filter out Ink's internal components from devtools for a cleaner view.\n// Also, ince `react-devtools-shared` package isn't published on npm, we can't\n// use its types, that's why there are hard-coded values in `type` fields below.\n// See https://github.com/facebook/react/blob/edf6eac8a181860fd8a2d076a43806f1237495a1/packages/react-devtools-shared/src/types.js#L24\ncustomGlobal.window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = [\n\t{\n\t\t// ComponentFilterElementType\n\t\ttype: 1,\n\t\t// ElementTypeHostComponent\n\t\tvalue: 7,\n\t\tisEnabled: true,\n\t},\n\t{\n\t\t// ComponentFilterDisplayName\n\t\ttype: 2,\n\t\tvalue: 'InternalApp',\n\t\tisEnabled: true,\n\t\tisValid: true,\n\t},\n\t{\n\t\t// ComponentFilterDisplayName\n\t\ttype: 2,\n\t\tvalue: 'InternalAppContext',\n\t\tisEnabled: true,\n\t\tisValid: true,\n\t},\n\t{\n\t\t// ComponentFilterDisplayName\n\t\ttype: 2,\n\t\tvalue: 'InternalStdoutContext',\n\t\tisEnabled: true,\n\t\tisValid: true,\n\t},\n\t{\n\t\t// ComponentFilterDisplayName\n\t\ttype: 2,\n\t\tvalue: 'InternalStderrContext',\n\t\tisEnabled: true,\n\t\tisValid: true,\n\t},\n\t{\n\t\t// ComponentFilterDisplayName\n\t\ttype: 2,\n\t\tvalue: 'InternalStdinContext',\n\t\tisEnabled: true,\n\t\tisValid: true,\n\t},\n\t{\n\t\t// ComponentFilterDisplayName\n\t\ttype: 2,\n\t\tvalue: 'InternalFocusContext',\n\t\tisEnabled: true,\n\t\tisValid: true,\n\t},\n];\n"
  },
  {
    "path": "src/devtools.ts",
    "content": "/* eslint-disable import-x/order */\n\n// eslint-disable-next-line import-x/no-unassigned-import\nimport './devtools-window-polyfill.js';\n\nimport WebSocket from 'ws';\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-expect-error\nimport devtools from 'react-devtools-core';\n\nconst isDevToolsReachable = async (): Promise<boolean> =>\n\tnew Promise(resolve => {\n\t\tconst socket = new WebSocket('ws://localhost:8097');\n\n\t\tconst timeout = setTimeout(() => {\n\t\t\tsocket.terminate();\n\t\t\tresolve(false);\n\t\t}, 2000);\n\t\t// Don't let the timeout keep the process alive on its own\n\t\ttimeout.unref();\n\n\t\tsocket.on('open', () => {\n\t\t\tclearTimeout(timeout);\n\t\t\tsocket.terminate();\n\t\t\tresolve(true);\n\t\t});\n\n\t\tsocket.on('error', () => {\n\t\t\tclearTimeout(timeout);\n\t\t\tsocket.terminate();\n\t\t\tresolve(false);\n\t\t});\n\t});\n\nif (await isDevToolsReachable()) {\n\t// eslint-disable-next-line @typescript-eslint/no-unsafe-call\n\t(devtools as any).initialize();\n\t// eslint-disable-next-line @typescript-eslint/no-unsafe-call\n\t(devtools as any).connectToDevTools();\n} else {\n\tconsole.warn(\n\t\t'DEV is set to true, but the React DevTools server is not running. Start it with:\\n\\n$ npx react-devtools\\n',\n\t);\n}\n"
  },
  {
    "path": "src/dom.ts",
    "content": "import Yoga, {type Node as YogaNode} from 'yoga-layout';\nimport measureText from './measure-text.js';\nimport {type Styles} from './styles.js';\nimport wrapText from './wrap-text.js';\nimport squashTextNodes from './squash-text-nodes.js';\nimport {type OutputTransformer} from './render-node-to-output.js';\n\ntype InkNode = {\n\tparentNode: DOMElement | undefined;\n\tyogaNode?: YogaNode;\n\tinternal_static?: boolean;\n\tstyle: Styles;\n};\n\ntype LayoutListener = () => void;\n\nexport type TextName = '#text';\nexport type ElementNames =\n\t| 'ink-root'\n\t| 'ink-box'\n\t| 'ink-text'\n\t| 'ink-virtual-text';\n\nexport type NodeNames = ElementNames | TextName;\n\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type DOMElement = {\n\tnodeName: ElementNames;\n\tattributes: Record<string, DOMNodeAttribute>;\n\tchildNodes: DOMNode[];\n\tinternal_transform?: OutputTransformer;\n\n\tinternal_accessibility?: {\n\t\trole?:\n\t\t\t| 'button'\n\t\t\t| 'checkbox'\n\t\t\t| 'combobox'\n\t\t\t| 'list'\n\t\t\t| 'listbox'\n\t\t\t| 'listitem'\n\t\t\t| 'menu'\n\t\t\t| 'menuitem'\n\t\t\t| 'option'\n\t\t\t| 'progressbar'\n\t\t\t| 'radio'\n\t\t\t| 'radiogroup'\n\t\t\t| 'tab'\n\t\t\t| 'tablist'\n\t\t\t| 'table'\n\t\t\t| 'textbox'\n\t\t\t| 'timer'\n\t\t\t| 'toolbar';\n\t\tstate?: {\n\t\t\tbusy?: boolean;\n\t\t\tchecked?: boolean;\n\t\t\tdisabled?: boolean;\n\t\t\texpanded?: boolean;\n\t\t\tmultiline?: boolean;\n\t\t\tmultiselectable?: boolean;\n\t\t\treadonly?: boolean;\n\t\t\trequired?: boolean;\n\t\t\tselected?: boolean;\n\t\t};\n\t};\n\n\t// Internal properties\n\tisStaticDirty?: boolean;\n\tstaticNode?: DOMElement;\n\tonComputeLayout?: () => void;\n\tonRender?: () => void;\n\tonImmediateRender?: () => void;\n\tinternal_layoutListeners?: Set<LayoutListener>;\n} & InkNode;\n\nexport type TextNode = {\n\tnodeName: TextName;\n\tnodeValue: string;\n} & InkNode;\n\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type DOMNode<T = {nodeName: NodeNames}> = T extends {\n\tnodeName: infer U;\n}\n\t? U extends '#text'\n\t\t? TextNode\n\t\t: DOMElement\n\t: never;\n\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type DOMNodeAttribute = boolean | string | number;\n\nexport const createNode = (nodeName: ElementNames): DOMElement => {\n\tconst node: DOMElement = {\n\t\tnodeName,\n\t\tstyle: {},\n\t\tattributes: {},\n\t\tchildNodes: [],\n\t\tparentNode: undefined,\n\t\tyogaNode: nodeName === 'ink-virtual-text' ? undefined : Yoga.Node.create(),\n\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\tinternal_accessibility: {},\n\t};\n\n\tif (nodeName === 'ink-text') {\n\t\tnode.yogaNode?.setMeasureFunc(measureTextNode.bind(null, node));\n\t}\n\n\treturn node;\n};\n\nexport const appendChildNode = (\n\tnode: DOMElement,\n\tchildNode: DOMElement,\n): void => {\n\tif (childNode.parentNode) {\n\t\tremoveChildNode(childNode.parentNode, childNode);\n\t}\n\n\tchildNode.parentNode = node;\n\tnode.childNodes.push(childNode);\n\n\tif (childNode.yogaNode) {\n\t\tnode.yogaNode?.insertChild(\n\t\t\tchildNode.yogaNode,\n\t\t\tnode.yogaNode.getChildCount(),\n\t\t);\n\t}\n\n\tif (node.nodeName === 'ink-text' || node.nodeName === 'ink-virtual-text') {\n\t\tmarkNodeAsDirty(node);\n\t}\n};\n\nexport const insertBeforeNode = (\n\tnode: DOMElement,\n\tnewChildNode: DOMNode,\n\tbeforeChildNode: DOMNode,\n): void => {\n\tif (newChildNode.parentNode) {\n\t\tremoveChildNode(newChildNode.parentNode, newChildNode);\n\t}\n\n\tnewChildNode.parentNode = node;\n\n\tconst index = node.childNodes.indexOf(beforeChildNode);\n\tif (index >= 0) {\n\t\tnode.childNodes.splice(index, 0, newChildNode);\n\t\tif (newChildNode.yogaNode) {\n\t\t\tnode.yogaNode?.insertChild(newChildNode.yogaNode, index);\n\t\t}\n\t} else {\n\t\tnode.childNodes.push(newChildNode);\n\n\t\tif (newChildNode.yogaNode) {\n\t\t\tnode.yogaNode?.insertChild(\n\t\t\t\tnewChildNode.yogaNode,\n\t\t\t\tnode.yogaNode.getChildCount(),\n\t\t\t);\n\t\t}\n\t}\n\n\tif (node.nodeName === 'ink-text' || node.nodeName === 'ink-virtual-text') {\n\t\tmarkNodeAsDirty(node);\n\t}\n};\n\nexport const removeChildNode = (\n\tnode: DOMElement,\n\tremoveNode: DOMNode,\n): void => {\n\tif (removeNode.yogaNode) {\n\t\tremoveNode.parentNode?.yogaNode?.removeChild(removeNode.yogaNode);\n\t}\n\n\tremoveNode.parentNode = undefined;\n\n\tconst index = node.childNodes.indexOf(removeNode);\n\tif (index >= 0) {\n\t\tnode.childNodes.splice(index, 1);\n\t}\n\n\tif (node.nodeName === 'ink-text' || node.nodeName === 'ink-virtual-text') {\n\t\tmarkNodeAsDirty(node);\n\t}\n};\n\nexport const setAttribute = (\n\tnode: DOMElement,\n\tkey: string,\n\tvalue: DOMNodeAttribute,\n): void => {\n\tif (key === 'internal_accessibility') {\n\t\tnode.internal_accessibility = value as DOMElement['internal_accessibility'];\n\t\treturn;\n\t}\n\n\tnode.attributes[key] = value;\n};\n\nexport const setStyle = (node: DOMNode, style?: Styles): void => {\n\t// Rendering code assumes style is always an object.\n\tnode.style = style ?? {};\n};\n\nexport const createTextNode = (text: string): TextNode => {\n\tconst node: TextNode = {\n\t\tnodeName: '#text',\n\t\tnodeValue: text,\n\t\tyogaNode: undefined,\n\t\tparentNode: undefined,\n\t\tstyle: {},\n\t};\n\n\tsetTextNodeValue(node, text);\n\n\treturn node;\n};\n\nconst measureTextNode = function (\n\tnode: DOMNode,\n\twidth: number,\n): {width: number; height: number} {\n\tconst text =\n\t\tnode.nodeName === '#text' ? node.nodeValue : squashTextNodes(node);\n\n\tconst dimensions = measureText(text);\n\n\t// Text fits into container, no need to wrap\n\tif (dimensions.width <= width) {\n\t\treturn dimensions;\n\t}\n\n\t// This is happening when <Box> is shrinking child nodes and Yoga asks\n\t// if we can fit this text node in a <1px space, so we just tell Yoga \"no\"\n\tif (dimensions.width >= 1 && width > 0 && width < 1) {\n\t\treturn dimensions;\n\t}\n\n\tconst textWrap = node.style?.textWrap ?? 'wrap';\n\tconst wrappedText = wrapText(text, width, textWrap);\n\n\treturn measureText(wrappedText);\n};\n\nconst findClosestYogaNode = (node?: DOMNode): YogaNode | undefined => {\n\tif (!node?.parentNode) {\n\t\treturn undefined;\n\t}\n\n\treturn node.yogaNode ?? findClosestYogaNode(node.parentNode);\n};\n\nconst markNodeAsDirty = (node?: DOMNode): void => {\n\t// Mark closest Yoga node as dirty to measure text dimensions again\n\tconst yogaNode = findClosestYogaNode(node);\n\tyogaNode?.markDirty();\n};\n\nexport const setTextNodeValue = (node: TextNode, text: string): void => {\n\tif (typeof text !== 'string') {\n\t\ttext = String(text);\n\t}\n\n\tnode.nodeValue = text;\n\tmarkNodeAsDirty(node);\n};\n\nexport const addLayoutListener = (\n\trootNode: DOMElement,\n\tlistener: LayoutListener,\n): (() => void) => {\n\tif (rootNode.nodeName !== 'ink-root') {\n\t\treturn () => {};\n\t}\n\n\trootNode.internal_layoutListeners ??= new Set();\n\trootNode.internal_layoutListeners.add(listener);\n\n\treturn () => {\n\t\trootNode.internal_layoutListeners?.delete(listener);\n\t};\n};\n\nexport const emitLayoutListeners = (rootNode: DOMElement): void => {\n\tif (rootNode.nodeName !== 'ink-root' || !rootNode.internal_layoutListeners) {\n\t\treturn;\n\t}\n\n\tfor (const listener of rootNode.internal_layoutListeners) {\n\t\tlistener();\n\t}\n};\n"
  },
  {
    "path": "src/get-max-width.ts",
    "content": "import Yoga, {type Node as YogaNode} from 'yoga-layout';\n\nconst getMaxWidth = (yogaNode: YogaNode) => {\n\treturn (\n\t\tyogaNode.getComputedWidth() -\n\t\tyogaNode.getComputedPadding(Yoga.EDGE_LEFT) -\n\t\tyogaNode.getComputedPadding(Yoga.EDGE_RIGHT) -\n\t\tyogaNode.getComputedBorder(Yoga.EDGE_LEFT) -\n\t\tyogaNode.getComputedBorder(Yoga.EDGE_RIGHT)\n\t);\n};\n\nexport default getMaxWidth;\n"
  },
  {
    "path": "src/global.d.ts",
    "content": "import {type ReactNode, type Key, type Ref} from 'react';\nimport {type Except} from 'type-fest';\nimport {type DOMElement} from './dom.js';\nimport {type Styles} from './styles.js';\n\ndeclare module 'react' {\n\tnamespace JSX {\n\t\t// eslint-disable-next-line @typescript-eslint/consistent-type-definitions\n\t\tinterface IntrinsicElements {\n\t\t\t'ink-box': Ink.Box;\n\t\t\t'ink-text': Ink.Text;\n\t\t}\n\t}\n}\n\ndeclare namespace Ink {\n\ttype Box = {\n\t\tinternal_static?: boolean;\n\t\tchildren?: ReactNode;\n\t\tkey?: Key;\n\t\tref?: Ref<DOMElement>;\n\t\tstyle?: Except<Styles, 'textWrap'>;\n\t\tinternal_accessibility?: DOMElement['internal_accessibility'];\n\t};\n\n\ttype Text = {\n\t\tchildren?: ReactNode;\n\t\tkey?: Key;\n\t\tstyle?: Styles;\n\n\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\tinternal_transform?: (children: string, index: number) => string;\n\t\tinternal_accessibility?: DOMElement['internal_accessibility'];\n\t};\n}\n"
  },
  {
    "path": "src/hooks/use-app.ts",
    "content": "import {useContext} from 'react';\nimport AppContext from '../components/AppContext.js';\n\n/**\nA React hook that returns app lifecycle methods like `exit()` and `waitUntilRenderFlush()`.\n*/\nconst useApp = () => useContext(AppContext);\nexport default useApp;\n"
  },
  {
    "path": "src/hooks/use-box-metrics.ts",
    "content": "import {type RefObject, useState, useEffect, useCallback, useMemo} from 'react';\nimport {type DOMElement, addLayoutListener} from '../dom.js';\nimport useStdout from './use-stdout.js';\n\n// Yoga's `right`/`bottom` are omitted: always `0` for flow layout and unintuitive for absolute positioning.\n/**\nMetrics of a box element.\n\nAll positions are relative to the element's parent.\n*/\nexport type BoxMetrics = {\n\t/**\n\tElement width.\n\t*/\n\treadonly width: number;\n\n\t/**\n\tElement height.\n\t*/\n\treadonly height: number;\n\n\t/**\n\tDistance from the left edge of the parent.\n\t*/\n\treadonly left: number;\n\n\t/**\n\tDistance from the top edge of the parent.\n\t*/\n\treadonly top: number;\n};\n\nexport type UseBoxMetricsResult = BoxMetrics & {\n\t/**\n\tWhether the currently tracked element has been measured in the latest layout pass.\n\t*/\n\treadonly hasMeasured: boolean;\n};\n\nconst emptyMetrics: BoxMetrics = {\n\twidth: 0,\n\theight: 0,\n\tleft: 0,\n\ttop: 0,\n};\n\nconst findRootNode = (node?: DOMElement): DOMElement | undefined => {\n\tif (!node) {\n\t\treturn undefined;\n\t}\n\n\tif (!node.parentNode) {\n\t\treturn node.nodeName === 'ink-root' ? node : undefined;\n\t}\n\n\treturn findRootNode(node.parentNode);\n};\n\n/**\nA React hook that returns the current layout metrics for a tracked box element.\nIt updates when layout changes (for example terminal resize, sibling/content changes, or position changes).\n\nThe 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.\n\nUse `hasMeasured` to detect when the currently tracked element has been measured.\n\n@example\n```tsx\nimport {useRef} from 'react';\nimport {Box, Text, useBoxMetrics} from 'ink';\n\nconst Example = () => {\n\tconst ref = useRef(null);\n\tconst {width, height, left, top, hasMeasured} = useBoxMetrics(ref);\n\treturn (\n\t\t<Box ref={ref}>\n\t\t\t<Text>\n\t\t\t\t{hasMeasured ? `${width}x${height} at ${left},${top}` : 'Measuring...'}\n\t\t\t</Text>\n\t\t</Box>\n\t);\n};\n```\n*/\nconst useBoxMetrics = (ref: RefObject<DOMElement>): UseBoxMetricsResult => {\n\tconst [metrics, setMetrics] = useState<BoxMetrics>(emptyMetrics);\n\tconst [hasMeasured, setHasMeasured] = useState(false);\n\tconst {stdout} = useStdout();\n\n\tconst updateMetrics = useCallback(() => {\n\t\tconst layout = ref.current?.yogaNode?.getComputedLayout() ?? emptyMetrics;\n\n\t\tsetMetrics(previousMetrics => {\n\t\t\tconst hasChanged =\n\t\t\t\tpreviousMetrics.width !== layout.width ||\n\t\t\t\tpreviousMetrics.height !== layout.height ||\n\t\t\t\tpreviousMetrics.left !== layout.left ||\n\t\t\t\tpreviousMetrics.top !== layout.top;\n\n\t\t\treturn hasChanged ? layout : previousMetrics;\n\t\t});\n\n\t\tsetHasMeasured(Boolean(ref.current));\n\t}, [ref]);\n\n\t// Runs after every render of this component.\n\t// This keeps metrics fresh when local state/props in this subtree change.\n\tuseEffect(updateMetrics);\n\n\t// Subscribe to root layout commits so memoized components still receive\n\t// sibling-driven position/size updates, even when they skip re-rendering.\n\tuseEffect(() => {\n\t\tconst rootNode = findRootNode(ref.current);\n\n\t\tif (!rootNode) {\n\t\t\treturn;\n\t\t}\n\n\t\treturn addLayoutListener(rootNode, updateMetrics);\n\t});\n\n\t// Terminal resize events do not go through React's render cycle. Ink\n\t// recalculates Yoga layout in its own resize handler — registered in the\n\t// Ink constructor, before this hook mounts — so by the time the resize\n\t// callback runs, Yoga has already computed the post-resize metrics.\n\tuseEffect(() => {\n\t\tstdout.on('resize', updateMetrics);\n\n\t\treturn () => {\n\t\t\tstdout.off('resize', updateMetrics);\n\t\t};\n\t}, [stdout, updateMetrics]);\n\n\treturn useMemo(\n\t\t() => ({\n\t\t\t...metrics,\n\t\t\thasMeasured,\n\t\t}),\n\t\t[metrics, hasMeasured],\n\t);\n};\n\nexport default useBoxMetrics;\n"
  },
  {
    "path": "src/hooks/use-cursor.ts",
    "content": "import {useContext, useRef, useCallback, useInsertionEffect} from 'react';\nimport CursorContext from '../components/CursorContext.js';\nimport {type CursorPosition} from '../log-update.js';\n\n/**\nA React hook that returns methods to control the terminal cursor position.\n\nSetting a cursor position makes the cursor visible at the specified coordinates (relative to the Ink output origin). This is useful for IME (Input Method Editor) support, where the composing character is displayed at the cursor location.\n\nPass `undefined` to hide the cursor.\n*/\nconst useCursor = () => {\n\tconst context = useContext(CursorContext);\n\tconst positionRef = useRef<CursorPosition | undefined>(undefined);\n\n\tconst setCursorPosition = useCallback(\n\t\t(position: CursorPosition | undefined) => {\n\t\t\tpositionRef.current = position;\n\t\t},\n\t\t[],\n\t);\n\n\t// Propagate cursor position to log-update only during commit phase.\n\t// useInsertionEffect runs before resetAfterCommit (which triggers onRender),\n\t// and does NOT run for abandoned concurrent renders (e.g. suspended components).\n\t// This prevents cursor state from leaking across render boundaries.\n\tuseInsertionEffect(() => {\n\t\tcontext.setCursorPosition(positionRef.current);\n\t\treturn () => {\n\t\t\tcontext.setCursorPosition(undefined);\n\t\t};\n\t});\n\n\treturn {setCursorPosition};\n};\n\nexport default useCursor;\n"
  },
  {
    "path": "src/hooks/use-focus-manager.ts",
    "content": "import {useContext} from 'react';\nimport FocusContext, {type Props} from '../components/FocusContext.js';\n\ntype Output = {\n\t/**\n\tEnable focus management for all components.\n\t*/\n\tenableFocus: Props['enableFocus'];\n\n\t/**\n\tDisable focus management for all components. The currently active component (if there's one) will lose its focus.\n\t*/\n\tdisableFocus: Props['disableFocus'];\n\n\t/**\n\tSwitch 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.\n\t*/\n\tfocusNext: Props['focusNext'];\n\n\t/**\n\tSwitch 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.\n\t*/\n\tfocusPrevious: Props['focusPrevious'];\n\n\t/**\n\tSwitch focus to the element with provided `id`. If there's no element with that `id`, focus is not changed.\n\t*/\n\tfocus: Props['focus'];\n\n\t/**\n\tThe ID of the currently focused component, or `undefined` if no component is focused.\n\n\t@example\n\t```tsx\n\timport {Text, useFocusManager} from 'ink';\n\n\tconst Example = () => {\n\t\tconst {activeId} = useFocusManager();\n\n\t\treturn <Text>Focused: {activeId ?? 'none'}</Text>;\n\t};\n\t```\n\t*/\n\tactiveId: Props['activeId'];\n};\n\n/**\nA React hook that returns methods to enable or disable focus management for all components or manually switch focus to the next or previous components.\n*/\nconst useFocusManager = (): Output => {\n\tconst focusContext = useContext(FocusContext);\n\n\treturn {\n\t\tenableFocus: focusContext.enableFocus,\n\t\tdisableFocus: focusContext.disableFocus,\n\t\tfocusNext: focusContext.focusNext,\n\t\tfocusPrevious: focusContext.focusPrevious,\n\t\tfocus: focusContext.focus,\n\t\tactiveId: focusContext.activeId,\n\t};\n};\n\nexport default useFocusManager;\n"
  },
  {
    "path": "src/hooks/use-focus.ts",
    "content": "import {useEffect, useContext, useMemo} from 'react';\nimport FocusContext from '../components/FocusContext.js';\nimport useStdin from './use-stdin.js';\n\ntype Input = {\n\t/**\n\tEnable or disable this component's focus, while still maintaining its position in the list of focusable components.\n\t*/\n\tisActive?: boolean;\n\n\t/**\n\tAuto-focus this component if there's no active (focused) component right now.\n\t*/\n\tautoFocus?: boolean;\n\n\t/**\n\tAssign an ID to this component, so it can be programmatically focused with `focus(id)`.\n\t*/\n\tid?: string;\n};\n\ntype Output = {\n\t/**\n\tDetermines whether this component is focused.\n\t*/\n\tisFocused: boolean;\n\n\t/**\n\tAllows focusing a specific element with the provided `id`.\n\t*/\n\tfocus: (id: string) => void;\n};\n\n/**\nA React hook that returns focus state and focus controls for the current component.\nA 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.\n*/\nconst useFocus = ({\n\tisActive = true,\n\tautoFocus = false,\n\tid: customId,\n}: Input = {}): Output => {\n\tconst {isRawModeSupported, setRawMode} = useStdin();\n\tconst {activeId, add, remove, activate, deactivate, focus} =\n\t\tuseContext(FocusContext);\n\n\tconst id = useMemo(() => {\n\t\treturn customId ?? Math.random().toString().slice(2, 7);\n\t}, [customId]);\n\n\tuseEffect(() => {\n\t\tadd(id, {autoFocus});\n\n\t\treturn () => {\n\t\t\tremove(id);\n\t\t};\n\t}, [id, autoFocus]);\n\n\tuseEffect(() => {\n\t\tif (isActive) {\n\t\t\tactivate(id);\n\t\t} else {\n\t\t\tdeactivate(id);\n\t\t}\n\t}, [isActive, id]);\n\n\tuseEffect(() => {\n\t\tif (!isRawModeSupported || !isActive) {\n\t\t\treturn;\n\t\t}\n\n\t\tsetRawMode(true);\n\n\t\treturn () => {\n\t\t\tsetRawMode(false);\n\t\t};\n\t}, [isActive]);\n\n\treturn {\n\t\tisFocused: Boolean(id) && activeId === id,\n\t\tfocus,\n\t};\n};\n\nexport default useFocus;\n"
  },
  {
    "path": "src/hooks/use-input.ts",
    "content": "import {useEffect} from 'react';\nimport parseKeypress, {nonAlphanumericKeys} from '../parse-keypress.js';\nimport reconciler from '../reconciler.js';\nimport {useStdinContext} from './use-stdin.js';\n\n/**\nHandy information about a key that was pressed.\n*/\nexport type Key = {\n\t/**\n\tUp arrow key was pressed.\n\t*/\n\tupArrow: boolean;\n\n\t/**\n\tDown arrow key was pressed.\n\t*/\n\tdownArrow: boolean;\n\n\t/**\n\tLeft arrow key was pressed.\n\t*/\n\tleftArrow: boolean;\n\n\t/**\n\tRight arrow key was pressed.\n\t*/\n\trightArrow: boolean;\n\n\t/**\n\tPage Down key was pressed.\n\t*/\n\tpageDown: boolean;\n\n\t/**\n\tPage Up key was pressed.\n\t*/\n\tpageUp: boolean;\n\n\t/**\n\tHome key was pressed.\n\t*/\n\thome: boolean;\n\n\t/**\n\tEnd key was pressed.\n\t*/\n\tend: boolean;\n\n\t/**\n\tReturn (Enter) key was pressed.\n\t*/\n\treturn: boolean;\n\n\t/**\n\tEscape key was pressed.\n\t*/\n\tescape: boolean;\n\n\t/**\n\tCtrl key was pressed.\n\t*/\n\tctrl: boolean;\n\n\t/**\n\tShift key was pressed.\n\t*/\n\tshift: boolean;\n\n\t/**\n\tTab key was pressed.\n\t*/\n\ttab: boolean;\n\n\t/**\n\tBackspace key was pressed.\n\t*/\n\tbackspace: boolean;\n\n\t/**\n\tDelete key was pressed.\n\t*/\n\tdelete: boolean;\n\n\t/**\n\t[Meta key](https://en.wikipedia.org/wiki/Meta_key) was pressed.\n\t*/\n\tmeta: boolean;\n\n\t/**\n\tSuper key (Cmd on Mac, Win on Windows) was pressed.\n\n\tOnly available with kitty keyboard protocol.\n\t*/\n\tsuper: boolean;\n\n\t/**\n\tHyper key was pressed.\n\n\tOnly available with kitty keyboard protocol.\n\t*/\n\thyper: boolean;\n\n\t/**\n\tCaps Lock is active.\n\n\tOnly available with kitty keyboard protocol.\n\t*/\n\tcapsLock: boolean;\n\n\t/**\n\tNum Lock is active.\n\n\tOnly available with kitty keyboard protocol.\n\t*/\n\tnumLock: boolean;\n\n\t/**\n\tEvent type for key events.\n\n\tOnly available with kitty keyboard protocol.\n\t*/\n\teventType?: 'press' | 'repeat' | 'release';\n};\n\ntype Handler = (input: string, key: Key) => void;\n\ntype Options = {\n\t/**\n\tEnable or disable capturing of user input. Useful when there are multiple `useInput` hooks used at once to avoid handling the same input several times.\n\n\t@default true\n\t*/\n\tisActive?: boolean;\n};\n\n/**\nA React hook that returns `void` and handles user input.\nIt's a more convenient alternative to using `StdinContext` 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`.\n\n```\nimport {useInput} from 'ink';\n\nconst UserInput = () => {\n  useInput((input, key) => {\n    if (input === 'q') {\n      // Exit program\n    }\n\n    if (key.leftArrow) {\n      // Left arrow key pressed\n    }\n  });\n\n  return …\n};\n```\n*/\nconst useInput = (inputHandler: Handler, options: Options = {}) => {\n\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\tconst {stdin, setRawMode, internal_exitOnCtrlC, internal_eventEmitter} =\n\t\tuseStdinContext();\n\n\tuseEffect(() => {\n\t\tif (options.isActive === false) {\n\t\t\treturn;\n\t\t}\n\n\t\tsetRawMode(true);\n\n\t\treturn () => {\n\t\t\tsetRawMode(false);\n\t\t};\n\t}, [options.isActive, setRawMode]);\n\n\tuseEffect(() => {\n\t\tif (options.isActive === false) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst handleData = (data: string) => {\n\t\t\tconst keypress = parseKeypress(data);\n\n\t\t\tconst key: Key = {\n\t\t\t\tupArrow: keypress.name === 'up',\n\t\t\t\tdownArrow: keypress.name === 'down',\n\t\t\t\tleftArrow: keypress.name === 'left',\n\t\t\t\trightArrow: keypress.name === 'right',\n\t\t\t\tpageDown: keypress.name === 'pagedown',\n\t\t\t\tpageUp: keypress.name === 'pageup',\n\t\t\t\thome: keypress.name === 'home',\n\t\t\t\tend: keypress.name === 'end',\n\t\t\t\treturn: keypress.name === 'return',\n\t\t\t\tescape: keypress.name === 'escape',\n\t\t\t\tctrl: keypress.ctrl,\n\t\t\t\tshift: keypress.shift,\n\t\t\t\ttab: keypress.name === 'tab',\n\t\t\t\tbackspace: keypress.name === 'backspace',\n\t\t\t\tdelete: keypress.name === 'delete',\n\t\t\t\t// `parseKeypress` parses \\u001B\\u001B[A (meta + up arrow) as meta = false\n\t\t\t\t// but with option = true, so we need to take this into account here\n\t\t\t\t// to avoid breaking changes in Ink.\n\t\t\t\t// TODO(vadimdemedes): consider removing this in the next major version.\n\t\t\t\tmeta: keypress.meta || keypress.name === 'escape' || keypress.option,\n\t\t\t\t// Kitty keyboard protocol modifiers\n\t\t\t\tsuper: keypress.super ?? false,\n\t\t\t\thyper: keypress.hyper ?? false,\n\t\t\t\tcapsLock: keypress.capsLock ?? false,\n\t\t\t\tnumLock: keypress.numLock ?? false,\n\t\t\t\teventType: keypress.eventType,\n\t\t\t};\n\n\t\t\tlet input: string;\n\t\t\tif (keypress.isKittyProtocol) {\n\t\t\t\t// Use text-as-codepoints field for printable keys (needed when\n\t\t\t\t// reportAllKeysAsEscapeCodes flag is enabled), suppress non-printable\n\t\t\t\tif (keypress.isPrintable) {\n\t\t\t\t\tinput = keypress.text ?? keypress.name;\n\t\t\t\t} else if (keypress.ctrl && keypress.name.length === 1) {\n\t\t\t\t\t// Ctrl+letter via codepoint 1-26 form: not printable text, but\n\t\t\t\t\t// the letter name must flow through so handlers (e.g. exitOnCtrlC\n\t\t\t\t\t// checking `input === 'c' && key.ctrl`) still work.\n\t\t\t\t\tinput = keypress.name;\n\t\t\t\t} else {\n\t\t\t\t\tinput = '';\n\t\t\t\t}\n\t\t\t} else if (keypress.ctrl) {\n\t\t\t\tinput = keypress.name;\n\t\t\t} else {\n\t\t\t\tinput = keypress.sequence;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!keypress.isKittyProtocol &&\n\t\t\t\tnonAlphanumericKeys.includes(keypress.name)\n\t\t\t) {\n\t\t\t\tinput = '';\n\t\t\t}\n\n\t\t\t// Strip meta if it's still remaining after `parseKeypress`\n\t\t\t// TODO(vadimdemedes): remove this in the next major version.\n\t\t\tif (input.startsWith('\\u001B')) {\n\t\t\t\tinput = input.slice(1);\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tinput.length === 1 &&\n\t\t\t\ttypeof input[0] === 'string' &&\n\t\t\t\t/[A-Z]/.test(input[0])\n\t\t\t) {\n\t\t\t\tkey.shift = true;\n\t\t\t}\n\n\t\t\t// If app is supposed to exit on Ctrl+C, skip input listeners.\n\t\t\tif (input === 'c' && key.ctrl && internal_exitOnCtrlC) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Use discreteUpdates to assign DiscreteEventPriority to state\n\t\t\t// updates from keyboard input, ensuring they are processed at the\n\t\t\t// highest priority in concurrent mode.\n\t\t\t// @ts-expect-error Types require 5 arguments (fn, a, b, c, d) but only fn is needed at runtime.\n\t\t\treconciler.discreteUpdates(() => {\n\t\t\t\tinputHandler(input, key);\n\t\t\t});\n\t\t};\n\n\t\tinternal_eventEmitter.on('input', handleData);\n\n\t\treturn () => {\n\t\t\tinternal_eventEmitter.removeListener('input', handleData);\n\t\t};\n\t}, [options.isActive, stdin, internal_exitOnCtrlC, inputHandler]);\n};\n\nexport default useInput;\n"
  },
  {
    "path": "src/hooks/use-is-screen-reader-enabled.ts",
    "content": "import {useContext} from 'react';\nimport {accessibilityContext} from '../components/AccessibilityContext.js';\n\n/**\nA React hook that returns whether a screen reader is enabled.\nThis is useful when you want to render different output for screen readers.\n*/\nconst useIsScreenReaderEnabled = (): boolean => {\n\tconst {isScreenReaderEnabled} = useContext(accessibilityContext);\n\treturn isScreenReaderEnabled;\n};\n\nexport default useIsScreenReaderEnabled;\n"
  },
  {
    "path": "src/hooks/use-paste.ts",
    "content": "import {useEffect} from 'react';\nimport reconciler from '../reconciler.js';\nimport {useStdinContext} from './use-stdin.js';\n\ntype Options = {\n\t/**\n\tEnable or disable the paste handler. Useful when multiple components use `usePaste` and only one should be active at a time.\n\n\t@default true\n\t*/\n\tisActive?: boolean;\n};\n\n/**\nA React hook that calls `handler` whenever the user pastes text in the terminal. 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.\n\n`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.\n\n```\nimport {useInput, usePaste} from 'ink';\n\nconst MyInput = () => {\n\tuseInput((input, key) => {\n\t\t// Only receives typed characters and key events, not pasted text.\n\t\tif (key.return) {\n\t\t\t// Submit\n\t\t}\n\t});\n\n\tusePaste((text) => {\n\t\t// Receives the full pasted string, including newlines.\n\t\tconsole.log('Pasted:', text);\n\t});\n\n\treturn …\n};\n```\n*/\nconst usePaste = (\n\thandler: (text: string) => void,\n\toptions: Options = {},\n): void => {\n\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\tconst {setRawMode, setBracketedPasteMode, internal_eventEmitter} =\n\t\tuseStdinContext();\n\n\tuseEffect(() => {\n\t\tif (options.isActive === false) {\n\t\t\treturn;\n\t\t}\n\n\t\tsetRawMode(true);\n\t\tsetBracketedPasteMode(true);\n\n\t\treturn () => {\n\t\t\tsetRawMode(false);\n\t\t\tsetBracketedPasteMode(false);\n\t\t};\n\t}, [options.isActive, setRawMode, setBracketedPasteMode]);\n\n\tuseEffect(() => {\n\t\tif (options.isActive === false) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst handlePaste = (text: string) => {\n\t\t\t// Use discreteUpdates to assign DiscreteEventPriority to state\n\t\t\t// updates triggered by paste, matching the priority of useInput.\n\t\t\t// @ts-expect-error Types require 5 arguments (fn, a, b, c, d) but only fn is needed at runtime.\n\t\t\treconciler.discreteUpdates(() => {\n\t\t\t\thandler(text);\n\t\t\t});\n\t\t};\n\n\t\tinternal_eventEmitter.on('paste', handlePaste);\n\n\t\treturn () => {\n\t\t\tinternal_eventEmitter.removeListener('paste', handlePaste);\n\t\t};\n\t}, [options.isActive, internal_eventEmitter, handler]);\n};\n\nexport default usePaste;\n"
  },
  {
    "path": "src/hooks/use-stderr.ts",
    "content": "import {useContext} from 'react';\nimport StderrContext from '../components/StderrContext.js';\n\n/**\nA React hook that returns the stderr stream.\n*/\nconst useStderr = () => useContext(StderrContext);\nexport default useStderr;\n"
  },
  {
    "path": "src/hooks/use-stdin.ts",
    "content": "import {useContext} from 'react';\nimport StdinContext, {\n\ttype PublicProps,\n\ttype Props,\n} from '../components/StdinContext.js';\n\n/**\nA React hook that returns the stdin stream and stdin-related utilities.\n*/\nconst useStdin = (): PublicProps => useContext(StdinContext);\n\nexport const useStdinContext = (): Props => useContext(StdinContext);\n\nexport default useStdin;\n"
  },
  {
    "path": "src/hooks/use-stdout.ts",
    "content": "import {useContext} from 'react';\nimport StdoutContext from '../components/StdoutContext.js';\n\n/**\nA React hook that returns the stdout stream where Ink renders your app.\n*/\nconst useStdout = () => useContext(StdoutContext);\nexport default useStdout;\n"
  },
  {
    "path": "src/hooks/use-window-size.ts",
    "content": "import {useState, useEffect} from 'react';\nimport {getWindowSize} from '../utils.js';\nimport useStdout from './use-stdout.js';\n\n/**\nDimensions of the terminal window.\n*/\nexport type WindowSize = {\n\t/**\n\tNumber of columns (horizontal character cells).\n\t*/\n\treadonly columns: number;\n\n\t/**\n\tNumber of rows (vertical character cells).\n\t*/\n\treadonly rows: number;\n};\n\n/**\nA React hook that returns the current terminal window dimensions and re-renders the component whenever the terminal is resized.\n*/\nconst useWindowSize = (): WindowSize => {\n\tconst {stdout} = useStdout();\n\tconst [size, setSize] = useState<WindowSize>(() => getWindowSize(stdout));\n\n\tuseEffect(() => {\n\t\tconst onResize = () => {\n\t\t\tsetSize(getWindowSize(stdout));\n\t\t};\n\n\t\tstdout.on('resize', onResize);\n\n\t\treturn () => {\n\t\t\tstdout.off('resize', onResize);\n\t\t};\n\t}, [stdout]);\n\n\treturn size;\n};\n\nexport default useWindowSize;\n"
  },
  {
    "path": "src/index.ts",
    "content": "export type {RenderOptions, Instance} from './render.js';\nexport {default as render} from './render.js';\nexport type {RenderToStringOptions} from './render-to-string.js';\nexport {default as renderToString} from './render-to-string.js';\nexport type {Props as BoxProps} from './components/Box.js';\nexport {default as Box} from './components/Box.js';\nexport type {Props as TextProps} from './components/Text.js';\nexport {default as Text} from './components/Text.js';\nexport type {Props as AppProps} from './components/AppContext.js';\nexport type {PublicProps as StdinProps} from './components/StdinContext.js';\nexport type {Props as StdoutProps} from './components/StdoutContext.js';\nexport type {Props as StderrProps} from './components/StderrContext.js';\nexport type {Props as StaticProps} from './components/Static.js';\nexport {default as Static} from './components/Static.js';\nexport type {Props as TransformProps} from './components/Transform.js';\nexport {default as Transform} from './components/Transform.js';\nexport type {Props as NewlineProps} from './components/Newline.js';\nexport {default as Newline} from './components/Newline.js';\nexport {default as Spacer} from './components/Spacer.js';\nexport type {Key} from './hooks/use-input.js';\nexport {default as useInput} from './hooks/use-input.js';\nexport {default as usePaste} from './hooks/use-paste.js';\nexport {default as useApp} from './hooks/use-app.js';\nexport {default as useStdin} from './hooks/use-stdin.js';\nexport {default as useStdout} from './hooks/use-stdout.js';\nexport {default as useStderr} from './hooks/use-stderr.js';\nexport {default as useFocus} from './hooks/use-focus.js';\nexport {default as useFocusManager} from './hooks/use-focus-manager.js';\nexport {default as useIsScreenReaderEnabled} from './hooks/use-is-screen-reader-enabled.js';\nexport {default as useCursor} from './hooks/use-cursor.js';\nexport type {WindowSize} from './hooks/use-window-size.js';\nexport {default as useWindowSize} from './hooks/use-window-size.js';\nexport type {BoxMetrics, UseBoxMetricsResult} from './hooks/use-box-metrics.js';\nexport {default as useBoxMetrics} from './hooks/use-box-metrics.js';\nexport type {CursorPosition} from './log-update.js';\nexport {default as measureElement} from './measure-element.js';\nexport type {DOMElement} from './dom.js';\nexport {kittyFlags, kittyModifiers} from './kitty-keyboard.js';\nexport type {KittyKeyboardOptions, KittyFlagName} from './kitty-keyboard.js';\n"
  },
  {
    "path": "src/ink.tsx",
    "content": "import process from 'node:process';\nimport {Buffer} from 'node:buffer';\nimport React, {type ReactNode} from 'react';\nimport {throttle, type DebouncedFunc} from 'es-toolkit/compat';\nimport ansiEscapes from 'ansi-escapes';\nimport isInCi from 'is-in-ci';\nimport autoBind from 'auto-bind';\nimport signalExit from 'signal-exit';\nimport patchConsole from 'patch-console';\nimport {LegacyRoot, ConcurrentRoot} from 'react-reconciler/constants.js';\nimport {type FiberRoot} from 'react-reconciler';\nimport Yoga from 'yoga-layout';\nimport wrapAnsi from 'wrap-ansi';\nimport {getWindowSize} from './utils.js';\nimport reconciler from './reconciler.js';\nimport render from './renderer.js';\nimport * as dom from './dom.js';\nimport {hideCursorEscape, showCursorEscape} from './cursor-helpers.js';\nimport logUpdate, {type LogUpdate, type CursorPosition} from './log-update.js';\nimport {bsu, esu, shouldSynchronize} from './write-synchronized.js';\nimport instances from './instances.js';\nimport App from './components/App.js';\nimport {accessibilityContext as AccessibilityContext} from './components/AccessibilityContext.js';\nimport {\n\ttype KittyKeyboardOptions,\n\ttype KittyFlagName,\n\tresolveFlags,\n} from './kitty-keyboard.js';\n\nconst noop = () => {};\n\nconst yieldImmediate = async () =>\n\tnew Promise<void>(resolve => {\n\t\tsetImmediate(resolve);\n\t});\n\nconst kittyQueryEscapeByte = 0x1b;\nconst kittyQueryOpenBracketByte = 0x5b;\nconst kittyQueryQuestionMarkByte = 0x3f;\nconst kittyQueryLetterByte = 0x75;\nconst zeroByte = 0x30;\nconst nineByte = 0x39;\n\ntype KittyQueryResponseMatch =\n\t| {state: 'complete'; endIndex: number}\n\t| {state: 'partial'};\n\nconst isDigitByte = (byte: number): boolean =>\n\tbyte >= zeroByte && byte <= nineByte;\n\nconst matchKittyQueryResponse = (\n\tbuffer: number[],\n\tstartIndex: number,\n): KittyQueryResponseMatch | undefined => {\n\tif (\n\t\tbuffer[startIndex] !== kittyQueryEscapeByte ||\n\t\tbuffer[startIndex + 1] !== kittyQueryOpenBracketByte ||\n\t\tbuffer[startIndex + 2] !== kittyQueryQuestionMarkByte\n\t) {\n\t\treturn undefined;\n\t}\n\n\tlet index = startIndex + 3;\n\tconst digitsStartIndex = index;\n\twhile (index < buffer.length && isDigitByte(buffer[index]!)) {\n\t\tindex++;\n\t}\n\n\tif (index === digitsStartIndex) {\n\t\treturn undefined;\n\t}\n\n\tif (index === buffer.length) {\n\t\treturn {state: 'partial'};\n\t}\n\n\tif (buffer[index] === kittyQueryLetterByte) {\n\t\treturn {state: 'complete', endIndex: index};\n\t}\n\n\treturn undefined;\n};\n\nconst hasCompleteKittyQueryResponse = (buffer: number[]): boolean => {\n\tfor (let index = 0; index < buffer.length; index++) {\n\t\tconst match = matchKittyQueryResponse(buffer, index);\n\t\tif (match?.state === 'complete') {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n};\n\nconst stripKittyQueryResponsesAndTrailingPartial = (\n\tbuffer: number[],\n): number[] => {\n\tconst keptBytes: number[] = [];\n\tlet index = 0;\n\twhile (index < buffer.length) {\n\t\tconst match = matchKittyQueryResponse(buffer, index);\n\t\tif (match?.state === 'complete') {\n\t\t\tindex = match.endIndex + 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (match?.state === 'partial') {\n\t\t\tbreak;\n\t\t}\n\n\t\tkeptBytes.push(buffer[index]!);\n\t\tindex++;\n\t}\n\n\treturn keptBytes;\n};\n\nconst shouldClearTerminalForFrame = ({\n\tisTty,\n\tviewportRows,\n\tpreviousOutputHeight,\n\tnextOutputHeight,\n\tisUnmounting,\n}: {\n\tisTty: boolean;\n\tviewportRows: number;\n\tpreviousOutputHeight: number;\n\tnextOutputHeight: number;\n\tisUnmounting: boolean;\n}): boolean => {\n\tif (!isTty) {\n\t\treturn false;\n\t}\n\n\tconst hadPreviousFrame = previousOutputHeight > 0;\n\tconst wasFullscreen = previousOutputHeight >= viewportRows;\n\tconst wasOverflowing = previousOutputHeight > viewportRows;\n\tconst isOverflowing = nextOutputHeight > viewportRows;\n\tconst isLeavingFullscreen = wasFullscreen && nextOutputHeight < viewportRows;\n\tconst shouldClearOnUnmount = isUnmounting && wasFullscreen;\n\n\treturn (\n\t\t// Overflowing frames still need full clear fallback.\n\t\twasOverflowing ||\n\t\t(isOverflowing && hadPreviousFrame) ||\n\t\t// Clear when shrinking from fullscreen to non-fullscreen output.\n\t\tisLeavingFullscreen ||\n\t\t// Preserve legacy unmount behavior for fullscreen frames: final teardown\n\t\t// render should clear once to avoid leaving a scrolled viewport state.\n\t\tshouldClearOnUnmount\n\t);\n};\n\nconst isErrorInput = (value: unknown): value is Error => {\n\treturn (\n\t\tvalue instanceof Error ||\n\t\tObject.prototype.toString.call(value) === '[object Error]'\n\t);\n};\n\ntype MaybeWritableStream = NodeJS.WriteStream & {\n\twritable?: boolean;\n\twritableEnded?: boolean;\n\tdestroyed?: boolean;\n\twritableLength?: number;\n\t_writableState?: unknown;\n};\n\nconst getWritableStreamState = (stdout: MaybeWritableStream) => {\n\tconst canWriteToStdout =\n\t\t!stdout.destroyed && !stdout.writableEnded && (stdout.writable ?? true);\n\tconst hasWritableState =\n\t\tstdout._writableState !== undefined || stdout.writableLength !== undefined;\n\n\treturn {\n\t\tcanWriteToStdout,\n\t\thasWritableState,\n\t};\n};\n\nconst settleThrottle = (\n\tthrottled: unknown,\n\tcanWriteToStdout: boolean,\n): void => {\n\tif (\n\t\t!throttled ||\n\t\ttypeof (throttled as {flush?: unknown}).flush !== 'function'\n\t) {\n\t\treturn;\n\t}\n\n\tconst throttledValue = throttled as {\n\t\tflush: () => void;\n\t\tcancel?: () => void;\n\t};\n\n\tif (canWriteToStdout) {\n\t\tthrottledValue.flush();\n\t} else if (typeof throttledValue.cancel === 'function') {\n\t\tthrottledValue.cancel();\n\t}\n};\n\n/**\nPerformance metrics for a render operation.\n*/\nexport type RenderMetrics = {\n\t/**\n\tTime spent rendering in milliseconds.\n\t*/\n\trenderTime: number;\n};\n\nexport type Options = {\n\tstdout: NodeJS.WriteStream;\n\tstdin: NodeJS.ReadStream;\n\tstderr: NodeJS.WriteStream;\n\tdebug: boolean;\n\texitOnCtrlC: boolean;\n\tpatchConsole: boolean;\n\tonRender?: (metrics: RenderMetrics) => void;\n\tisScreenReaderEnabled?: boolean;\n\twaitUntilExit?: () => Promise<unknown>;\n\tmaxFps?: number;\n\tincrementalRendering?: boolean;\n\n\t/**\n\tEnable React Concurrent Rendering mode.\n\n\tWhen enabled:\n\t- Suspense boundaries work correctly with async data\n\t- `useTransition` and `useDeferredValue` are fully functional\n\t- Updates can be interrupted for higher priority work\n\n\tNote: 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.\n\n\t@default false\n\t@experimental\n\t*/\n\tconcurrent?: boolean;\n\tkittyKeyboard?: KittyKeyboardOptions;\n\n\t/**\n\tOverride automatic interactive mode detection.\n\n\tBy default, Ink detects whether the environment is interactive based on CI detection (via [`is-in-ci`](https://github.com/sindresorhus/is-in-ci)) and `stdout.isTTY`. Most users should not need to set this.\n\n\tWhen non-interactive, Ink disables ANSI erase sequences, cursor manipulation, synchronized output, resize handling, and kitty keyboard auto-detection, writing only the final frame at unmount.\n\n\tSet to `false` to force non-interactive mode or `true` to force interactive mode when the automatic detection doesn't suit your use case.\n\n\tNote: 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.\n\n\t@default true (false if in CI or `stdout.isTTY` is falsy)\n\n\t@see {@link RenderOptions.interactive}\n\t*/\n\tinteractive?: boolean;\n\n\t/**\n\tRender 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.\n\n\tNote: 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.\n\n\tNote: 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.\n\n\tOnly works in interactive mode. Ignored when `interactive` is `false` or in a non-interactive environment (CI, piped stdout).\n\n\tNote: 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.\n\n\t@default false\n\n\t@see {@link RenderOptions.alternateScreen}\n\t*/\n\talternateScreen?: boolean;\n};\n\nexport default class Ink {\n\t/**\n\tWhether this instance is using concurrent rendering mode.\n\t*/\n\treadonly isConcurrent: boolean;\n\n\tprivate readonly options: Options;\n\tprivate readonly log: LogUpdate;\n\tprivate cursorPosition: CursorPosition | undefined;\n\tprivate readonly throttledLog:\n\t\t| LogUpdate\n\t\t| DebouncedFunc<(output: string) => void>;\n\n\tprivate readonly isScreenReaderEnabled: boolean;\n\tprivate readonly interactive: boolean;\n\tprivate alternateScreen: boolean;\n\n\t// Ignore last render after unmounting a tree to prevent empty output before exit\n\tprivate isUnmounted: boolean;\n\tprivate isUnmounting: boolean;\n\tprivate lastOutput: string;\n\tprivate lastOutputToRender: string;\n\tprivate lastOutputHeight: number;\n\tprivate lastTerminalWidth: number;\n\tprivate readonly container: FiberRoot;\n\tprivate readonly rootNode: dom.DOMElement;\n\t// This variable is used only in debug mode to store full static output\n\t// so that it's rerendered every time, not just new static parts, like in non-debug mode\n\tprivate fullStaticOutput: string;\n\tprivate readonly exitPromise!: Promise<unknown>;\n\tprivate exitResult: unknown;\n\tprivate beforeExitHandler?: () => void;\n\tprivate restoreConsole?: () => void;\n\tprivate readonly unsubscribeResize?: () => void;\n\tprivate readonly throttledOnRender?: DebouncedFunc<() => void>;\n\tprivate hasPendingThrottledRender = false;\n\tprivate kittyProtocolEnabled = false;\n\tprivate cancelKittyDetection?: () => void;\n\tprivate nextRenderCommit?: {promise: Promise<void>; resolve: () => void};\n\n\tconstructor(options: Options) {\n\t\tautoBind(this);\n\n\t\tthis.options = options;\n\t\tthis.rootNode = dom.createNode('ink-root');\n\t\tthis.rootNode.onComputeLayout = this.calculateLayout;\n\n\t\tthis.isScreenReaderEnabled =\n\t\t\toptions.isScreenReaderEnabled ??\n\t\t\tprocess.env['INK_SCREEN_READER'] === 'true';\n\n\t\t// CI detection takes precedence: even a TTY stdout in CI defaults to non-interactive.\n\t\t// Using Boolean(isTTY) (rather than an 'in' guard) correctly handles piped streams\n\t\t// where the property is absent (e.g. `node app.js | cat`).\n\t\tthis.interactive = this.resolveInteractiveOption(options.interactive);\n\n\t\tthis.alternateScreen = false;\n\n\t\tconst unthrottled = options.debug || this.isScreenReaderEnabled;\n\t\tconst maxFps = options.maxFps ?? 30;\n\t\tconst renderThrottleMs =\n\t\t\tmaxFps > 0 ? Math.max(1, Math.ceil(1000 / maxFps)) : 0;\n\n\t\tif (unthrottled) {\n\t\t\tthis.rootNode.onRender = this.onRender;\n\t\t\tthis.throttledOnRender = undefined;\n\t\t} else {\n\t\t\tconst throttled = throttle(this.onRender, renderThrottleMs, {\n\t\t\t\tleading: true,\n\t\t\t\ttrailing: true,\n\t\t\t});\n\t\t\tthis.rootNode.onRender = () => {\n\t\t\t\tthis.hasPendingThrottledRender = true;\n\t\t\t\tthrottled();\n\t\t\t};\n\n\t\t\tthis.throttledOnRender = throttled;\n\t\t}\n\n\t\tthis.rootNode.onImmediateRender = this.onRender;\n\t\tthis.log = logUpdate.create(options.stdout, {\n\t\t\tincremental: options.incrementalRendering,\n\t\t});\n\t\tthis.cursorPosition = undefined;\n\t\tthis.throttledLog = unthrottled\n\t\t\t? this.log\n\t\t\t: throttle(\n\t\t\t\t\t(output: string) => {\n\t\t\t\t\t\tconst shouldWrite = this.log.willRender(output);\n\t\t\t\t\t\tconst sync = this.shouldSync();\n\t\t\t\t\t\tif (sync && shouldWrite) {\n\t\t\t\t\t\t\tthis.options.stdout.write(bsu);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthis.log(output);\n\n\t\t\t\t\t\tif (sync && shouldWrite) {\n\t\t\t\t\t\t\tthis.options.stdout.write(esu);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tundefined,\n\t\t\t\t\t{\n\t\t\t\t\t\tleading: true,\n\t\t\t\t\t\ttrailing: true,\n\t\t\t\t\t},\n\t\t\t\t);\n\n\t\t// Ignore last render after unmounting a tree to prevent empty output before exit\n\t\tthis.isUnmounted = false;\n\t\tthis.isUnmounting = false;\n\n\t\t// Store concurrent mode setting\n\t\tthis.isConcurrent = options.concurrent ?? false;\n\n\t\t// Store last output to only rerender when needed\n\t\tthis.lastOutput = '';\n\t\tthis.lastOutputToRender = '';\n\t\tthis.lastOutputHeight = 0;\n\t\tthis.lastTerminalWidth = getWindowSize(this.options.stdout).columns;\n\n\t\t// This variable is used only in debug mode to store full static output\n\t\t// so that it's rerendered every time, not just new static parts, like in non-debug mode\n\t\tthis.fullStaticOutput = '';\n\n\t\t// Use ConcurrentRoot for concurrent mode, LegacyRoot for legacy mode\n\t\tconst rootTag = options.concurrent ? ConcurrentRoot : LegacyRoot;\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n\t\tthis.container = reconciler.createContainer(\n\t\t\tthis.rootNode,\n\t\t\trootTag,\n\t\t\tnull,\n\t\t\tfalse,\n\t\t\tnull,\n\t\t\t'id',\n\t\t\t() => {},\n\t\t\t() => {},\n\t\t\t() => {},\n\t\t\t() => {},\n\t\t);\n\n\t\t// Unmount when process exits\n\t\tthis.unsubscribeExit = signalExit(this.unmount, {alwaysLast: false});\n\n\t\tthis.setAlternateScreen(Boolean(options.alternateScreen));\n\n\t\tif (process.env['DEV'] === 'true') {\n\t\t\t// @ts-expect-error outdated types\n\t\t\treconciler.injectIntoDevTools();\n\t\t}\n\n\t\tif (options.patchConsole) {\n\t\t\tthis.patchConsole();\n\t\t}\n\n\t\tif (this.interactive) {\n\t\t\toptions.stdout.on('resize', this.resized);\n\n\t\t\tthis.unsubscribeResize = () => {\n\t\t\t\toptions.stdout.off('resize', this.resized);\n\t\t\t};\n\t\t}\n\n\t\tthis.initKittyKeyboard();\n\n\t\tthis.exitPromise = new Promise((resolve, reject) => {\n\t\t\tthis.resolveExitPromise = resolve;\n\t\t\tthis.rejectExitPromise = reject;\n\t\t});\n\t\t// Prevent global unhandled-rejection crashes when app code exits with an\n\t\t// error but consumers never call waitUntilExit().\n\n\t\tvoid this.exitPromise.catch(noop);\n\t}\n\n\tresized = () => {\n\t\tconst currentWidth = getWindowSize(this.options.stdout).columns;\n\n\t\tif (currentWidth < this.lastTerminalWidth) {\n\t\t\t// We clear the screen when decreasing terminal width to prevent duplicate overlapping re-renders.\n\t\t\tthis.log.clear();\n\t\t\tthis.lastOutput = '';\n\t\t\tthis.lastOutputToRender = '';\n\t\t}\n\n\t\tthis.calculateLayout();\n\t\tthis.onRender();\n\n\t\tthis.lastTerminalWidth = currentWidth;\n\t};\n\n\tresolveExitPromise: (result?: unknown) => void = () => {};\n\trejectExitPromise: (reason?: Error) => void = () => {};\n\tunsubscribeExit: () => void = () => {};\n\n\thandleAppExit = (errorOrResult?: unknown): void => {\n\t\tif (this.isUnmounted || this.isUnmounting) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (isErrorInput(errorOrResult)) {\n\t\t\tthis.unmount(errorOrResult);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.exitResult = errorOrResult;\n\t\tthis.unmount();\n\t};\n\n\tsetCursorPosition = (position: CursorPosition | undefined): void => {\n\t\tthis.cursorPosition = position;\n\t\tthis.log.setCursorPosition(position);\n\t};\n\n\trestoreLastOutput = (): void => {\n\t\tif (!this.interactive) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Clear() resets log-update's cursor state, so replay the latest cursor intent\n\t\t// before restoring output after external stdout/stderr writes.\n\t\tthis.log.setCursorPosition(this.cursorPosition);\n\t\tthis.log(this.lastOutputToRender || this.lastOutput + '\\n');\n\t};\n\n\tcalculateLayout = () => {\n\t\tconst terminalWidth = getWindowSize(this.options.stdout).columns;\n\n\t\tthis.rootNode.yogaNode!.setWidth(terminalWidth);\n\n\t\tthis.rootNode.yogaNode!.calculateLayout(\n\t\t\tundefined,\n\t\t\tundefined,\n\t\t\tYoga.DIRECTION_LTR,\n\t\t);\n\t};\n\n\tonRender: () => void = () => {\n\t\tthis.hasPendingThrottledRender = false;\n\n\t\tif (this.isUnmounted) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.nextRenderCommit) {\n\t\t\tthis.nextRenderCommit.resolve();\n\t\t\tthis.nextRenderCommit = undefined;\n\t\t}\n\n\t\tconst startTime = performance.now();\n\t\tconst {output, outputHeight, staticOutput} = render(\n\t\t\tthis.rootNode,\n\t\t\tthis.isScreenReaderEnabled,\n\t\t);\n\n\t\tthis.options.onRender?.({renderTime: performance.now() - startTime});\n\n\t\t// If <Static> output isn't empty, it means new children have been added to it\n\t\tconst hasStaticOutput = staticOutput && staticOutput !== '\\n';\n\n\t\tif (this.options.debug) {\n\t\t\tif (hasStaticOutput) {\n\t\t\t\tthis.fullStaticOutput += staticOutput;\n\t\t\t}\n\n\t\t\tthis.lastOutput = output;\n\t\t\tthis.lastOutputToRender = output;\n\t\t\tthis.lastOutputHeight = outputHeight;\n\t\t\tthis.options.stdout.write(this.fullStaticOutput + output);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.interactive) {\n\t\t\tif (hasStaticOutput) {\n\t\t\t\tthis.options.stdout.write(staticOutput);\n\t\t\t}\n\n\t\t\tthis.lastOutput = output;\n\t\t\tthis.lastOutputToRender = output + '\\n';\n\t\t\tthis.lastOutputHeight = outputHeight;\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.isScreenReaderEnabled) {\n\t\t\tconst sync = this.shouldSync();\n\t\t\tif (sync) {\n\t\t\t\tthis.options.stdout.write(bsu);\n\t\t\t}\n\n\t\t\tif (hasStaticOutput) {\n\t\t\t\t// We need to erase the main output before writing new static output\n\t\t\t\tconst erase =\n\t\t\t\t\tthis.lastOutputHeight > 0\n\t\t\t\t\t\t? ansiEscapes.eraseLines(this.lastOutputHeight)\n\t\t\t\t\t\t: '';\n\t\t\t\tthis.options.stdout.write(erase + staticOutput);\n\t\t\t\t// After erasing, the last output is gone, so we should reset its height\n\t\t\t\tthis.lastOutputHeight = 0;\n\t\t\t}\n\n\t\t\tif (output === this.lastOutput && !hasStaticOutput) {\n\t\t\t\tif (sync) {\n\t\t\t\t\tthis.options.stdout.write(esu);\n\t\t\t\t}\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst terminalWidth = getWindowSize(this.options.stdout).columns;\n\n\t\t\tconst wrappedOutput = wrapAnsi(output, terminalWidth, {\n\t\t\t\ttrim: false,\n\t\t\t\thard: true,\n\t\t\t});\n\n\t\t\t// If we haven't erased yet, do it now.\n\t\t\tif (hasStaticOutput) {\n\t\t\t\tthis.options.stdout.write(wrappedOutput);\n\t\t\t} else {\n\t\t\t\tconst erase =\n\t\t\t\t\tthis.lastOutputHeight > 0\n\t\t\t\t\t\t? ansiEscapes.eraseLines(this.lastOutputHeight)\n\t\t\t\t\t\t: '';\n\t\t\t\tthis.options.stdout.write(erase + wrappedOutput);\n\t\t\t}\n\n\t\t\tthis.lastOutput = output;\n\t\t\tthis.lastOutputToRender = wrappedOutput;\n\t\t\tthis.lastOutputHeight =\n\t\t\t\twrappedOutput === '' ? 0 : wrappedOutput.split('\\n').length;\n\n\t\t\tif (sync) {\n\t\t\t\tthis.options.stdout.write(esu);\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (hasStaticOutput) {\n\t\t\tthis.fullStaticOutput += staticOutput;\n\t\t}\n\n\t\tthis.renderInteractiveFrame(\n\t\t\toutput,\n\t\t\toutputHeight,\n\t\t\thasStaticOutput ? staticOutput : '',\n\t\t);\n\t};\n\n\trender(node: ReactNode): void {\n\t\tconst tree = (\n\t\t\t<AccessibilityContext.Provider\n\t\t\t\tvalue={{isScreenReaderEnabled: this.isScreenReaderEnabled}}\n\t\t\t>\n\t\t\t\t<App\n\t\t\t\t\tstdin={this.options.stdin}\n\t\t\t\t\tstdout={this.options.stdout}\n\t\t\t\t\tstderr={this.options.stderr}\n\t\t\t\t\texitOnCtrlC={this.options.exitOnCtrlC}\n\t\t\t\t\tinteractive={this.interactive}\n\t\t\t\t\twriteToStdout={this.writeToStdout}\n\t\t\t\t\twriteToStderr={this.writeToStderr}\n\t\t\t\t\tsetCursorPosition={this.setCursorPosition}\n\t\t\t\t\tonExit={this.handleAppExit}\n\t\t\t\t\tonWaitUntilRenderFlush={this.waitUntilRenderFlush}\n\t\t\t\t>\n\t\t\t\t\t{node}\n\t\t\t\t</App>\n\t\t\t</AccessibilityContext.Provider>\n\t\t);\n\n\t\tif (this.options.concurrent) {\n\t\t\t// Concurrent mode: use updateContainer (async scheduling)\n\t\t\treconciler.updateContainer(tree, this.container, null, noop);\n\t\t} else {\n\t\t\t// Legacy mode: use updateContainerSync + flushSyncWork (sync)\n\t\t\treconciler.updateContainerSync(tree, this.container, null, noop);\n\t\t\treconciler.flushSyncWork();\n\t\t}\n\t}\n\n\twriteToStdout(data: string): void {\n\t\tif (this.isUnmounted) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.options.debug) {\n\t\t\tthis.options.stdout.write(data + this.fullStaticOutput + this.lastOutput);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.interactive) {\n\t\t\tthis.options.stdout.write(data);\n\t\t\treturn;\n\t\t}\n\n\t\tconst sync = this.shouldSync();\n\t\tif (sync) {\n\t\t\tthis.options.stdout.write(bsu);\n\t\t}\n\n\t\tthis.log.clear();\n\t\tthis.options.stdout.write(data);\n\t\tthis.restoreLastOutput();\n\n\t\tif (sync) {\n\t\t\tthis.options.stdout.write(esu);\n\t\t}\n\t}\n\n\twriteToStderr(data: string): void {\n\t\tif (this.isUnmounted) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.options.debug) {\n\t\t\tthis.options.stderr.write(data);\n\t\t\tthis.options.stdout.write(this.fullStaticOutput + this.lastOutput);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.interactive) {\n\t\t\tthis.options.stderr.write(data);\n\t\t\treturn;\n\t\t}\n\n\t\tconst sync = this.shouldSync();\n\t\tif (sync) {\n\t\t\tthis.options.stdout.write(bsu);\n\t\t}\n\n\t\tthis.log.clear();\n\t\tthis.options.stderr.write(data);\n\t\tthis.restoreLastOutput();\n\n\t\tif (sync) {\n\t\t\tthis.options.stdout.write(esu);\n\t\t}\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/no-restricted-types\n\tunmount(error?: Error | number | null): void {\n\t\tif (this.isUnmounted || this.isUnmounting) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.isUnmounting = true;\n\n\t\tif (this.beforeExitHandler) {\n\t\t\tprocess.off('beforeExit', this.beforeExitHandler);\n\t\t\tthis.beforeExitHandler = undefined;\n\t\t}\n\n\t\tconst stdout = this.options.stdout as MaybeWritableStream;\n\t\tconst {canWriteToStdout, hasWritableState} = getWritableStreamState(stdout);\n\n\t\t// Clear any pending throttled render timer on unmount. When stdout is writable,\n\t\t// flush so the final frame is emitted; otherwise cancel to avoid delayed callbacks.\n\t\tsettleThrottle(this.throttledOnRender, canWriteToStdout);\n\n\t\tif (canWriteToStdout) {\n\t\t\t// If throttling is enabled and there is already a pending render, flushing above\n\t\t\t// is sufficient. Also avoid calling onRender() again when static output already\n\t\t\t// exists, as that can duplicate <Static> children output on exit (see issue #397).\n\t\t\tconst shouldRenderFinalFrame =\n\t\t\t\t!this.throttledOnRender ||\n\t\t\t\t(!this.hasPendingThrottledRender && this.fullStaticOutput === '');\n\n\t\t\tif (shouldRenderFinalFrame) {\n\t\t\t\tthis.calculateLayout();\n\t\t\t\tthis.onRender();\n\t\t\t}\n\t\t}\n\n\t\t// Mark as unmounted after the final render but before stdout writes\n\t\t// that could re-enter exit() via synchronous write callbacks.\n\t\tthis.isUnmounted = true;\n\n\t\tthis.unsubscribeExit();\n\n\t\t// Flush any pending throttled log writes if possible, otherwise cancel to\n\t\t// prevent delayed callbacks from writing to a closed stream.\n\t\tsettleThrottle(this.throttledLog, canWriteToStdout);\n\t\tif (typeof this.restoreConsole === 'function') {\n\t\t\t// Once unmount starts, Ink stops trying to manage teardown-time\n\t\t\t// console output. Restoring the native console before React cleanup keeps\n\t\t\t// unmount behavior simple and avoids special-case handling for custom\n\t\t\t// streams, fullscreen frames, and alternate-screen teardown.\n\t\t\tthis.restoreConsole();\n\t\t}\n\n\t\tconst finishUnmount = (): void => {\n\t\t\tif (typeof this.unsubscribeResize === 'function') {\n\t\t\t\tthis.unsubscribeResize();\n\t\t\t}\n\n\t\t\t// Cancel any in-progress auto-detection before checking protocol state\n\t\t\tif (this.cancelKittyDetection) {\n\t\t\t\tthis.cancelKittyDetection();\n\t\t\t}\n\n\t\t\tif (canWriteToStdout) {\n\t\t\t\tif (this.kittyProtocolEnabled) {\n\t\t\t\t\tthis.writeBestEffort(this.options.stdout, '\\u001B[<u');\n\t\t\t\t}\n\n\t\t\t\t// Alternate-screen content is disposable by design. We intentionally\n\t\t\t\t// leave it active until React cleanup finishes, then restore the\n\t\t\t\t// primary buffer without replaying prior frames, hook writes, or\n\t\t\t\t// diagnostics onto it. Trying to preserve teardown output across the\n\t\t\t\t// buffer switch adds fragile lifecycle-specific behavior, so Ink keeps\n\t\t\t\t// alternate-screen teardown intentionally simple and best-effort.\n\t\t\t\tif (this.alternateScreen) {\n\t\t\t\t\tthis.writeBestEffort(\n\t\t\t\t\t\tthis.options.stdout,\n\t\t\t\t\t\tansiEscapes.exitAlternativeScreen,\n\t\t\t\t\t);\n\t\t\t\t\tthis.writeBestEffort(this.options.stdout, showCursorEscape);\n\t\t\t\t\tthis.alternateScreen = false;\n\t\t\t\t}\n\n\t\t\t\tif (!this.interactive) {\n\t\t\t\t\t// Non-interactive environments don't handle erasing ansi escapes well.\n\t\t\t\t\t// In debug mode, each render already writes to stdout, so only a trailing\n\t\t\t\t\t// newline is needed. In non-debug mode, write the last frame now (it was\n\t\t\t\t\t// deferred during rendering).\n\t\t\t\t\tthis.options.stdout.write(\n\t\t\t\t\t\tthis.options.debug ? '\\n' : this.lastOutput + '\\n',\n\t\t\t\t\t);\n\t\t\t\t} else if (!this.options.debug) {\n\t\t\t\t\tthis.log.done();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.kittyProtocolEnabled = false;\n\n\t\t\tinstances.delete(this.options.stdout);\n\n\t\t\t// Ensure all queued writes have been processed before resolving the\n\t\t\t// exit promise. For real writable streams, queue an empty write as a\n\t\t\t// barrier — its callback fires only after all prior writes complete.\n\t\t\t// For non-stream objects (e.g. test spies), resolve on next tick.\n\t\t\t//\n\t\t\t// When called from signal-exit during process shutdown (error is a\n\t\t\t// number or null rather than undefined/Error), resolve synchronously\n\t\t\t// because the event loop is draining and async callbacks won't fire.\n\t\t\tconst {exitResult} = this;\n\n\t\t\tconst resolveOrReject = () => {\n\t\t\t\tif (isErrorInput(error)) {\n\t\t\t\t\tthis.rejectExitPromise(error);\n\t\t\t\t} else {\n\t\t\t\t\tthis.resolveExitPromise(exitResult);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst isProcessExiting = error !== undefined && !isErrorInput(error);\n\n\t\t\tif (isProcessExiting) {\n\t\t\t\tresolveOrReject();\n\t\t\t} else if (canWriteToStdout && hasWritableState) {\n\t\t\t\tthis.options.stdout.write('', resolveOrReject);\n\t\t\t} else {\n\t\t\t\tsetImmediate(resolveOrReject);\n\t\t\t}\n\t\t};\n\n\t\tconst concurrentReconciler = reconciler as {\n\t\t\tflushPassiveEffects?: () => boolean;\n\t\t};\n\n\t\tif (this.options.concurrent) {\n\t\t\treconciler.updateContainerSync(null, this.container, null, noop);\n\t\t\treconciler.flushSyncWork();\n\t\t\tconcurrentReconciler.flushPassiveEffects?.();\n\t\t\tfinishUnmount();\n\t\t} else {\n\t\t\t// Legacy mode: use updateContainerSync + flushSyncWork (sync)\n\t\t\treconciler.updateContainerSync(null, this.container, null, noop);\n\t\t\treconciler.flushSyncWork();\n\t\t\tfinishUnmount();\n\t\t}\n\t}\n\n\tasync waitUntilExit(): Promise<unknown> {\n\t\tif (!this.beforeExitHandler) {\n\t\t\tthis.beforeExitHandler = () => {\n\t\t\t\tthis.unmount();\n\t\t\t};\n\n\t\t\tprocess.once('beforeExit', this.beforeExitHandler);\n\t\t}\n\n\t\treturn this.exitPromise;\n\t}\n\n\tasync waitUntilRenderFlush(): Promise<void> {\n\t\tif (this.isUnmounted || this.isUnmounting) {\n\t\t\tawait this.awaitExit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Yield to the macrotask queue so that React's scheduler has a chance to\n\t\t// fire passive effects and process any work they enqueued.\n\t\tawait yieldImmediate();\n\n\t\tif (this.isUnmounted || this.isUnmounting) {\n\t\t\tawait this.awaitExit();\n\t\t\treturn;\n\t\t}\n\n\t\t// In concurrent mode, React's scheduler may still be mid-render after\n\t\t// the yield. Wait for the next render commit instead of polling.\n\t\tif (this.isConcurrent && this.hasPendingConcurrentWork()) {\n\t\t\tawait Promise.race([this.awaitNextRender(), this.awaitExit()]);\n\n\t\t\tif (this.isUnmounted || this.isUnmounting) {\n\t\t\t\tthis.nextRenderCommit = undefined;\n\t\t\t\tawait this.awaitExit();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\treconciler.flushSyncWork();\n\n\t\tconst stdout = this.options.stdout as MaybeWritableStream;\n\t\tconst {canWriteToStdout, hasWritableState} = getWritableStreamState(stdout);\n\n\t\t// Flush pending throttled render/log timers so their output is included in this wait.\n\t\tsettleThrottle(this.throttledOnRender, canWriteToStdout);\n\t\tsettleThrottle(this.throttledLog, canWriteToStdout);\n\n\t\tif (canWriteToStdout && hasWritableState) {\n\t\t\tawait new Promise<void>(resolve => {\n\t\t\t\tthis.options.stdout.write('', () => {\n\t\t\t\t\tresolve();\n\t\t\t\t});\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tawait yieldImmediate();\n\t}\n\n\tclear(): void {\n\t\tif (this.interactive && !this.options.debug) {\n\t\t\tthis.log.clear();\n\t\t\t// Sync lastOutput so that unmount's final onRender\n\t\t\t// sees it as unchanged and log-update skips it\n\t\t\tthis.log.sync(this.lastOutputToRender || this.lastOutput + '\\n');\n\t\t}\n\t}\n\n\tpatchConsole(): void {\n\t\tif (this.options.debug) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.restoreConsole = patchConsole((stream, data) => {\n\t\t\tif (stream === 'stdout') {\n\t\t\t\tthis.writeToStdout(data);\n\t\t\t}\n\n\t\t\tif (stream === 'stderr') {\n\t\t\t\tconst isReactMessage = data.startsWith('The above error occurred');\n\n\t\t\t\tif (!isReactMessage) {\n\t\t\t\t\tthis.writeToStderr(data);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate setAlternateScreen(enabled: boolean): void {\n\t\tthis.alternateScreen = this.resolveAlternateScreenOption(\n\t\t\tenabled,\n\t\t\tthis.interactive,\n\t\t);\n\n\t\tif (this.alternateScreen) {\n\t\t\tthis.writeBestEffort(\n\t\t\t\tthis.options.stdout,\n\t\t\t\tansiEscapes.enterAlternativeScreen,\n\t\t\t);\n\t\t\tthis.writeBestEffort(this.options.stdout, hideCursorEscape);\n\t\t}\n\t}\n\n\tprivate resolveInteractiveOption(interactive: boolean | undefined): boolean {\n\t\treturn interactive ?? (!isInCi && Boolean(this.options.stdout.isTTY));\n\t}\n\n\tprivate resolveAlternateScreenOption(\n\t\talternateScreen: boolean | undefined,\n\t\tinteractive: boolean,\n\t): boolean {\n\t\treturn (\n\t\t\tBoolean(alternateScreen) &&\n\t\t\tinteractive &&\n\t\t\tBoolean(this.options.stdout.isTTY)\n\t\t);\n\t}\n\n\tprivate shouldSync(): boolean {\n\t\treturn shouldSynchronize(this.options.stdout, this.interactive);\n\t}\n\n\t// Best-effort write: streams may already be destroyed during shutdown.\n\tprivate writeBestEffort(stream: NodeJS.WriteStream, data: string): void {\n\t\ttry {\n\t\t\tstream.write(data);\n\t\t} catch {}\n\t}\n\n\t// Waits for the exit promise to settle, suppressing any rejection.\n\t// Errors are surfaced via waitUntilExit() instead.\n\tprivate async awaitExit(): Promise<void> {\n\t\ttry {\n\t\t\tawait this.exitPromise;\n\t\t} catch {}\n\t}\n\n\tprivate hasPendingConcurrentWork(): boolean {\n\t\tconst concurrentContainer = this.container as {\n\t\t\tpendingLanes?: number;\n\t\t\tcallbackNode?: unknown;\n\t\t};\n\t\treturn (\n\t\t\t(concurrentContainer.pendingLanes ?? 0) !== 0 &&\n\t\t\tconcurrentContainer.callbackNode !== undefined &&\n\t\t\tconcurrentContainer.callbackNode !== null\n\t\t);\n\t}\n\n\tprivate async awaitNextRender(): Promise<void> {\n\t\tif (!this.nextRenderCommit) {\n\t\t\tlet resolveRender!: () => void;\n\t\t\tconst promise = new Promise<void>(resolve => {\n\t\t\t\tresolveRender = resolve;\n\t\t\t});\n\t\t\tthis.nextRenderCommit = {promise, resolve: resolveRender};\n\t\t}\n\n\t\treturn this.nextRenderCommit.promise;\n\t}\n\n\tprivate renderInteractiveFrame(\n\t\toutput: string,\n\t\toutputHeight: number,\n\t\tstaticOutput: string,\n\t): void {\n\t\tconst hasStaticOutput = staticOutput !== '';\n\t\tconst isTty = this.options.stdout.isTTY;\n\n\t\t// Detect fullscreen: output fills or exceeds terminal height.\n\t\t// Only apply when writing to a real TTY — piped output always gets trailing newlines.\n\t\tconst viewportRows = isTty ? getWindowSize(this.options.stdout).rows : 24;\n\t\tconst isFullscreen = isTty && outputHeight >= viewportRows;\n\t\tconst outputToRender = isFullscreen ? output : output + '\\n';\n\n\t\tconst shouldClearTerminal = shouldClearTerminalForFrame({\n\t\t\tisTty,\n\t\t\tviewportRows,\n\t\t\tpreviousOutputHeight: this.lastOutputHeight,\n\t\t\tnextOutputHeight: outputHeight,\n\t\t\tisUnmounting: this.isUnmounting,\n\t\t});\n\n\t\tif (shouldClearTerminal) {\n\t\t\tconst sync = this.shouldSync();\n\t\t\tif (sync) {\n\t\t\t\tthis.options.stdout.write(bsu);\n\t\t\t}\n\n\t\t\tthis.options.stdout.write(\n\t\t\t\tansiEscapes.clearTerminal + this.fullStaticOutput + output,\n\t\t\t);\n\t\t\tthis.lastOutput = output;\n\t\t\tthis.lastOutputToRender = outputToRender;\n\t\t\tthis.lastOutputHeight = outputHeight;\n\t\t\tthis.log.sync(outputToRender);\n\n\t\t\tif (sync) {\n\t\t\t\tthis.options.stdout.write(esu);\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// To ensure static output is cleanly rendered before main output, clear main output first\n\t\tif (hasStaticOutput) {\n\t\t\tconst sync = this.shouldSync();\n\t\t\tif (sync) {\n\t\t\t\tthis.options.stdout.write(bsu);\n\t\t\t}\n\n\t\t\tthis.log.clear();\n\t\t\tthis.options.stdout.write(staticOutput);\n\t\t\tthis.log(outputToRender);\n\n\t\t\tif (sync) {\n\t\t\t\tthis.options.stdout.write(esu);\n\t\t\t}\n\t\t} else if (output !== this.lastOutput || this.log.isCursorDirty()) {\n\t\t\t// ThrottledLog manages its own bsu/esu at actual write time\n\t\t\tthis.throttledLog(outputToRender);\n\t\t}\n\n\t\tthis.lastOutput = output;\n\t\tthis.lastOutputToRender = outputToRender;\n\t\tthis.lastOutputHeight = outputHeight;\n\t}\n\n\tprivate initKittyKeyboard(): void {\n\t\t// Protocol is opt-in: if kittyKeyboard is not specified, do nothing\n\t\tif (!this.options.kittyKeyboard) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst opts = this.options.kittyKeyboard;\n\t\tconst mode = opts.mode ?? 'auto';\n\n\t\tif (mode === 'disabled') {\n\t\t\treturn;\n\t\t}\n\n\t\tconst flags: KittyFlagName[] = opts.flags ?? ['disambiguateEscapeCodes'];\n\n\t\t// 'enabled' force-enables the protocol as long as both streams are TTYs,\n\t\t// regardless of the interactive setting (e.g. even in CI).\n\t\tif (mode === 'enabled') {\n\t\t\tif (this.options.stdin.isTTY && this.options.stdout.isTTY) {\n\t\t\t\tthis.enableKittyProtocol(flags);\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// Auto mode: require interactive + TTY\n\t\tif (\n\t\t\t!this.interactive ||\n\t\t\t!this.options.stdin.isTTY ||\n\t\t\t!this.options.stdout.isTTY\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Auto mode: query the terminal for kitty keyboard protocol support.\n\t\t// The CSI ? u query is safe to send to any terminal — unsupporting\n\t\t// terminals simply won't respond, and the 200ms timeout handles that.\n\t\t// This avoids maintaining a hardcoded whitelist of terminal names.\n\t\tthis.confirmKittySupport(flags);\n\t}\n\n\tprivate confirmKittySupport(flags: KittyFlagName[]): void {\n\t\tconst {stdin, stdout} = this.options;\n\n\t\tlet responseBuffer: number[] = [];\n\n\t\tconst cleanup = (): void => {\n\t\t\tthis.cancelKittyDetection = undefined;\n\t\t\tclearTimeout(timer);\n\t\t\tstdin.removeListener('data', onData);\n\n\t\t\t// Re-emit any buffered data that wasn't the protocol response,\n\t\t\t// so it isn't lost from Ink's normal input pipeline.\n\t\t\t// Clear responseBuffer afterwards to make cleanup idempotent.\n\t\t\tconst remaining =\n\t\t\t\tstripKittyQueryResponsesAndTrailingPartial(responseBuffer);\n\t\t\tresponseBuffer = [];\n\t\t\tif (remaining.length > 0) {\n\t\t\t\tstdin.unshift(Buffer.from(remaining));\n\t\t\t}\n\t\t};\n\n\t\tconst onData = (data: Uint8Array | string): void => {\n\t\t\tconst chunk = typeof data === 'string' ? Buffer.from(data) : data;\n\t\t\tfor (const byte of chunk) {\n\t\t\t\tresponseBuffer.push(byte);\n\t\t\t}\n\n\t\t\tif (hasCompleteKittyQueryResponse(responseBuffer)) {\n\t\t\t\tcleanup();\n\t\t\t\tif (!this.isUnmounted) {\n\t\t\t\t\tthis.enableKittyProtocol(flags);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\t// Attach listener before writing the query so that synchronous\n\t\t// or immediate responses are not missed.\n\t\tstdin.on('data', onData);\n\t\tconst timer = setTimeout(cleanup, 200);\n\t\tthis.cancelKittyDetection = cleanup;\n\n\t\tstdout.write('\\u001B[?u');\n\t}\n\n\tprivate enableKittyProtocol(flags: KittyFlagName[]): void {\n\t\tthis.options.stdout.write(`\\u001B[>${resolveFlags(flags)}u`);\n\t\tthis.kittyProtocolEnabled = true;\n\t}\n}\n"
  },
  {
    "path": "src/input-parser.ts",
    "content": "const escape = '\\u001B';\nconst pasteStart = '\\u001B[200~';\nconst pasteEnd = '\\u001B[201~';\n\nexport type InputEvent = string | {readonly paste: string};\n\ntype ParsedInput = {\n\treadonly events: InputEvent[];\n\treadonly pending: string;\n};\n\ntype ParsedSequence =\n\t| {\n\t\t\treadonly sequence: string;\n\t\t\treadonly nextIndex: number;\n\t  }\n\t| 'pending'\n\t| undefined;\n\nconst isCsiParameterByte = (byte: number): boolean => {\n\treturn byte >= 0x30 && byte <= 0x3f;\n};\n\nconst isCsiIntermediateByte = (byte: number): boolean => {\n\treturn byte >= 0x20 && byte <= 0x2f;\n};\n\nconst isCsiFinalByte = (byte: number): boolean => {\n\treturn byte >= 0x40 && byte <= 0x7e;\n};\n\nconst parseCsiSequence = (\n\tinput: string,\n\tstartIndex: number,\n\tprefixLength: number,\n): ParsedSequence => {\n\tconst csiPayloadStart = startIndex + prefixLength + 1;\n\tlet index = csiPayloadStart;\n\tfor (; index < input.length; index++) {\n\t\tconst byte = input.codePointAt(index);\n\t\tif (byte === undefined) {\n\t\t\treturn 'pending';\n\t\t}\n\n\t\tif (isCsiParameterByte(byte) || isCsiIntermediateByte(byte)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Preserve legacy terminal function-key sequences like ESC[[A and ESC[[5~.\n\t\tif (byte === 0x5b && index === csiPayloadStart) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (isCsiFinalByte(byte)) {\n\t\t\treturn {\n\t\t\t\tsequence: input.slice(startIndex, index + 1),\n\t\t\t\tnextIndex: index + 1,\n\t\t\t};\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\treturn 'pending';\n};\n\nconst parseSs3Sequence = (\n\tinput: string,\n\tstartIndex: number,\n\tprefixLength: number,\n): ParsedSequence => {\n\tconst nextIndex = startIndex + prefixLength + 2;\n\tif (nextIndex > input.length) {\n\t\treturn 'pending';\n\t}\n\n\tconst finalByte = input.codePointAt(nextIndex - 1);\n\tif (finalByte === undefined || !isCsiFinalByte(finalByte)) {\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\tsequence: input.slice(startIndex, nextIndex),\n\t\tnextIndex,\n\t};\n};\n\nconst parseControlSequence = (\n\tinput: string,\n\tstartIndex: number,\n\tprefixLength: number,\n): ParsedSequence => {\n\tconst sequenceType = input[startIndex + prefixLength];\n\tif (sequenceType === undefined) {\n\t\treturn 'pending';\n\t}\n\n\tif (sequenceType === '[') {\n\t\treturn parseCsiSequence(input, startIndex, prefixLength);\n\t}\n\n\tif (sequenceType === 'O') {\n\t\treturn parseSs3Sequence(input, startIndex, prefixLength);\n\t}\n\n\treturn undefined;\n};\n\nconst parseEscapedCodePoint = (\n\tinput: string,\n\tescapeIndex: number,\n): {\n\treadonly sequence: string;\n\treadonly nextIndex: number;\n} => {\n\tconst nextCodePoint = input.codePointAt(escapeIndex + 1);\n\tconst nextCodePointLength =\n\t\tnextCodePoint !== undefined && nextCodePoint > 0xff_ff ? 2 : 1;\n\tconst nextIndex = escapeIndex + 1 + nextCodePointLength;\n\n\treturn {\n\t\tsequence: input.slice(escapeIndex, nextIndex),\n\t\tnextIndex,\n\t};\n};\n\ntype ParsedEscapeSequence =\n\t| {\n\t\t\treadonly sequence: string;\n\t\t\treadonly nextIndex: number;\n\t  }\n\t| 'pending';\n\nconst parseEscapeSequence = (\n\tinput: string,\n\tescapeIndex: number,\n): ParsedEscapeSequence => {\n\tif (escapeIndex === input.length - 1) {\n\t\treturn 'pending';\n\t}\n\n\tconst next = input[escapeIndex + 1]!;\n\tif (next === escape) {\n\t\tif (escapeIndex + 2 >= input.length) {\n\t\t\treturn 'pending';\n\t\t}\n\n\t\tconst doubleEscapeSequence = parseControlSequence(input, escapeIndex, 2);\n\t\tif (doubleEscapeSequence === 'pending') {\n\t\t\treturn 'pending';\n\t\t}\n\n\t\tif (doubleEscapeSequence) {\n\t\t\treturn doubleEscapeSequence;\n\t\t}\n\n\t\treturn {\n\t\t\tsequence: input.slice(escapeIndex, escapeIndex + 2),\n\t\t\tnextIndex: escapeIndex + 2,\n\t\t};\n\t}\n\n\tconst controlSequence = parseControlSequence(input, escapeIndex, 1);\n\tif (controlSequence === 'pending') {\n\t\treturn 'pending';\n\t}\n\n\tif (controlSequence) {\n\t\treturn controlSequence;\n\t}\n\n\treturn parseEscapedCodePoint(input, escapeIndex);\n};\n\n/**\nSplit a chunk of non-escape text so that delete (0x7F) and backspace (0x08) characters become individual events. When a user holds the delete or backspace key, the terminal sends repeated bytes in a single stdin chunk. Without splitting, `parseKeypress` receives the multi-byte string and fails to recognize it as a key event, corrupting the input state.\n\nOther control characters like `\\r` and `\\t` are NOT split because they can legitimately appear inside pasted text.\n*/\nconst splitDeleteAndBackspace = (text: string, events: InputEvent[]): void => {\n\tlet textSegmentStart = 0;\n\n\tfor (let index = 0; index < text.length; index++) {\n\t\tconst character = text[index]!;\n\t\tif (character === '\\u007F' || character === '\\u0008') {\n\t\t\tif (index > textSegmentStart) {\n\t\t\t\tevents.push(text.slice(textSegmentStart, index));\n\t\t\t}\n\n\t\t\tevents.push(character);\n\t\t\ttextSegmentStart = index + 1;\n\t\t}\n\t}\n\n\tif (textSegmentStart < text.length) {\n\t\tevents.push(text.slice(textSegmentStart));\n\t}\n};\n\nconst parseKeypresses = (input: string): ParsedInput => {\n\tconst events: InputEvent[] = [];\n\tlet index = 0;\n\tconst pendingFrom = (pendingStartIndex: number): ParsedInput => ({\n\t\tevents,\n\t\tpending: input.slice(pendingStartIndex),\n\t});\n\n\twhile (index < input.length) {\n\t\tconst escapeIndex = input.indexOf(escape, index);\n\t\tif (escapeIndex === -1) {\n\t\t\tsplitDeleteAndBackspace(input.slice(index), events);\n\t\t\treturn {\n\t\t\t\tevents,\n\t\t\t\tpending: '',\n\t\t\t};\n\t\t}\n\n\t\tif (escapeIndex > index) {\n\t\t\tsplitDeleteAndBackspace(input.slice(index, escapeIndex), events);\n\t\t}\n\n\t\tconst parsedEscapeSequence = parseEscapeSequence(input, escapeIndex);\n\t\tif (parsedEscapeSequence === 'pending') {\n\t\t\treturn pendingFrom(escapeIndex);\n\t\t}\n\n\t\tif (parsedEscapeSequence.sequence === pasteStart) {\n\t\t\tconst afterStart = parsedEscapeSequence.nextIndex;\n\t\t\tconst endIndex = input.indexOf(pasteEnd, afterStart);\n\t\t\tif (endIndex === -1) {\n\t\t\t\treturn pendingFrom(escapeIndex);\n\t\t\t}\n\n\t\t\tevents.push({paste: input.slice(afterStart, endIndex)});\n\t\t\tindex = endIndex + pasteEnd.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tevents.push(parsedEscapeSequence.sequence);\n\t\tindex = parsedEscapeSequence.nextIndex;\n\t}\n\n\treturn {\n\t\tevents,\n\t\tpending: '',\n\t};\n};\n\nexport type InputParser = {\n\tpush: (chunk: string) => InputEvent[];\n\thasPendingEscape: () => boolean;\n\tflushPendingEscape: () => string | undefined;\n\treset: () => void;\n};\n\nexport const createInputParser = (): InputParser => {\n\tlet pending = '';\n\n\treturn {\n\t\tpush(chunk) {\n\t\t\tconst parsedInput = parseKeypresses(pending + chunk);\n\t\t\tpending = parsedInput.pending;\n\t\t\treturn parsedInput.events;\n\t\t},\n\t\thasPendingEscape() {\n\t\t\t// Don't trigger the escape flush timer while assembling a paste start\n\t\t\t// marker (`\\u001B[200` and then `~`) or while waiting for paste end.\n\t\t\treturn (\n\t\t\t\tpending.startsWith(escape) &&\n\t\t\t\t!pending.startsWith(pasteStart) &&\n\t\t\t\tpending !== '\\u001B[200'\n\t\t\t);\n\t\t},\n\t\tflushPendingEscape() {\n\t\t\tif (!pending.startsWith(escape)) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tconst pendingEscape = pending;\n\t\t\tpending = '';\n\t\t\treturn pendingEscape;\n\t\t},\n\t\treset() {\n\t\t\tpending = '';\n\t\t},\n\t};\n};\n"
  },
  {
    "path": "src/instances.ts",
    "content": "// Store all instances of Ink (instance.js) to ensure that consecutive render() calls\n// use the same instance of Ink and don't create a new one\n//\n// This map has to be stored in a separate file, because render.js creates instances,\n// but instance.js should delete itself from the map on unmount\n\nimport type Ink from './ink.js';\n\nconst instances = new WeakMap<NodeJS.WriteStream, Ink>();\nexport default instances;\n"
  },
  {
    "path": "src/kitty-keyboard.ts",
    "content": "// Kitty keyboard protocol flags.\n// @see https://sw.kovidgoyal.net/kitty/keyboard-protocol/\nexport const kittyFlags = {\n\tdisambiguateEscapeCodes: 1,\n\treportEventTypes: 2,\n\treportAlternateKeys: 4,\n\treportAllKeysAsEscapeCodes: 8,\n\treportAssociatedText: 16,\n} as const;\n\n// Valid flag names for the kitty keyboard protocol.\nexport type KittyFlagName = keyof typeof kittyFlags;\n\n// Converts an array of flag names to the corresponding bitmask value.\nexport function resolveFlags(flags: KittyFlagName[]): number {\n\tlet result = 0;\n\tfor (const flag of flags) {\n\t\t// eslint-disable-next-line no-bitwise\n\t\tresult |= kittyFlags[flag];\n\t}\n\n\treturn result;\n}\n\n// Kitty keyboard modifier bits.\n// These are used in the modifier parameter of CSI u sequences.\n// Note: The actual modifier value is (modifiers - 1) as per the protocol.\nexport const kittyModifiers = {\n\tshift: 1,\n\talt: 2,\n\tctrl: 4,\n\tsuper: 8,\n\thyper: 16,\n\tmeta: 32,\n\tcapsLock: 64,\n\tnumLock: 128,\n} as const;\n\n// Options for configuring kitty keyboard protocol.\nexport type KittyKeyboardOptions = {\n\t// Mode for kitty keyboard protocol support.\n\t// - 'auto': Attempt to detect terminal support (default)\n\t// - 'enabled': Force enable the protocol\n\t// - 'disabled': Never enable the protocol\n\tmode?: 'auto' | 'enabled' | 'disabled';\n\n\t// Protocol flags to request from the terminal.\n\t// Pass an array of flag name strings.\n\t//\n\t// Available flags:\n\t// - 'disambiguateEscapeCodes' - Disambiguate escape codes (default)\n\t// - 'reportEventTypes' - Report key press, repeat, and release events\n\t// - 'reportAlternateKeys' - Report alternate key encodings\n\t// - 'reportAllKeysAsEscapeCodes' - Report all keys as escape codes\n\t// - 'reportAssociatedText' - Report associated text with key events\n\tflags?: KittyFlagName[];\n};\n"
  },
  {
    "path": "src/log-update.ts",
    "content": "import {type Writable} from 'node:stream';\nimport ansiEscapes from 'ansi-escapes';\nimport cliCursor from 'cli-cursor';\nimport {\n\ttype CursorPosition,\n\tcursorPositionChanged,\n\tbuildCursorSuffix,\n\tbuildCursorOnlySequence,\n\tbuildReturnToBottomPrefix,\n\thideCursorEscape,\n} from './cursor-helpers.js';\n\nexport type {CursorPosition} from './cursor-helpers.js';\n\nexport type LogUpdate = {\n\tclear: () => void;\n\tdone: () => void;\n\treset: () => void;\n\tsync: (str: string) => void;\n\tsetCursorPosition: (position: CursorPosition | undefined) => void;\n\tisCursorDirty: () => boolean;\n\twillRender: (str: string) => boolean;\n\t(str: string): boolean;\n};\n\n// Count visible lines in a string, ignoring the trailing empty element\n// that `split('\\n')` produces when the string ends with '\\n'.\nconst visibleLineCount = (lines: string[], str: string): number =>\n\tstr.endsWith('\\n') ? lines.length - 1 : lines.length;\n\nconst createStandard = (\n\tstream: Writable,\n\t{showCursor = false} = {},\n): LogUpdate => {\n\tlet previousLineCount = 0;\n\tlet previousOutput = '';\n\tlet hasHiddenCursor = false;\n\tlet cursorPosition: CursorPosition | undefined;\n\tlet cursorDirty = false;\n\tlet previousCursorPosition: CursorPosition | undefined;\n\tlet cursorWasShown = false;\n\n\tconst getActiveCursor = () => (cursorDirty ? cursorPosition : undefined);\n\tconst hasChanges = (\n\t\tstr: string,\n\t\tactiveCursor: CursorPosition | undefined,\n\t): boolean => {\n\t\tconst cursorChanged = cursorPositionChanged(\n\t\t\tactiveCursor,\n\t\t\tpreviousCursorPosition,\n\t\t);\n\t\treturn str !== previousOutput || cursorChanged;\n\t};\n\n\tconst render = (str: string) => {\n\t\tif (!showCursor && !hasHiddenCursor) {\n\t\t\tcliCursor.hide(stream);\n\t\t\thasHiddenCursor = true;\n\t\t}\n\n\t\t// Only use cursor if setCursorPosition was called since last render.\n\t\t// This ensures stale positions don't persist after component unmount.\n\t\tconst activeCursor = getActiveCursor();\n\t\tcursorDirty = false;\n\t\tconst cursorChanged = cursorPositionChanged(\n\t\t\tactiveCursor,\n\t\t\tpreviousCursorPosition,\n\t\t);\n\n\t\tif (!hasChanges(str, activeCursor)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst lines = str.split('\\n');\n\t\tconst visibleCount = visibleLineCount(lines, str);\n\t\tconst cursorSuffix = buildCursorSuffix(visibleCount, activeCursor);\n\n\t\tif (str === previousOutput && cursorChanged) {\n\t\t\tstream.write(\n\t\t\t\tbuildCursorOnlySequence({\n\t\t\t\t\tcursorWasShown,\n\t\t\t\t\tpreviousLineCount,\n\t\t\t\t\tpreviousCursorPosition,\n\t\t\t\t\tvisibleLineCount: visibleCount,\n\t\t\t\t\tcursorPosition: activeCursor,\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\tpreviousOutput = str;\n\t\t\tconst returnPrefix = buildReturnToBottomPrefix(\n\t\t\t\tcursorWasShown,\n\t\t\t\tpreviousLineCount,\n\t\t\t\tpreviousCursorPosition,\n\t\t\t);\n\t\t\tstream.write(\n\t\t\t\treturnPrefix +\n\t\t\t\t\tansiEscapes.eraseLines(previousLineCount) +\n\t\t\t\t\tstr +\n\t\t\t\t\tcursorSuffix,\n\t\t\t);\n\t\t\tpreviousLineCount = lines.length;\n\t\t}\n\n\t\tpreviousCursorPosition = activeCursor ? {...activeCursor} : undefined;\n\t\tcursorWasShown = activeCursor !== undefined;\n\t\treturn true;\n\t};\n\n\trender.clear = () => {\n\t\tconst prefix = buildReturnToBottomPrefix(\n\t\t\tcursorWasShown,\n\t\t\tpreviousLineCount,\n\t\t\tpreviousCursorPosition,\n\t\t);\n\t\tstream.write(prefix + ansiEscapes.eraseLines(previousLineCount));\n\t\tpreviousOutput = '';\n\t\tpreviousLineCount = 0;\n\t\tpreviousCursorPosition = undefined;\n\t\tcursorWasShown = false;\n\t};\n\n\trender.done = () => {\n\t\tpreviousOutput = '';\n\t\tpreviousLineCount = 0;\n\t\tpreviousCursorPosition = undefined;\n\t\tcursorWasShown = false;\n\n\t\tif (!showCursor) {\n\t\t\tcliCursor.show(stream);\n\t\t\thasHiddenCursor = false;\n\t\t}\n\t};\n\n\trender.reset = () => {\n\t\tpreviousOutput = '';\n\t\tpreviousLineCount = 0;\n\t\tpreviousCursorPosition = undefined;\n\t\tcursorWasShown = false;\n\t};\n\n\trender.sync = (str: string) => {\n\t\tconst activeCursor = cursorDirty ? cursorPosition : undefined;\n\t\tcursorDirty = false;\n\n\t\tconst lines = str.split('\\n');\n\t\tpreviousOutput = str;\n\t\tpreviousLineCount = lines.length;\n\n\t\tif (!activeCursor && cursorWasShown) {\n\t\t\tstream.write(hideCursorEscape);\n\t\t}\n\n\t\tif (activeCursor) {\n\t\t\tstream.write(\n\t\t\t\tbuildCursorSuffix(visibleLineCount(lines, str), activeCursor),\n\t\t\t);\n\t\t}\n\n\t\tpreviousCursorPosition = activeCursor ? {...activeCursor} : undefined;\n\t\tcursorWasShown = activeCursor !== undefined;\n\t};\n\n\trender.setCursorPosition = (position: CursorPosition | undefined) => {\n\t\tcursorPosition = position;\n\t\tcursorDirty = true;\n\t};\n\n\trender.isCursorDirty = () => cursorDirty;\n\trender.willRender = (str: string) => hasChanges(str, getActiveCursor());\n\n\treturn render;\n};\n\nconst createIncremental = (\n\tstream: Writable,\n\t{showCursor = false} = {},\n): LogUpdate => {\n\tlet previousLines: string[] = [];\n\tlet previousOutput = '';\n\tlet hasHiddenCursor = false;\n\tlet cursorPosition: CursorPosition | undefined;\n\tlet cursorDirty = false;\n\tlet previousCursorPosition: CursorPosition | undefined;\n\tlet cursorWasShown = false;\n\n\tconst getActiveCursor = () => (cursorDirty ? cursorPosition : undefined);\n\tconst hasChanges = (\n\t\tstr: string,\n\t\tactiveCursor: CursorPosition | undefined,\n\t): boolean => {\n\t\tconst cursorChanged = cursorPositionChanged(\n\t\t\tactiveCursor,\n\t\t\tpreviousCursorPosition,\n\t\t);\n\t\treturn str !== previousOutput || cursorChanged;\n\t};\n\n\tconst render = (str: string) => {\n\t\tif (!showCursor && !hasHiddenCursor) {\n\t\t\tcliCursor.hide(stream);\n\t\t\thasHiddenCursor = true;\n\t\t}\n\n\t\t// Only use cursor if setCursorPosition was called since last render.\n\t\t// This ensures stale positions don't persist after component unmount.\n\t\tconst activeCursor = getActiveCursor();\n\t\tcursorDirty = false;\n\t\tconst cursorChanged = cursorPositionChanged(\n\t\t\tactiveCursor,\n\t\t\tpreviousCursorPosition,\n\t\t);\n\n\t\tif (!hasChanges(str, activeCursor)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst nextLines = str.split('\\n');\n\t\tconst visibleCount = visibleLineCount(nextLines, str);\n\t\tconst previousVisible = visibleLineCount(previousLines, previousOutput);\n\n\t\tif (str === previousOutput && cursorChanged) {\n\t\t\tstream.write(\n\t\t\t\tbuildCursorOnlySequence({\n\t\t\t\t\tcursorWasShown,\n\t\t\t\t\tpreviousLineCount: previousLines.length,\n\t\t\t\t\tpreviousCursorPosition,\n\t\t\t\t\tvisibleLineCount: visibleCount,\n\t\t\t\t\tcursorPosition: activeCursor,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tpreviousCursorPosition = activeCursor ? {...activeCursor} : undefined;\n\t\t\tcursorWasShown = activeCursor !== undefined;\n\t\t\treturn true;\n\t\t}\n\n\t\tconst returnPrefix = buildReturnToBottomPrefix(\n\t\t\tcursorWasShown,\n\t\t\tpreviousLines.length,\n\t\t\tpreviousCursorPosition,\n\t\t);\n\n\t\tif (str === '\\n' || previousOutput.length === 0) {\n\t\t\tconst cursorSuffix = buildCursorSuffix(visibleCount, activeCursor);\n\t\t\tstream.write(\n\t\t\t\treturnPrefix +\n\t\t\t\t\tansiEscapes.eraseLines(previousLines.length) +\n\t\t\t\t\tstr +\n\t\t\t\t\tcursorSuffix,\n\t\t\t);\n\t\t\tcursorWasShown = activeCursor !== undefined;\n\t\t\tpreviousCursorPosition = activeCursor ? {...activeCursor} : undefined;\n\t\t\tpreviousOutput = str;\n\t\t\tpreviousLines = nextLines;\n\t\t\treturn true;\n\t\t}\n\n\t\tconst hasTrailingNewline = str.endsWith('\\n');\n\n\t\t// We aggregate all chunks for incremental rendering into a buffer, and then write them to stdout at the end.\n\t\tconst buffer: string[] = [];\n\n\t\tbuffer.push(returnPrefix);\n\n\t\t// Clear extra lines if the current content's line count is lower than the previous.\n\t\tif (visibleCount < previousVisible) {\n\t\t\tconst previousHadTrailingNewline = previousOutput.endsWith('\\n');\n\t\t\tconst extraSlot = previousHadTrailingNewline ? 1 : 0;\n\t\t\tbuffer.push(\n\t\t\t\tansiEscapes.eraseLines(previousVisible - visibleCount + extraSlot),\n\t\t\t\tansiEscapes.cursorUp(visibleCount),\n\t\t\t);\n\t\t} else {\n\t\t\tbuffer.push(ansiEscapes.cursorUp(previousVisible - 1));\n\t\t}\n\n\t\tfor (let i = 0; i < visibleCount; i++) {\n\t\t\tconst isLastLine = i === visibleCount - 1;\n\n\t\t\t// We do not write lines if the contents are the same. This prevents flickering during renders.\n\t\t\tif (nextLines[i] === previousLines[i]) {\n\t\t\t\t// Don't move past the last line when there's no trailing newline,\n\t\t\t\t// otherwise the cursor overshoots the rendered block.\n\t\t\t\tif (!isLastLine || hasTrailingNewline) {\n\t\t\t\t\tbuffer.push(ansiEscapes.cursorNextLine);\n\t\t\t\t}\n\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tbuffer.push(\n\t\t\t\tansiEscapes.cursorTo(0) +\n\t\t\t\t\tnextLines[i] +\n\t\t\t\t\tansiEscapes.eraseEndLine +\n\t\t\t\t\t// Don't append newline after the last line when the input\n\t\t\t\t\t// has no trailing newline (fullscreen mode).\n\t\t\t\t\t(isLastLine && !hasTrailingNewline ? '' : '\\n'),\n\t\t\t);\n\t\t}\n\n\t\tconst cursorSuffix = buildCursorSuffix(visibleCount, activeCursor);\n\t\tbuffer.push(cursorSuffix);\n\n\t\tstream.write(buffer.join(''));\n\n\t\tcursorWasShown = activeCursor !== undefined;\n\t\tpreviousCursorPosition = activeCursor ? {...activeCursor} : undefined;\n\t\tpreviousOutput = str;\n\t\tpreviousLines = nextLines;\n\t\treturn true;\n\t};\n\n\trender.clear = () => {\n\t\tconst prefix = buildReturnToBottomPrefix(\n\t\t\tcursorWasShown,\n\t\t\tpreviousLines.length,\n\t\t\tpreviousCursorPosition,\n\t\t);\n\t\tstream.write(prefix + ansiEscapes.eraseLines(previousLines.length));\n\t\tpreviousOutput = '';\n\t\tpreviousLines = [];\n\t\tpreviousCursorPosition = undefined;\n\t\tcursorWasShown = false;\n\t};\n\n\trender.done = () => {\n\t\tpreviousOutput = '';\n\t\tpreviousLines = [];\n\t\tpreviousCursorPosition = undefined;\n\t\tcursorWasShown = false;\n\n\t\tif (!showCursor) {\n\t\t\tcliCursor.show(stream);\n\t\t\thasHiddenCursor = false;\n\t\t}\n\t};\n\n\trender.reset = () => {\n\t\tpreviousOutput = '';\n\t\tpreviousLines = [];\n\t\tpreviousCursorPosition = undefined;\n\t\tcursorWasShown = false;\n\t};\n\n\trender.sync = (str: string) => {\n\t\tconst activeCursor = cursorDirty ? cursorPosition : undefined;\n\t\tcursorDirty = false;\n\n\t\tconst lines = str.split('\\n');\n\t\tpreviousOutput = str;\n\t\tpreviousLines = lines;\n\n\t\tif (!activeCursor && cursorWasShown) {\n\t\t\tstream.write(hideCursorEscape);\n\t\t}\n\n\t\tif (activeCursor) {\n\t\t\tstream.write(\n\t\t\t\tbuildCursorSuffix(visibleLineCount(lines, str), activeCursor),\n\t\t\t);\n\t\t}\n\n\t\tpreviousCursorPosition = activeCursor ? {...activeCursor} : undefined;\n\t\tcursorWasShown = activeCursor !== undefined;\n\t};\n\n\trender.setCursorPosition = (position: CursorPosition | undefined) => {\n\t\tcursorPosition = position;\n\t\tcursorDirty = true;\n\t};\n\n\trender.isCursorDirty = () => cursorDirty;\n\trender.willRender = (str: string) => hasChanges(str, getActiveCursor());\n\n\treturn render;\n};\n\nconst create = (\n\tstream: Writable,\n\t{showCursor = false, incremental = false} = {},\n): LogUpdate => {\n\tif (incremental) {\n\t\treturn createIncremental(stream, {showCursor});\n\t}\n\n\treturn createStandard(stream, {showCursor});\n};\n\nconst logUpdate = {create};\nexport default logUpdate;\n"
  },
  {
    "path": "src/measure-element.ts",
    "content": "import {type DOMElement} from './dom.js';\n\ntype Output = {\n\t/**\n\tElement width.\n\t*/\n\twidth: number;\n\n\t/**\n\tElement height.\n\t*/\n\theight: number;\n};\n\n/**\nMeasure the dimensions of a particular `<Box>` element.\nReturns an object with `width` and `height` properties.\nThis 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.\n\nNote: `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.\n*/\nconst measureElement = (node: DOMElement): Output => ({\n\twidth: node.yogaNode?.getComputedWidth() ?? 0,\n\theight: node.yogaNode?.getComputedHeight() ?? 0,\n});\n\nexport default measureElement;\n"
  },
  {
    "path": "src/measure-text.ts",
    "content": "import widestLine from 'widest-line';\n\nconst cache = new Map<string, Output>();\n\ntype Output = {\n\twidth: number;\n\theight: number;\n};\n\nconst measureText = (text: string): Output => {\n\tif (text.length === 0) {\n\t\treturn {\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t};\n\t}\n\n\tconst cachedDimensions = cache.get(text);\n\n\tif (cachedDimensions) {\n\t\treturn cachedDimensions;\n\t}\n\n\tconst width = widestLine(text);\n\tconst height = text.split('\\n').length;\n\tconst dimensions = {width, height};\n\tcache.set(text, dimensions);\n\n\treturn dimensions;\n};\n\nexport default measureText;\n"
  },
  {
    "path": "src/output.ts",
    "content": "import sliceAnsi from 'slice-ansi';\nimport stringWidth from 'string-width';\nimport {\n\ttype StyledChar,\n\tstyledCharsFromTokens,\n\tstyledCharsToString,\n\ttokenize,\n} from '@alcalzone/ansi-tokenize';\nimport {type OutputTransformer} from './render-node-to-output.js';\n\n/**\n\"Virtual\" output class\n\nHandles the positioning and saving of the output of each node in the tree. Also responsible for applying transformations to each character of the output.\n\nUsed to generate the final output of all nodes before writing it to actual output stream (e.g. stdout)\n*/\n\ntype Options = {\n\twidth: number;\n\theight: number;\n};\n\ntype Operation = WriteOperation | ClipOperation | UnclipOperation;\n\ntype WriteOperation = {\n\ttype: 'write';\n\tx: number;\n\ty: number;\n\ttext: string;\n\ttransformers: OutputTransformer[];\n};\n\ntype ClipOperation = {\n\ttype: 'clip';\n\tclip: Clip;\n};\n\ntype Clip = {\n\tx1: number | undefined;\n\tx2: number | undefined;\n\ty1: number | undefined;\n\ty2: number | undefined;\n};\n\ntype UnclipOperation = {\n\ttype: 'unclip';\n};\n\nclass OutputCaches {\n\twidths = new Map<string, number>();\n\tblockWidths = new Map<string, number>();\n\tstyledChars = new Map<string, StyledChar[]>();\n\n\tgetStyledChars(line: string): StyledChar[] {\n\t\tlet cached = this.styledChars.get(line);\n\t\tif (cached === undefined) {\n\t\t\tcached = styledCharsFromTokens(tokenize(line));\n\t\t\tthis.styledChars.set(line, cached);\n\t\t}\n\n\t\treturn cached;\n\t}\n\n\tgetStringWidth(text: string): number {\n\t\tlet cached = this.widths.get(text);\n\t\tif (cached === undefined) {\n\t\t\tcached = stringWidth(text);\n\t\t\tthis.widths.set(text, cached);\n\t\t}\n\n\t\treturn cached;\n\t}\n\n\tgetWidestLine(text: string): number {\n\t\tlet cached = this.blockWidths.get(text);\n\t\tif (cached === undefined) {\n\t\t\tlet lineWidth = 0;\n\t\t\tfor (const line of text.split('\\n')) {\n\t\t\t\tlineWidth = Math.max(lineWidth, this.getStringWidth(line));\n\t\t\t}\n\n\t\t\tcached = lineWidth;\n\t\t\tthis.blockWidths.set(text, cached);\n\t\t}\n\n\t\treturn cached;\n\t}\n}\n\nexport default class Output {\n\twidth: number;\n\theight: number;\n\n\tprivate readonly operations: Operation[] = [];\n\tprivate readonly caches: OutputCaches = new OutputCaches();\n\n\tconstructor(options: Options) {\n\t\tconst {width, height} = options;\n\n\t\tthis.width = width;\n\t\tthis.height = height;\n\t}\n\n\twrite(\n\t\tx: number,\n\t\ty: number,\n\t\ttext: string,\n\t\toptions: {transformers: OutputTransformer[]},\n\t): void {\n\t\tconst {transformers} = options;\n\n\t\tif (!text) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.operations.push({\n\t\t\ttype: 'write',\n\t\t\tx,\n\t\t\ty,\n\t\t\ttext,\n\t\t\ttransformers,\n\t\t});\n\t}\n\n\tclip(clip: Clip) {\n\t\tthis.operations.push({\n\t\t\ttype: 'clip',\n\t\t\tclip,\n\t\t});\n\t}\n\n\tunclip() {\n\t\tthis.operations.push({\n\t\t\ttype: 'unclip',\n\t\t});\n\t}\n\n\tget(): {output: string; height: number} {\n\t\t// Initialize output array with a specific set of rows, so that margin/padding at the bottom is preserved\n\t\tconst output: StyledChar[][] = [];\n\n\t\tfor (let y = 0; y < this.height; y++) {\n\t\t\tconst row: StyledChar[] = [];\n\n\t\t\tfor (let x = 0; x < this.width; x++) {\n\t\t\t\trow.push({\n\t\t\t\t\ttype: 'char',\n\t\t\t\t\tvalue: ' ',\n\t\t\t\t\tfullWidth: false,\n\t\t\t\t\tstyles: [],\n\t\t\t\t});\n\t\t\t}\n\n\t\t\toutput.push(row);\n\t\t}\n\n\t\tconst clips: Clip[] = [];\n\n\t\tfor (const operation of this.operations) {\n\t\t\tif (operation.type === 'clip') {\n\t\t\t\tclips.push(operation.clip);\n\t\t\t}\n\n\t\t\tif (operation.type === 'unclip') {\n\t\t\t\tclips.pop();\n\t\t\t}\n\n\t\t\tif (operation.type === 'write') {\n\t\t\t\tconst {text, transformers} = operation;\n\t\t\t\tlet {x, y} = operation;\n\t\t\t\tlet lines = text.split('\\n');\n\n\t\t\t\tconst clip = clips.at(-1);\n\n\t\t\t\tif (clip) {\n\t\t\t\t\tconst clipHorizontally =\n\t\t\t\t\t\ttypeof clip?.x1 === 'number' && typeof clip?.x2 === 'number';\n\n\t\t\t\t\tconst clipVertically =\n\t\t\t\t\t\ttypeof clip?.y1 === 'number' && typeof clip?.y2 === 'number';\n\n\t\t\t\t\t// If text is positioned outside of clipping area altogether,\n\t\t\t\t\t// skip to the next operation to avoid unnecessary calculations\n\t\t\t\t\tif (clipHorizontally) {\n\t\t\t\t\t\tconst width = this.caches.getWidestLine(text);\n\n\t\t\t\t\t\tif (x + width < clip.x1! || x > clip.x2!) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (clipVertically) {\n\t\t\t\t\t\tconst height = lines.length;\n\n\t\t\t\t\t\tif (y + height < clip.y1! || y > clip.y2!) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (clipHorizontally) {\n\t\t\t\t\t\tlines = lines.map(line => {\n\t\t\t\t\t\t\tconst from = x < clip.x1! ? clip.x1! - x : 0;\n\t\t\t\t\t\t\tconst width = this.caches.getStringWidth(line);\n\t\t\t\t\t\t\tconst to = x + width > clip.x2! ? clip.x2! - x : width;\n\n\t\t\t\t\t\t\treturn sliceAnsi(line, from, to);\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (x < clip.x1!) {\n\t\t\t\t\t\t\tx = clip.x1!;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (clipVertically) {\n\t\t\t\t\t\tconst from = y < clip.y1! ? clip.y1! - y : 0;\n\t\t\t\t\t\tconst height = lines.length;\n\t\t\t\t\t\tconst to = y + height > clip.y2! ? clip.y2! - y : height;\n\n\t\t\t\t\t\tlines = lines.slice(from, to);\n\n\t\t\t\t\t\tif (y < clip.y1!) {\n\t\t\t\t\t\t\ty = clip.y1!;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlet offsetY = 0;\n\n\t\t\t\tfor (let [index, line] of lines.entries()) {\n\t\t\t\t\tconst currentLine = output[y + offsetY];\n\n\t\t\t\t\t// Line can be missing if `text` is taller than height of pre-initialized `this.output`\n\t\t\t\t\tif (!currentLine) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const transformer of transformers) {\n\t\t\t\t\t\tline = transformer(line, index);\n\t\t\t\t\t}\n\n\t\t\t\t\tconst characters = this.caches.getStyledChars(line);\n\t\t\t\t\tlet offsetX = x;\n\n\t\t\t\t\tfor (const character of characters) {\n\t\t\t\t\t\tcurrentLine[offsetX] = character;\n\n\t\t\t\t\t\t// Determine printed width using string-width to align with measurement\n\t\t\t\t\t\tconst characterWidth = Math.max(\n\t\t\t\t\t\t\t1,\n\t\t\t\t\t\t\tthis.caches.getStringWidth(character.value),\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// For multi-column characters, clear following cells to avoid stray spaces/artifacts\n\t\t\t\t\t\tif (characterWidth > 1) {\n\t\t\t\t\t\t\tfor (let index = 1; index < characterWidth; index++) {\n\t\t\t\t\t\t\t\tcurrentLine[offsetX + index] = {\n\t\t\t\t\t\t\t\t\ttype: 'char',\n\t\t\t\t\t\t\t\t\tvalue: '',\n\t\t\t\t\t\t\t\t\tfullWidth: false,\n\t\t\t\t\t\t\t\t\tstyles: character.styles,\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\toffsetX += characterWidth;\n\t\t\t\t\t}\n\n\t\t\t\t\toffsetY++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst generatedOutput = output\n\t\t\t.map(line => {\n\t\t\t\t// See https://github.com/vadimdemedes/ink/pull/564#issuecomment-1637022742\n\t\t\t\tconst lineWithoutEmptyItems = line.filter(item => item !== undefined);\n\n\t\t\t\treturn styledCharsToString(lineWithoutEmptyItems).trimEnd();\n\t\t\t})\n\t\t\t.join('\\n');\n\n\t\treturn {\n\t\t\toutput: generatedOutput,\n\t\t\theight: output.length,\n\t\t};\n\t}\n}\n"
  },
  {
    "path": "src/parse-keypress.ts",
    "content": "// Copied from https://github.com/enquirer/enquirer/blob/36785f3399a41cd61e9d28d1eb9c2fcd73d69b4c/lib/keypress.js\nimport {Buffer} from 'node:buffer';\nimport {kittyModifiers} from './kitty-keyboard.js';\n\nconst metaKeyCodeRe = /^(?:\\x1b)([a-zA-Z0-9])$/;\n\nconst fnKeyRe =\n\t/^(?:\\x1b+)(O|N|\\[|\\[\\[)(?:(\\d+)(?:;(\\d+))?([~^$])|(?:1;)?(\\d+)?([a-zA-Z]))/;\n\nconst keyName: Record<string, string> = {\n\t/* xterm/gnome ESC O letter */\n\tOP: 'f1',\n\tOQ: 'f2',\n\tOR: 'f3',\n\tOS: 'f4',\n\t/* xterm/rxvt ESC [ number ~ */\n\t'[11~': 'f1',\n\t'[12~': 'f2',\n\t'[13~': 'f3',\n\t'[14~': 'f4',\n\t/* from Cygwin and used in libuv */\n\t'[[A': 'f1',\n\t'[[B': 'f2',\n\t'[[C': 'f3',\n\t'[[D': 'f4',\n\t'[[E': 'f5',\n\t/* common */\n\t'[15~': 'f5',\n\t'[17~': 'f6',\n\t'[18~': 'f7',\n\t'[19~': 'f8',\n\t'[20~': 'f9',\n\t'[21~': 'f10',\n\t'[23~': 'f11',\n\t'[24~': 'f12',\n\t/* xterm ESC [ letter */\n\t'[A': 'up',\n\t'[B': 'down',\n\t'[C': 'right',\n\t'[D': 'left',\n\t'[E': 'clear',\n\t'[F': 'end',\n\t'[H': 'home',\n\t/* xterm/gnome ESC O letter */\n\tOA: 'up',\n\tOB: 'down',\n\tOC: 'right',\n\tOD: 'left',\n\tOE: 'clear',\n\tOF: 'end',\n\tOH: 'home',\n\t/* xterm/rxvt ESC [ number ~ */\n\t'[1~': 'home',\n\t'[2~': 'insert',\n\t'[3~': 'delete',\n\t'[4~': 'end',\n\t'[5~': 'pageup',\n\t'[6~': 'pagedown',\n\t/* putty */\n\t'[[5~': 'pageup',\n\t'[[6~': 'pagedown',\n\t/* rxvt */\n\t'[7~': 'home',\n\t'[8~': 'end',\n\t/* rxvt keys with modifiers */\n\t'[a': 'up',\n\t'[b': 'down',\n\t'[c': 'right',\n\t'[d': 'left',\n\t'[e': 'clear',\n\n\t'[2$': 'insert',\n\t'[3$': 'delete',\n\t'[5$': 'pageup',\n\t'[6$': 'pagedown',\n\t'[7$': 'home',\n\t'[8$': 'end',\n\n\tOa: 'up',\n\tOb: 'down',\n\tOc: 'right',\n\tOd: 'left',\n\tOe: 'clear',\n\n\t'[2^': 'insert',\n\t'[3^': 'delete',\n\t'[5^': 'pageup',\n\t'[6^': 'pagedown',\n\t'[7^': 'home',\n\t'[8^': 'end',\n\t/* misc. */\n\t'[Z': 'tab',\n};\n\nexport const nonAlphanumericKeys = [...Object.values(keyName), 'backspace'];\n\nconst isShiftKey = (code: string) => {\n\treturn [\n\t\t'[a',\n\t\t'[b',\n\t\t'[c',\n\t\t'[d',\n\t\t'[e',\n\t\t'[2$',\n\t\t'[3$',\n\t\t'[5$',\n\t\t'[6$',\n\t\t'[7$',\n\t\t'[8$',\n\t\t'[Z',\n\t].includes(code);\n};\n\nconst isCtrlKey = (code: string) => {\n\treturn [\n\t\t'Oa',\n\t\t'Ob',\n\t\t'Oc',\n\t\t'Od',\n\t\t'Oe',\n\t\t'[2^',\n\t\t'[3^',\n\t\t'[5^',\n\t\t'[6^',\n\t\t'[7^',\n\t\t'[8^',\n\t].includes(code);\n};\n\ntype ParsedKey = {\n\tname: string;\n\tctrl: boolean;\n\tmeta: boolean;\n\tshift: boolean;\n\toption: boolean;\n\tsequence: string;\n\traw: string | undefined;\n\tcode?: string;\n\tsuper?: boolean;\n\thyper?: boolean;\n\tcapsLock?: boolean;\n\tnumLock?: boolean;\n\teventType?: 'press' | 'repeat' | 'release';\n\tisKittyProtocol?: boolean;\n\ttext?: string;\n\t// Whether this key represents printable text input.\n\t// When false, the key is a control/function/modifier key that should not\n\t// produce text input (e.g., arrows, function keys, capslock, media keys).\n\t// Only set by the kitty protocol parser.\n\tisPrintable?: boolean;\n};\n\n// Kitty keyboard protocol: CSI codepoint ; modifiers [: eventType] [; text-as-codepoints] u\nconst kittyKeyRe = /^\\x1b\\[(\\d+)(?:;(\\d+)(?::(\\d+))?(?:;([\\d:]+))?)?u$/;\n\n// Kitty-enhanced special keys: CSI number ; modifiers : eventType {letter|~}\n// These are legacy CSI sequences enhanced with the :eventType field.\n// Examples: \\x1b[1;1:1A (up arrow press), \\x1b[3;1:3~ (delete release)\nconst kittySpecialKeyRe = /^\\x1b\\[(\\d+);(\\d+):(\\d+)([A-Za-z~])$/;\n\n// Letter-terminated special key names (CSI 1 ; mods letter)\nconst kittySpecialLetterKeys: Record<string, string> = {\n\tA: 'up',\n\tB: 'down',\n\tC: 'right',\n\tD: 'left',\n\tE: 'clear',\n\tF: 'end',\n\tH: 'home',\n\tP: 'f1',\n\tQ: 'f2',\n\tR: 'f3',\n\tS: 'f4',\n};\n\n// Number-terminated special key names (CSI number ; mods ~)\nconst kittySpecialNumberKeys: Record<number, string> = {\n\t2: 'insert',\n\t3: 'delete',\n\t5: 'pageup',\n\t6: 'pagedown',\n\t7: 'home',\n\t8: 'end',\n\t11: 'f1',\n\t12: 'f2',\n\t13: 'f3',\n\t14: 'f4',\n\t15: 'f5',\n\t17: 'f6',\n\t18: 'f7',\n\t19: 'f8',\n\t20: 'f9',\n\t21: 'f10',\n\t23: 'f11',\n\t24: 'f12',\n};\n\n// Map of special codepoints to key names in kitty protocol\nconst kittyCodepointNames: Record<number, string> = {\n\t27: 'escape',\n\t// 13 (return) and 32 (space) are handled before this lookup\n\t// in parseKittyKeypress so they can be marked as printable.\n\t9: 'tab',\n\t127: 'delete',\n\t8: 'backspace',\n\t57358: 'capslock',\n\t57359: 'scrolllock',\n\t57360: 'numlock',\n\t57361: 'printscreen',\n\t57362: 'pause',\n\t57363: 'menu',\n\t57376: 'f13',\n\t57377: 'f14',\n\t57378: 'f15',\n\t57379: 'f16',\n\t57380: 'f17',\n\t57381: 'f18',\n\t57382: 'f19',\n\t57383: 'f20',\n\t57384: 'f21',\n\t57385: 'f22',\n\t57386: 'f23',\n\t57387: 'f24',\n\t57388: 'f25',\n\t57389: 'f26',\n\t57390: 'f27',\n\t57391: 'f28',\n\t57392: 'f29',\n\t57393: 'f30',\n\t57394: 'f31',\n\t57395: 'f32',\n\t57396: 'f33',\n\t57397: 'f34',\n\t57398: 'f35',\n\t57399: 'kp0',\n\t57400: 'kp1',\n\t57401: 'kp2',\n\t57402: 'kp3',\n\t57403: 'kp4',\n\t57404: 'kp5',\n\t57405: 'kp6',\n\t57406: 'kp7',\n\t57407: 'kp8',\n\t57408: 'kp9',\n\t57409: 'kpdecimal',\n\t57410: 'kpdivide',\n\t57411: 'kpmultiply',\n\t57412: 'kpsubtract',\n\t57413: 'kpadd',\n\t57414: 'kpenter',\n\t57415: 'kpequal',\n\t57416: 'kpseparator',\n\t57417: 'kpleft',\n\t57418: 'kpright',\n\t57419: 'kpup',\n\t57420: 'kpdown',\n\t57421: 'kppageup',\n\t57422: 'kppagedown',\n\t57423: 'kphome',\n\t57424: 'kpend',\n\t57425: 'kpinsert',\n\t57426: 'kpdelete',\n\t57427: 'kpbegin',\n\t57428: 'mediaplay',\n\t57429: 'mediapause',\n\t57430: 'mediaplaypause',\n\t57431: 'mediareverse',\n\t57432: 'mediastop',\n\t57433: 'mediafastforward',\n\t57434: 'mediarewind',\n\t57435: 'mediatracknext',\n\t57436: 'mediatrackprevious',\n\t57437: 'mediarecord',\n\t57438: 'lowervolume',\n\t57439: 'raisevolume',\n\t57440: 'mutevolume',\n\t57441: 'leftshift',\n\t57442: 'leftcontrol',\n\t57443: 'leftalt',\n\t57444: 'leftsuper',\n\t57445: 'lefthyper',\n\t57446: 'leftmeta',\n\t57447: 'rightshift',\n\t57448: 'rightcontrol',\n\t57449: 'rightalt',\n\t57450: 'rightsuper',\n\t57451: 'righthyper',\n\t57452: 'rightmeta',\n\t57453: 'isoLevel3Shift',\n\t57454: 'isoLevel5Shift',\n};\n\n// Valid Unicode codepoint range, excluding surrogates\nconst isValidCodepoint = (cp: number): boolean =>\n\tcp >= 0 && cp <= 0x10_ffff && !(cp >= 0xd8_00 && cp <= 0xdf_ff);\n\nconst safeFromCodePoint = (cp: number): string =>\n\tisValidCodepoint(cp) ? String.fromCodePoint(cp) : '?';\n\ntype EventType = 'press' | 'repeat' | 'release';\n\nfunction resolveEventType(value: number): EventType {\n\tif (value === 3) return 'release';\n\tif (value === 2) return 'repeat';\n\treturn 'press';\n}\n\nfunction parseKittyModifiers(\n\tmodifiers: number,\n): Pick<\n\tParsedKey,\n\t| 'ctrl'\n\t| 'shift'\n\t| 'meta'\n\t| 'option'\n\t| 'super'\n\t| 'hyper'\n\t| 'capsLock'\n\t| 'numLock'\n> {\n\treturn {\n\t\tctrl: !!(modifiers & kittyModifiers.ctrl),\n\t\tshift: !!(modifiers & kittyModifiers.shift),\n\t\tmeta: !!(modifiers & kittyModifiers.meta),\n\t\toption: !!(modifiers & kittyModifiers.alt),\n\t\tsuper: !!(modifiers & kittyModifiers.super),\n\t\thyper: !!(modifiers & kittyModifiers.hyper),\n\t\tcapsLock: !!(modifiers & kittyModifiers.capsLock),\n\t\tnumLock: !!(modifiers & kittyModifiers.numLock),\n\t};\n}\n\nconst parseKittyKeypress = (s: string): ParsedKey | null => {\n\tconst match = kittyKeyRe.exec(s);\n\tif (!match) return null;\n\n\tconst codepoint = parseInt(match[1]!, 10);\n\tconst modifiers = match[2] ? Math.max(0, parseInt(match[2], 10) - 1) : 0;\n\tconst eventType = match[3] ? parseInt(match[3], 10) : 1;\n\tconst textField = match[4];\n\n\t// Bail on invalid primary codepoint\n\tif (!isValidCodepoint(codepoint)) {\n\t\treturn null;\n\t}\n\n\t// Parse text-as-codepoints field (colon-separated Unicode codepoints)\n\tlet text: string | undefined;\n\tif (textField) {\n\t\ttext = textField\n\t\t\t.split(':')\n\t\t\t.map(cp => safeFromCodePoint(parseInt(cp, 10)))\n\t\t\t.join('');\n\t}\n\n\t// Determine key name from codepoint\n\tlet name: string;\n\tlet isPrintable: boolean;\n\tif (codepoint === 32) {\n\t\tname = 'space';\n\t\tisPrintable = true;\n\t} else if (codepoint === 13) {\n\t\tname = 'return';\n\t\tisPrintable = true;\n\t} else if (kittyCodepointNames[codepoint]) {\n\t\tname = kittyCodepointNames[codepoint]!;\n\t\tisPrintable = false;\n\t} else if (codepoint >= 1 && codepoint <= 26) {\n\t\t// Ctrl+letter comes as codepoint 1-26\n\t\tname = String.fromCodePoint(codepoint + 96); // 'a' is 97\n\t\tisPrintable = false;\n\t} else {\n\t\tname = safeFromCodePoint(codepoint).toLowerCase();\n\t\tisPrintable = true;\n\t}\n\n\t// Default text to the character from the codepoint when not explicitly\n\t// provided by the protocol, so keys like space and return produce their\n\t// expected text input (' ' and '\\r' respectively).\n\tif (isPrintable && !text) {\n\t\ttext = safeFromCodePoint(codepoint);\n\t}\n\n\treturn {\n\t\tname,\n\t\t...parseKittyModifiers(modifiers),\n\t\teventType: resolveEventType(eventType),\n\t\tsequence: s,\n\t\traw: s,\n\t\tisKittyProtocol: true,\n\t\tisPrintable,\n\t\ttext,\n\t};\n};\n\n// Parse kitty-enhanced special key sequences (arrow keys, function keys, etc.)\n// These use the legacy CSI format but with an added :eventType field.\nconst parseKittySpecialKey = (s: string): ParsedKey | null => {\n\tconst match = kittySpecialKeyRe.exec(s);\n\tif (!match) return null;\n\n\tconst number = parseInt(match[1]!, 10);\n\tconst modifiers = Math.max(0, parseInt(match[2]!, 10) - 1);\n\tconst eventType = parseInt(match[3]!, 10);\n\tconst terminator = match[4]!;\n\n\tconst name =\n\t\tterminator === '~'\n\t\t\t? kittySpecialNumberKeys[number]\n\t\t\t: kittySpecialLetterKeys[terminator];\n\n\tif (!name) return null;\n\n\treturn {\n\t\tname,\n\t\t...parseKittyModifiers(modifiers),\n\t\teventType: resolveEventType(eventType),\n\t\tsequence: s,\n\t\traw: s,\n\t\tisKittyProtocol: true,\n\t\tisPrintable: false,\n\t};\n};\n\nconst parseKeypress = (s: Buffer | string = ''): ParsedKey => {\n\tlet parts;\n\n\tif (Buffer.isBuffer(s)) {\n\t\tif (s[0]! > 127 && s[1] === undefined) {\n\t\t\t(s[0] as unknown as number) -= 128;\n\t\t\ts = '\\x1b' + String(s);\n\t\t} else {\n\t\t\ts = String(s);\n\t\t}\n\t} else if (s !== undefined && typeof s !== 'string') {\n\t\ts = String(s);\n\t} else if (!s) {\n\t\ts = '';\n\t}\n\n\t// Try kitty keyboard protocol parsers first\n\tconst kittyResult = parseKittyKeypress(s);\n\tif (kittyResult) return kittyResult;\n\n\tconst kittySpecialResult = parseKittySpecialKey(s);\n\tif (kittySpecialResult) return kittySpecialResult;\n\n\t// If the input matched the kitty CSI-u pattern but was rejected (e.g.,\n\t// invalid codepoint), return a safe empty keypress instead of falling\n\t// through to legacy parsing which can produce unsafe states (undefined name)\n\tif (kittyKeyRe.test(s)) {\n\t\treturn {\n\t\t\tname: '',\n\t\t\tctrl: false,\n\t\t\tmeta: false,\n\t\t\tshift: false,\n\t\t\toption: false,\n\t\t\tsequence: s,\n\t\t\traw: s,\n\t\t\tisKittyProtocol: true,\n\t\t\tisPrintable: false,\n\t\t};\n\t}\n\n\tconst key: ParsedKey = {\n\t\tname: '',\n\t\tctrl: false,\n\t\tmeta: false,\n\t\tshift: false,\n\t\toption: false,\n\t\tsequence: s,\n\t\traw: s,\n\t};\n\n\tkey.sequence = key.sequence || s || key.name;\n\n\tif (s === '\\r' || s === '\\x1b\\r') {\n\t\t// carriage return (or option+return on macOS)\n\t\tkey.raw = undefined;\n\t\tkey.name = 'return';\n\t\tkey.option = s.length === 2;\n\t} else if (s === '\\n') {\n\t\t// enter, should have been called linefeed\n\t\tkey.name = 'enter';\n\t} else if (s === '\\t') {\n\t\t// tab\n\t\tkey.name = 'tab';\n\t} else if (s === '\\b' || s === '\\x1b\\b') {\n\t\t// backspace or ctrl+h\n\t\tkey.name = 'backspace';\n\t\tkey.meta = s.charAt(0) === '\\x1b';\n\t} else if (s === '\\x7f' || s === '\\x1b\\x7f') {\n\t\t// TODO(vadimdemedes): `enquirer` detects delete key as backspace, but I had to split them up to avoid breaking changes in Ink. Merge them back together in the next major version.\n\t\t// delete\n\t\tkey.name = 'delete';\n\t\tkey.meta = s.charAt(0) === '\\x1b';\n\t} else if (s === '\\x1b' || s === '\\x1b\\x1b') {\n\t\t// escape key\n\t\tkey.name = 'escape';\n\t\tkey.meta = s.length === 2;\n\t} else if (s === ' ' || s === '\\x1b ') {\n\t\tkey.name = 'space';\n\t\tkey.meta = s.length === 2;\n\t} else if (s.length === 1 && s <= '\\x1a') {\n\t\t// ctrl+letter\n\t\tkey.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1);\n\t\tkey.ctrl = true;\n\t} else if (s.length === 1 && s >= '0' && s <= '9') {\n\t\t// number\n\t\tkey.name = 'number';\n\t} else if (s.length === 1 && s >= 'a' && s <= 'z') {\n\t\t// lowercase letter\n\t\tkey.name = s;\n\t} else if (s.length === 1 && s >= 'A' && s <= 'Z') {\n\t\t// shift+letter\n\t\tkey.name = s.toLowerCase();\n\t\tkey.shift = true;\n\t} else if ((parts = metaKeyCodeRe.exec(s))) {\n\t\t// meta+character key\n\t\tkey.meta = true;\n\t\tkey.shift = /^[A-Z]$/.test(parts[1]!);\n\t} else if ((parts = fnKeyRe.exec(s))) {\n\t\tconst segs = [...s];\n\n\t\tif (segs[0] === '\\u001b' && segs[1] === '\\u001b') {\n\t\t\tkey.option = true;\n\t\t}\n\n\t\t// ansi escape sequence\n\t\t// reassemble the key code leaving out leading \\x1b's,\n\t\t// the modifier key bitflag and any meaningless \"1;\" sequence\n\t\tconst code = [parts[1], parts[2], parts[4], parts[6]]\n\t\t\t.filter(Boolean)\n\t\t\t.join('');\n\n\t\tconst modifier = ((parts[3] || parts[5] || 1) as number) - 1;\n\n\t\t// Parse the key modifier\n\t\tkey.ctrl = !!(modifier & 4);\n\t\tkey.meta = !!(modifier & 10);\n\t\tkey.shift = !!(modifier & 1);\n\t\tkey.code = code;\n\n\t\tkey.name = keyName[code]!;\n\t\tkey.shift = isShiftKey(code) || key.shift;\n\t\tkey.ctrl = isCtrlKey(code) || key.ctrl;\n\t}\n\n\treturn key;\n};\n\nexport default parseKeypress;\n"
  },
  {
    "path": "src/reconciler.ts",
    "content": "import process from 'node:process';\nimport createReconciler, {type ReactContext} from 'react-reconciler';\nimport {\n\tDefaultEventPriority,\n\tNoEventPriority,\n} from 'react-reconciler/constants.js';\nimport * as Scheduler from 'scheduler';\nimport Yoga, {type Node as YogaNode} from 'yoga-layout';\nimport {createContext, version as reactVersion} from 'react';\nimport {\n\tcreateTextNode,\n\tappendChildNode,\n\tinsertBeforeNode,\n\tremoveChildNode,\n\temitLayoutListeners,\n\tsetStyle,\n\tsetTextNodeValue,\n\tcreateNode,\n\tsetAttribute,\n\ttype DOMNodeAttribute,\n\ttype TextNode,\n\ttype ElementNames,\n\ttype DOMElement,\n} from './dom.js';\nimport applyStyles, {type Styles} from './styles.js';\nimport {type OutputTransformer} from './render-node-to-output.js';\n\n// We need to conditionally perform devtools connection to avoid\n// accidentally breaking other third-party code.\n// See https://github.com/vadimdemedes/ink/issues/384\n// See https://github.com/vadimdemedes/ink/issues/648\nif (process.env['DEV'] === 'true') {\n\t// Intentionally no warning when the package is missing.\n\t// DEV may be set for other reasons; devtools is opt-in via installing the package.\n\tlet isDevtoolsInstalled = false;\n\ttry {\n\t\timport.meta.resolve('react-devtools-core');\n\t\tisDevtoolsInstalled = true;\n\t} catch {}\n\n\tif (isDevtoolsInstalled) {\n\t\tawait import('./devtools.js');\n\t}\n}\n\ntype AnyObject = Record<string, unknown>;\n\nconst diff = (before: AnyObject, after: AnyObject): AnyObject | undefined => {\n\tif (before === after) {\n\t\treturn;\n\t}\n\n\tif (!before) {\n\t\treturn after;\n\t}\n\n\tconst changed: AnyObject = {};\n\tlet isChanged = false;\n\n\tfor (const key of Object.keys(before)) {\n\t\tconst isDeleted = after ? !Object.hasOwn(after, key) : true;\n\n\t\tif (isDeleted) {\n\t\t\tchanged[key] = undefined;\n\t\t\tisChanged = true;\n\t\t}\n\t}\n\n\tif (after) {\n\t\tfor (const key of Object.keys(after)) {\n\t\t\tif (after[key] !== before[key]) {\n\t\t\t\tchanged[key] = after[key];\n\t\t\t\tisChanged = true;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn isChanged ? changed : undefined;\n};\n\nconst cleanupYogaNode = (node?: YogaNode): void => {\n\tnode?.unsetMeasureFunc();\n\tnode?.freeRecursive();\n};\n\ntype Props = Record<string, unknown>;\n\ntype HostContext = {\n\tisInsideText: boolean;\n};\n\nlet currentUpdatePriority = NoEventPriority;\n\nlet currentRootNode: DOMElement | undefined;\n\nasync function loadPackageJson() {\n\tconst fs = await import('node:fs');\n\tconst content = fs.readFileSync(\n\t\tnew URL('../package.json', import.meta.url),\n\t\t'utf8',\n\t);\n\n\tconst parsedContent = JSON.parse(content) as\n\t\t| {\n\t\t\t\tname?: string;\n\t\t\t\tversion?: string;\n\t\t  }\n\t\t| undefined;\n\n\treturn {\n\t\tname: parsedContent?.name,\n\t\tversion: parsedContent?.version,\n\t};\n}\n\nlet packageInfo = {\n\tname: 'ink',\n\tversion: reactVersion,\n};\n\nif (process.env['DEV'] === 'true') {\n\ttry {\n\t\tconst loaded = await loadPackageJson();\n\t\tpackageInfo = {\n\t\t\t// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing\n\t\t\tname: loaded.name || packageInfo.name,\n\t\t\t// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing\n\t\t\tversion: loaded.version || packageInfo.version,\n\t\t};\n\t} catch (error) {\n\t\tconsole.warn(\n\t\t\t'Failed to load package.json in development mode. Falling back to default renderer metadata.',\n\t\t\terror,\n\t\t);\n\t}\n}\n\nexport default createReconciler<\n\tElementNames,\n\tProps,\n\tDOMElement,\n\tDOMElement,\n\tTextNode,\n\tDOMElement,\n\tunknown,\n\tunknown,\n\tunknown,\n\tHostContext,\n\tunknown,\n\tunknown,\n\tunknown,\n\tunknown\n>({\n\tgetRootHostContext: () => ({\n\t\tisInsideText: false,\n\t}),\n\tprepareForCommit: () => null,\n\tpreparePortalMount: () => null,\n\tclearContainer: () => false,\n\tresetAfterCommit(rootNode) {\n\t\tif (typeof rootNode.onComputeLayout === 'function') {\n\t\t\trootNode.onComputeLayout();\n\t\t}\n\n\t\temitLayoutListeners(rootNode);\n\n\t\t// Since renders are throttled at the instance level and <Static> component children\n\t\t// are rendered only once and then get deleted, we need an escape hatch to\n\t\t// trigger an immediate render to ensure <Static> children are written to output before they get erased\n\t\tif (rootNode.isStaticDirty) {\n\t\t\trootNode.isStaticDirty = false;\n\t\t\tif (typeof rootNode.onImmediateRender === 'function') {\n\t\t\t\trootNode.onImmediateRender();\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (typeof rootNode.onRender === 'function') {\n\t\t\trootNode.onRender();\n\t\t}\n\t},\n\tgetChildHostContext(parentHostContext, type) {\n\t\tconst previousIsInsideText = parentHostContext.isInsideText;\n\t\tconst isInsideText = type === 'ink-text' || type === 'ink-virtual-text';\n\n\t\tif (previousIsInsideText === isInsideText) {\n\t\t\treturn parentHostContext;\n\t\t}\n\n\t\treturn {isInsideText};\n\t},\n\tshouldSetTextContent: () => false,\n\tcreateInstance(originalType, newProps, rootNode, hostContext) {\n\t\tif (hostContext.isInsideText && originalType === 'ink-box') {\n\t\t\tthrow new Error(`<Box> can’t be nested inside <Text> component`);\n\t\t}\n\n\t\tconst type =\n\t\t\toriginalType === 'ink-text' && hostContext.isInsideText\n\t\t\t\t? 'ink-virtual-text'\n\t\t\t\t: originalType;\n\n\t\tconst node = createNode(type);\n\n\t\tfor (const [key, value] of Object.entries(newProps)) {\n\t\t\tif (key === 'children') {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (key === 'style') {\n\t\t\t\tsetStyle(node, value as Styles);\n\n\t\t\t\tif (node.yogaNode) {\n\t\t\t\t\tapplyStyles(node.yogaNode, value as Styles);\n\t\t\t\t}\n\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (key === 'internal_transform') {\n\t\t\t\tnode.internal_transform = value as OutputTransformer;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (key === 'internal_static') {\n\t\t\t\tcurrentRootNode = rootNode;\n\t\t\t\tnode.internal_static = true;\n\t\t\t\trootNode.isStaticDirty = true;\n\n\t\t\t\t// Save reference to <Static> node to skip traversal of entire\n\t\t\t\t// node tree to find it\n\t\t\t\trootNode.staticNode = node;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tsetAttribute(node, key, value as DOMNodeAttribute);\n\t\t}\n\n\t\treturn node;\n\t},\n\tcreateTextInstance(text, _root, hostContext) {\n\t\tif (!hostContext.isInsideText) {\n\t\t\tthrow new Error(\n\t\t\t\t`Text string \"${text}\" must be rendered inside <Text> component`,\n\t\t\t);\n\t\t}\n\n\t\treturn createTextNode(text);\n\t},\n\tresetTextContent() {},\n\thideTextInstance(node) {\n\t\tsetTextNodeValue(node, '');\n\t},\n\tunhideTextInstance(node, text) {\n\t\tsetTextNodeValue(node, text);\n\t},\n\tgetPublicInstance: instance => instance,\n\thideInstance(node) {\n\t\tnode.yogaNode?.setDisplay(Yoga.DISPLAY_NONE);\n\t},\n\tunhideInstance(node) {\n\t\tnode.yogaNode?.setDisplay(Yoga.DISPLAY_FLEX);\n\t},\n\tappendInitialChild: appendChildNode,\n\tappendChild: appendChildNode,\n\tinsertBefore: insertBeforeNode,\n\tfinalizeInitialChildren() {\n\t\treturn false;\n\t},\n\tisPrimaryRenderer: true,\n\tsupportsMutation: true,\n\tsupportsPersistence: false,\n\tsupportsHydration: false,\n\t// Scheduler integration for concurrent mode\n\tsupportsMicrotasks: true,\n\tscheduleMicrotask: queueMicrotask,\n\t// @ts-expect-error @types/react-reconciler is outdated and doesn't include scheduleCallback\n\tscheduleCallback: Scheduler.unstable_scheduleCallback,\n\tcancelCallback: Scheduler.unstable_cancelCallback,\n\tshouldYield: Scheduler.unstable_shouldYield,\n\tnow: Scheduler.unstable_now,\n\tscheduleTimeout: setTimeout,\n\tcancelTimeout: clearTimeout,\n\tnoTimeout: -1,\n\tbeforeActiveInstanceBlur() {},\n\tafterActiveInstanceBlur() {},\n\tdetachDeletedInstance() {},\n\tgetInstanceFromNode: () => null,\n\tprepareScopeUpdate() {},\n\tgetInstanceFromScope: () => null,\n\tappendChildToContainer: appendChildNode,\n\tinsertInContainerBefore: insertBeforeNode,\n\tremoveChildFromContainer(node, removeNode) {\n\t\tremoveChildNode(node, removeNode);\n\t\tcleanupYogaNode(removeNode.yogaNode);\n\t},\n\tcommitUpdate(node, _type, oldProps, newProps) {\n\t\tif (currentRootNode && node.internal_static) {\n\t\t\tcurrentRootNode.isStaticDirty = true;\n\t\t}\n\n\t\tconst props = diff(oldProps, newProps);\n\n\t\tconst style = diff(\n\t\t\toldProps['style'] as Styles,\n\t\t\tnewProps['style'] as Styles,\n\t\t);\n\n\t\tif (!props && !style) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (props) {\n\t\t\tfor (const [key, value] of Object.entries(props)) {\n\t\t\t\tif (key === 'style') {\n\t\t\t\t\tsetStyle(node, value as Styles);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (key === 'internal_transform') {\n\t\t\t\t\tnode.internal_transform = value as OutputTransformer;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (key === 'internal_static') {\n\t\t\t\t\tnode.internal_static = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tsetAttribute(node, key, value as DOMNodeAttribute);\n\t\t\t}\n\t\t}\n\n\t\tif (style && node.yogaNode) {\n\t\t\tapplyStyles(\n\t\t\t\tnode.yogaNode,\n\t\t\t\tstyle,\n\t\t\t\t(newProps['style'] as Styles | undefined) ?? {},\n\t\t\t);\n\t\t}\n\t},\n\tcommitTextUpdate(node, _oldText, newText) {\n\t\tsetTextNodeValue(node, newText);\n\t},\n\tremoveChild(node, removeNode) {\n\t\tremoveChildNode(node, removeNode);\n\t\tcleanupYogaNode(removeNode.yogaNode);\n\t},\n\tsetCurrentUpdatePriority(newPriority: number) {\n\t\tcurrentUpdatePriority = newPriority;\n\t},\n\tgetCurrentUpdatePriority: () => currentUpdatePriority,\n\tresolveUpdatePriority() {\n\t\tif (currentUpdatePriority !== NoEventPriority) {\n\t\t\treturn currentUpdatePriority;\n\t\t}\n\n\t\treturn DefaultEventPriority;\n\t},\n\tmaySuspendCommit() {\n\t\t// Return true to enable Suspense resource preloading\n\t\treturn true;\n\t},\n\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\tNotPendingTransition: undefined,\n\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\tHostTransitionContext: createContext(\n\t\tnull,\n\t) as unknown as ReactContext<unknown>,\n\tresetFormInstance() {},\n\trequestPostPaintCallback() {},\n\tshouldAttemptEagerTransition() {\n\t\treturn false;\n\t},\n\ttrackSchedulerEvent() {},\n\tresolveEventType() {\n\t\treturn null;\n\t},\n\tresolveEventTimeStamp() {\n\t\treturn -1.1;\n\t},\n\tpreloadInstance() {\n\t\treturn true;\n\t},\n\tstartSuspendingCommit() {},\n\tsuspendInstance() {},\n\twaitForCommitToBeReady() {\n\t\treturn null;\n\t},\n\trendererPackageName: packageInfo.name,\n\trendererVersion: packageInfo.version,\n});\n"
  },
  {
    "path": "src/render-background.ts",
    "content": "import colorize from './colorize.js';\nimport {type DOMNode} from './dom.js';\nimport type Output from './output.js';\n\nconst renderBackground = (\n\tx: number,\n\ty: number,\n\tnode: DOMNode,\n\toutput: Output,\n): void => {\n\tif (!node.style.backgroundColor) {\n\t\treturn;\n\t}\n\n\tconst width = node.yogaNode!.getComputedWidth();\n\tconst height = node.yogaNode!.getComputedHeight();\n\n\t// Calculate the actual content area considering borders\n\tconst leftBorderWidth =\n\t\tnode.style.borderStyle && node.style.borderLeft !== false ? 1 : 0;\n\tconst rightBorderWidth =\n\t\tnode.style.borderStyle && node.style.borderRight !== false ? 1 : 0;\n\tconst topBorderHeight =\n\t\tnode.style.borderStyle && node.style.borderTop !== false ? 1 : 0;\n\tconst bottomBorderHeight =\n\t\tnode.style.borderStyle && node.style.borderBottom !== false ? 1 : 0;\n\n\tconst contentWidth = width - leftBorderWidth - rightBorderWidth;\n\tconst contentHeight = height - topBorderHeight - bottomBorderHeight;\n\n\tif (!(contentWidth > 0 && contentHeight > 0)) {\n\t\treturn;\n\t}\n\n\t// Create background fill for each row\n\tconst backgroundLine = colorize(\n\t\t' '.repeat(contentWidth),\n\t\tnode.style.backgroundColor,\n\t\t'background',\n\t);\n\n\tfor (let row = 0; row < contentHeight; row++) {\n\t\toutput.write(\n\t\t\tx + leftBorderWidth,\n\t\t\ty + topBorderHeight + row,\n\t\t\tbackgroundLine,\n\t\t\t{transformers: []},\n\t\t);\n\t}\n};\n\nexport default renderBackground;\n"
  },
  {
    "path": "src/render-border.ts",
    "content": "import cliBoxes from 'cli-boxes';\nimport chalk from 'chalk';\nimport colorize from './colorize.js';\nimport {type DOMNode} from './dom.js';\nimport type Output from './output.js';\n\nconst renderBorder = (\n\tx: number,\n\ty: number,\n\tnode: DOMNode,\n\toutput: Output,\n): void => {\n\tif (node.style.borderStyle) {\n\t\tconst width = node.yogaNode!.getComputedWidth();\n\t\tconst height = node.yogaNode!.getComputedHeight();\n\t\tconst box =\n\t\t\ttypeof node.style.borderStyle === 'string'\n\t\t\t\t? cliBoxes[node.style.borderStyle]\n\t\t\t\t: node.style.borderStyle;\n\n\t\tconst topBorderColor = node.style.borderTopColor ?? node.style.borderColor;\n\t\tconst bottomBorderColor =\n\t\t\tnode.style.borderBottomColor ?? node.style.borderColor;\n\t\tconst leftBorderColor =\n\t\t\tnode.style.borderLeftColor ?? node.style.borderColor;\n\t\tconst rightBorderColor =\n\t\t\tnode.style.borderRightColor ?? node.style.borderColor;\n\n\t\tconst dimTopBorderColor =\n\t\t\tnode.style.borderTopDimColor ?? node.style.borderDimColor;\n\n\t\tconst dimBottomBorderColor =\n\t\t\tnode.style.borderBottomDimColor ?? node.style.borderDimColor;\n\n\t\tconst dimLeftBorderColor =\n\t\t\tnode.style.borderLeftDimColor ?? node.style.borderDimColor;\n\n\t\tconst dimRightBorderColor =\n\t\t\tnode.style.borderRightDimColor ?? node.style.borderDimColor;\n\n\t\tconst showTopBorder = node.style.borderTop !== false;\n\t\tconst showBottomBorder = node.style.borderBottom !== false;\n\t\tconst showLeftBorder = node.style.borderLeft !== false;\n\t\tconst showRightBorder = node.style.borderRight !== false;\n\n\t\tconst contentWidth =\n\t\t\twidth - (showLeftBorder ? 1 : 0) - (showRightBorder ? 1 : 0);\n\n\t\tlet topBorder = showTopBorder\n\t\t\t? colorize(\n\t\t\t\t\t(showLeftBorder ? box.topLeft : '') +\n\t\t\t\t\t\tbox.top.repeat(contentWidth) +\n\t\t\t\t\t\t(showRightBorder ? box.topRight : ''),\n\t\t\t\t\ttopBorderColor,\n\t\t\t\t\t'foreground',\n\t\t\t\t)\n\t\t\t: undefined;\n\n\t\tif (showTopBorder && dimTopBorderColor) {\n\t\t\ttopBorder = chalk.dim(topBorder);\n\t\t}\n\n\t\tlet verticalBorderHeight = height;\n\n\t\tif (showTopBorder) {\n\t\t\tverticalBorderHeight -= 1;\n\t\t}\n\n\t\tif (showBottomBorder) {\n\t\t\tverticalBorderHeight -= 1;\n\t\t}\n\n\t\tlet leftBorder = (\n\t\t\tcolorize(box.left, leftBorderColor, 'foreground') + '\\n'\n\t\t).repeat(verticalBorderHeight);\n\n\t\tif (dimLeftBorderColor) {\n\t\t\tleftBorder = chalk.dim(leftBorder);\n\t\t}\n\n\t\tlet rightBorder = (\n\t\t\tcolorize(box.right, rightBorderColor, 'foreground') + '\\n'\n\t\t).repeat(verticalBorderHeight);\n\n\t\tif (dimRightBorderColor) {\n\t\t\trightBorder = chalk.dim(rightBorder);\n\t\t}\n\n\t\tlet bottomBorder = showBottomBorder\n\t\t\t? colorize(\n\t\t\t\t\t(showLeftBorder ? box.bottomLeft : '') +\n\t\t\t\t\t\tbox.bottom.repeat(contentWidth) +\n\t\t\t\t\t\t(showRightBorder ? box.bottomRight : ''),\n\t\t\t\t\tbottomBorderColor,\n\t\t\t\t\t'foreground',\n\t\t\t\t)\n\t\t\t: undefined;\n\n\t\tif (showBottomBorder && dimBottomBorderColor) {\n\t\t\tbottomBorder = chalk.dim(bottomBorder);\n\t\t}\n\n\t\tconst offsetY = showTopBorder ? 1 : 0;\n\n\t\tif (topBorder) {\n\t\t\toutput.write(x, y, topBorder, {transformers: []});\n\t\t}\n\n\t\tif (showLeftBorder) {\n\t\t\toutput.write(x, y + offsetY, leftBorder, {transformers: []});\n\t\t}\n\n\t\tif (showRightBorder) {\n\t\t\toutput.write(x + width - 1, y + offsetY, rightBorder, {\n\t\t\t\ttransformers: [],\n\t\t\t});\n\t\t}\n\n\t\tif (bottomBorder) {\n\t\t\toutput.write(x, y + height - 1, bottomBorder, {transformers: []});\n\t\t}\n\t}\n};\n\nexport default renderBorder;\n"
  },
  {
    "path": "src/render-node-to-output.ts",
    "content": "import widestLine from 'widest-line';\nimport indentString from 'indent-string';\nimport Yoga from 'yoga-layout';\nimport wrapText from './wrap-text.js';\nimport getMaxWidth from './get-max-width.js';\nimport squashTextNodes from './squash-text-nodes.js';\nimport renderBorder from './render-border.js';\nimport renderBackground from './render-background.js';\nimport {type DOMElement} from './dom.js';\nimport type Output from './output.js';\n\n// If parent container is `<Box>`, text nodes will be treated as separate nodes in\n// the tree and will have their own coordinates in the layout.\n// To ensure text nodes are aligned correctly, take X and Y of the first text node\n// and use it as offset for the rest of the nodes\n// Only first node is taken into account, because other text nodes can't have margin or padding,\n// so their coordinates will be relative to the first node anyway\nconst applyPaddingToText = (node: DOMElement, text: string): string => {\n\tconst yogaNode = node.childNodes[0]?.yogaNode;\n\n\tif (yogaNode) {\n\t\tconst offsetX = yogaNode.getComputedLeft();\n\t\tconst offsetY = yogaNode.getComputedTop();\n\t\ttext = '\\n'.repeat(offsetY) + indentString(text, offsetX);\n\t}\n\n\treturn text;\n};\n\nexport type OutputTransformer = (s: string, index: number) => string;\n\nexport const renderNodeToScreenReaderOutput = (\n\tnode: DOMElement,\n\toptions: {\n\t\tparentRole?: string;\n\t\tskipStaticElements?: boolean;\n\t} = {},\n): string => {\n\tif (options.skipStaticElements && node.internal_static) {\n\t\treturn '';\n\t}\n\n\tif (node.yogaNode?.getDisplay() === Yoga.DISPLAY_NONE) {\n\t\treturn '';\n\t}\n\n\tlet output = '';\n\n\tif (node.nodeName === 'ink-text') {\n\t\toutput = squashTextNodes(node);\n\t} else if (node.nodeName === 'ink-box' || node.nodeName === 'ink-root') {\n\t\tconst separator =\n\t\t\tnode.style.flexDirection === 'row' ||\n\t\t\tnode.style.flexDirection === 'row-reverse'\n\t\t\t\t? ' '\n\t\t\t\t: '\\n';\n\n\t\tconst childNodes =\n\t\t\tnode.style.flexDirection === 'row-reverse' ||\n\t\t\tnode.style.flexDirection === 'column-reverse'\n\t\t\t\t? [...node.childNodes].reverse()\n\t\t\t\t: [...node.childNodes];\n\n\t\toutput = childNodes\n\t\t\t.map(childNode => {\n\t\t\t\tconst screenReaderOutput = renderNodeToScreenReaderOutput(\n\t\t\t\t\tchildNode as DOMElement,\n\t\t\t\t\t{\n\t\t\t\t\t\tparentRole: node.internal_accessibility?.role,\n\t\t\t\t\t\tskipStaticElements: options.skipStaticElements,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\treturn screenReaderOutput;\n\t\t\t})\n\t\t\t.filter(Boolean)\n\t\t\t.join(separator);\n\t}\n\n\tif (node.internal_accessibility) {\n\t\tconst {role, state} = node.internal_accessibility;\n\n\t\tif (state) {\n\t\t\tconst stateKeys = Object.keys(state) as Array<keyof typeof state>;\n\t\t\tconst stateDescription = stateKeys.filter(key => state[key]).join(', ');\n\n\t\t\tif (stateDescription) {\n\t\t\t\toutput = `(${stateDescription}) ${output}`;\n\t\t\t}\n\t\t}\n\n\t\tif (role && role !== options.parentRole) {\n\t\t\toutput = `${role}: ${output}`;\n\t\t}\n\t}\n\n\treturn output;\n};\n\n// After nodes are laid out, render each to output object, which later gets rendered to terminal\nconst renderNodeToOutput = (\n\tnode: DOMElement,\n\toutput: Output,\n\toptions: {\n\t\toffsetX?: number;\n\t\toffsetY?: number;\n\t\ttransformers?: OutputTransformer[];\n\t\tskipStaticElements: boolean;\n\t},\n) => {\n\tconst {\n\t\toffsetX = 0,\n\t\toffsetY = 0,\n\t\ttransformers = [],\n\t\tskipStaticElements,\n\t} = options;\n\n\tif (skipStaticElements && node.internal_static) {\n\t\treturn;\n\t}\n\n\tconst {yogaNode} = node;\n\n\tif (yogaNode) {\n\t\tif (yogaNode.getDisplay() === Yoga.DISPLAY_NONE) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Left and top positions in Yoga are relative to their parent node\n\t\tconst x = offsetX + yogaNode.getComputedLeft();\n\t\tconst y = offsetY + yogaNode.getComputedTop();\n\n\t\t// Transformers are functions that transform final text output of each component\n\t\t// See Output class for logic that applies transformers\n\t\tlet newTransformers = transformers;\n\n\t\tif (typeof node.internal_transform === 'function') {\n\t\t\tnewTransformers = [node.internal_transform, ...transformers];\n\t\t}\n\n\t\tif (node.nodeName === 'ink-text') {\n\t\t\tlet text = squashTextNodes(node);\n\n\t\t\tif (text.length > 0) {\n\t\t\t\tconst currentWidth = widestLine(text);\n\t\t\t\tconst maxWidth = getMaxWidth(yogaNode);\n\n\t\t\t\tif (currentWidth > maxWidth) {\n\t\t\t\t\tconst textWrap = node.style.textWrap ?? 'wrap';\n\t\t\t\t\ttext = wrapText(text, maxWidth, textWrap);\n\t\t\t\t}\n\n\t\t\t\ttext = applyPaddingToText(node, text);\n\n\t\t\t\toutput.write(x, y, text, {transformers: newTransformers});\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tlet clipped = false;\n\n\t\tif (node.nodeName === 'ink-box') {\n\t\t\trenderBackground(x, y, node, output);\n\t\t\trenderBorder(x, y, node, output);\n\n\t\t\tconst clipHorizontally =\n\t\t\t\tnode.style.overflowX === 'hidden' || node.style.overflow === 'hidden';\n\t\t\tconst clipVertically =\n\t\t\t\tnode.style.overflowY === 'hidden' || node.style.overflow === 'hidden';\n\n\t\t\tif (clipHorizontally || clipVertically) {\n\t\t\t\tconst x1 = clipHorizontally\n\t\t\t\t\t? x + yogaNode.getComputedBorder(Yoga.EDGE_LEFT)\n\t\t\t\t\t: undefined;\n\n\t\t\t\tconst x2 = clipHorizontally\n\t\t\t\t\t? x +\n\t\t\t\t\t\tyogaNode.getComputedWidth() -\n\t\t\t\t\t\tyogaNode.getComputedBorder(Yoga.EDGE_RIGHT)\n\t\t\t\t\t: undefined;\n\n\t\t\t\tconst y1 = clipVertically\n\t\t\t\t\t? y + yogaNode.getComputedBorder(Yoga.EDGE_TOP)\n\t\t\t\t\t: undefined;\n\n\t\t\t\tconst y2 = clipVertically\n\t\t\t\t\t? y +\n\t\t\t\t\t\tyogaNode.getComputedHeight() -\n\t\t\t\t\t\tyogaNode.getComputedBorder(Yoga.EDGE_BOTTOM)\n\t\t\t\t\t: undefined;\n\n\t\t\t\toutput.clip({x1, x2, y1, y2});\n\t\t\t\tclipped = true;\n\t\t\t}\n\t\t}\n\n\t\tif (node.nodeName === 'ink-root' || node.nodeName === 'ink-box') {\n\t\t\tfor (const childNode of node.childNodes) {\n\t\t\t\trenderNodeToOutput(childNode as DOMElement, output, {\n\t\t\t\t\toffsetX: x,\n\t\t\t\t\toffsetY: y,\n\t\t\t\t\ttransformers: newTransformers,\n\t\t\t\t\tskipStaticElements,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (clipped) {\n\t\t\t\toutput.unclip();\n\t\t\t}\n\t\t}\n\t}\n};\n\nexport default renderNodeToOutput;\n"
  },
  {
    "path": "src/render-to-string.ts",
    "content": "import type {ReactNode} from 'react';\nimport Yoga from 'yoga-layout';\nimport {LegacyRoot} from 'react-reconciler/constants.js';\nimport reconciler from './reconciler.js';\nimport renderer from './renderer.js';\nimport {createNode, type DOMElement} from './dom.js';\n\nexport type RenderToStringOptions = {\n\t/**\n\tWidth of the virtual terminal in columns.\n\n\t@default 80\n\t*/\n\tcolumns?: number;\n};\n\n/**\nRender 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.\n\nUseful 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.\n\n**Notes:**\n\n- Terminal-specific hooks (`useInput`, `useStdin`, `useStdout`, `useStderr`, `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.\n- `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.\n- `useLayoutEffect` callbacks fire synchronously during commit, so state updates they trigger **will** be reflected in the output.\n- The `<Static>` component is supported — its output is prepended to the dynamic output.\n- If a component throws during rendering, the error is propagated to the caller after cleanup.\n\n@example\n```\nimport {renderToString, Text, Box} from 'ink';\n\nconst output = renderToString(\n\t<Box padding={1}>\n\t\t<Text color=\"green\">Hello World</Text>\n\t</Box>,\n\t{columns: 40}\n);\n\nconsole.log(output);\n```\n*/\nconst renderToString = (\n\tnode: ReactNode,\n\toptions?: RenderToStringOptions,\n): string => {\n\tconst columns = options?.columns ?? 80;\n\n\t// Create a standalone root node — no stdout, stdin, or terminal bindings\n\tconst rootNode: DOMElement = createNode('ink-root');\n\n\t// Capture static output from intermediate renders.\n\t// The <Static> component uses useLayoutEffect to clear its children after\n\t// the first commit. The reconciler's resetAfterCommit calls onImmediateRender\n\t// when static content is dirty (and returns early, skipping the normal\n\t// onRender callback), giving us a chance to capture it before it's cleared\n\t// by the subsequent re-render.\n\tlet capturedStaticOutput = '';\n\n\trootNode.onComputeLayout = () => {\n\t\trootNode.yogaNode!.setWidth(columns);\n\t\trootNode.yogaNode!.calculateLayout(\n\t\t\tundefined,\n\t\t\tundefined,\n\t\t\tYoga.DIRECTION_LTR,\n\t\t);\n\t};\n\n\trootNode.onImmediateRender = () => {\n\t\tconst {staticOutput} = renderer(rootNode, false);\n\t\tif (staticOutput && staticOutput !== '\\n') {\n\t\t\tcapturedStaticOutput += staticOutput;\n\t\t}\n\t};\n\n\t// Capture the first uncaught error so we can re-throw it after cleanup.\n\t// React's reconciler catches component errors internally and reports them\n\t// via onUncaughtError rather than letting them propagate. For a synchronous\n\t// utility like renderToString, callers expect errors to throw.\n\tlet uncaughtError: unknown;\n\n\t// Create a reconciler container in legacy (synchronous) mode.\n\t// The four trailing callbacks are: onUncaughtError, onCaughtError,\n\t// onRecoverableError, and onHostTransitionComplete.\n\t// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n\tconst container = reconciler.createContainer(\n\t\trootNode,\n\t\tLegacyRoot,\n\t\tnull,\n\t\tfalse,\n\t\tnull,\n\t\t'render-to-string',\n\t\t(error: unknown) => {\n\t\t\tuncaughtError ??= error;\n\t\t},\n\t\t() => {},\n\t\t() => {},\n\t\t() => {},\n\t);\n\n\tlet teardownSucceeded = false;\n\n\ttry {\n\t\t// Synchronously render the React tree into the container\n\t\treconciler.updateContainerSync(node, container, null, () => {});\n\t\treconciler.flushSyncWork();\n\n\t\t// Yoga layout has already been calculated by onComputeLayout during commit.\n\t\t// Render the DOM tree to a string — this captures the dynamic (non-static) output.\n\t\tconst {output} = renderer(rootNode, false);\n\n\t\t// Tear down: unmount the tree so the reconciler cleans up child nodes\n\t\t// and runs effect cleanup functions. Child Yoga nodes are freed by the\n\t\t// reconciler's removeChildFromContainer → cleanupYogaNode → freeRecursive.\n\t\treconciler.updateContainerSync(null, container, null, () => {});\n\t\treconciler.flushSyncWork();\n\t\tteardownSucceeded = true;\n\n\t\t// Free the root yoga node itself (children already freed by reconciler)\n\t\trootNode.yogaNode!.free();\n\n\t\t// Re-throw after full cleanup so callers see the original error.\n\t\tif (uncaughtError !== undefined) {\n\t\t\tthrow uncaughtError instanceof Error\n\t\t\t\t? uncaughtError\n\t\t\t\t: // eslint-disable-next-line @typescript-eslint/no-base-to-string\n\t\t\t\t\tnew Error(String(uncaughtError));\n\t\t}\n\n\t\t// The renderer appends a trailing newline to static output for terminal\n\t\t// rendering (so dynamic output starts on a fresh line). Strip it here\n\t\t// so renderToString returns clean output.\n\t\tconst normalizedStaticOutput = capturedStaticOutput.endsWith('\\n')\n\t\t\t? capturedStaticOutput.slice(0, -1)\n\t\t\t: capturedStaticOutput;\n\n\t\tif (normalizedStaticOutput && output) {\n\t\t\treturn normalizedStaticOutput + '\\n' + output;\n\t\t}\n\n\t\treturn normalizedStaticOutput || output;\n\t} finally {\n\t\t// Ensure native Yoga memory is freed even if rendering or teardown threw.\n\t\t// Yoga nodes are WASM-backed and not garbage collected.\n\t\tif (!teardownSucceeded && rootNode.yogaNode) {\n\t\t\ttry {\n\t\t\t\t// If reconciler teardown failed, some child nodes may not have been\n\t\t\t\t// freed. Use freeRecursive to clean up the entire tree as best-effort.\n\t\t\t\trootNode.yogaNode.freeRecursive();\n\t\t\t} catch {\n\t\t\t\t// Best-effort: node may already be partially freed\n\t\t\t}\n\t\t}\n\t}\n};\n\nexport default renderToString;\n"
  },
  {
    "path": "src/render.ts",
    "content": "import {Stream} from 'node:stream';\nimport process from 'node:process';\nimport type {ReactNode} from 'react';\nimport Ink, {type Options as InkOptions, type RenderMetrics} from './ink.js';\nimport instances from './instances.js';\nimport {type KittyKeyboardOptions} from './kitty-keyboard.js';\n\nexport type RenderOptions = {\n\t/**\n\tOutput stream where the app will be rendered.\n\n\t@default process.stdout\n\t*/\n\tstdout?: NodeJS.WriteStream;\n\n\t/**\n\tInput stream where app will listen for input.\n\n\t@default process.stdin\n\t*/\n\tstdin?: NodeJS.ReadStream;\n\n\t/**\n\tError stream.\n\t@default process.stderr\n\t*/\n\tstderr?: NodeJS.WriteStream;\n\n\t/**\n\tIf true, each update will be rendered as separate output, without replacing the previous one.\n\n\t@default false\n\t*/\n\tdebug?: boolean;\n\n\t/**\n\tConfigure whether Ink should listen for Ctrl+C keyboard input and exit the app. This is needed in case `process.stdin` is in raw mode, because then Ctrl+C is ignored by default and the process is expected to handle it manually.\n\n\t@default true\n\t*/\n\texitOnCtrlC?: boolean;\n\n\t/**\n\tPatch console methods to ensure console output doesn't mix with Ink's output.\n\n\tNote: 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.\n\n\t@default true\n\t*/\n\tpatchConsole?: boolean;\n\n\t/**\n\tRuns the given callback after each render and re-render with render metrics.\n\n\tNote: this callback runs after Ink commits a frame, but it does not wait for `stdout`/`stderr` stream callbacks.\n\tTo run code after output is flushed, use `waitUntilRenderFlush()`.\n\t*/\n\tonRender?: (metrics: RenderMetrics) => void;\n\n\t/**\n\tEnable screen reader support. See https://github.com/vadimdemedes/ink/blob/master/readme.md#screen-reader-support\n\n\t@default process.env['INK_SCREEN_READER'] === 'true'\n\t*/\n\tisScreenReaderEnabled?: boolean;\n\n\t/**\n\tMaximum frames per second for render updates.\n\tThis controls how frequently the UI can update to prevent excessive re-rendering.\n\tHigher values allow more frequent updates but may impact performance.\n\n\t@default 30\n\t*/\n\tmaxFps?: number;\n\n\t/**\n\tEnable incremental rendering mode which only updates changed lines instead of redrawing the entire output.\n\tThis can reduce flickering and improve performance for frequently updating UIs.\n\n\t@default false\n\t*/\n\tincrementalRendering?: boolean;\n\n\t/**\n\tEnable React Concurrent Rendering mode.\n\n\tWhen enabled:\n\t- Suspense boundaries work correctly with async data\n\t- `useTransition` and `useDeferredValue` are fully functional\n\t- Updates can be interrupted for higher priority work\n\n\tNote: 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.\n\n\t@default false\n\t*/\n\tconcurrent?: boolean;\n\n\t/**\n\tConfigure kitty keyboard protocol support for enhanced keyboard input.\n\tEnables additional modifiers (super, hyper, capsLock, numLock) and\n\tdisambiguated key events in terminals that support the protocol.\n\n\t@see https://sw.kovidgoyal.net/kitty/keyboard-protocol/\n\t*/\n\tkittyKeyboard?: KittyKeyboardOptions;\n\n\t/**\n\tOverride automatic interactive mode detection.\n\n\tBy default, Ink detects whether the environment is interactive based on CI detection (via [`is-in-ci`](https://github.com/sindresorhus/is-in-ci)) and `stdout.isTTY`. Most users should not need to set this.\n\n\tWhen non-interactive, Ink disables ANSI erase sequences, cursor manipulation, synchronized output, resize handling, and kitty keyboard auto-detection, writing only the final frame at unmount.\n\n\tSet to `false` to force non-interactive mode or `true` to force interactive mode when the automatic detection doesn't suit your use case.\n\n\tNote: 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.\n\n\t@default true (false if in CI or `stdout.isTTY` is falsy)\n\t*/\n\tinteractive?: boolean;\n\n\t/**\n\tRender 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.\n\n\tNote: 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.\n\n\tNote: 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.\n\n\tOnly works in interactive mode. Ignored when `interactive` is `false` or in a non-interactive environment (CI, piped stdout).\n\n\tNote: 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.\n\n\t@default false\n\t*/\n\talternateScreen?: boolean;\n};\n\nexport type Instance = {\n\t/**\n\tReplace the previous root node with a new one or update props of the current root node.\n\t*/\n\trerender: Ink['render'];\n\n\t/**\n\tManually unmount the whole Ink app.\n\t*/\n\tunmount: Ink['unmount'];\n\n\t/**\n\tReturns a promise that settles when the app is unmounted.\n\n\tIt resolves with the value passed to `exit(value)` and rejects with the error passed to `exit(error)`.\n\tWhen `unmount()` is called manually, it settles after unmount-related stdout writes complete.\n\n\t@example\n\t```jsx\n\tconst {unmount, waitUntilExit} = render(<MyApp />);\n\n\tsetTimeout(unmount, 1000);\n\n\tawait waitUntilExit(); // resolves after `unmount()` is called\n\t```\n\t*/\n\twaitUntilExit: Ink['waitUntilExit'];\n\n\t/**\n\tReturns a promise that settles after pending render output is flushed to stdout.\n\n\tThis can be used after `rerender()` when you need to run code only after the frame is written.\n\n\t@example\n\t```jsx\n\tconst {rerender, waitUntilRenderFlush} = render(<MyApp step=\"loading\" />);\n\n\trerender(<MyApp step=\"ready\" />);\n\tawait waitUntilRenderFlush(); // output for \"ready\" is flushed\n\n\trunNextCommand();\n\t```\n\t*/\n\twaitUntilRenderFlush: Ink['waitUntilRenderFlush'];\n\n\t/**\n\tUnmount the current app and remove the internal Ink instance for this stdout.\n\n\tThis is mostly useful for advanced cases where you need `render()` to create a fresh instance for the same stream without leaving terminal state such as the alternate screen behind.\n\t*/\n\tcleanup: () => void;\n\n\t/**\n\tClear output.\n\t*/\n\tclear: () => void;\n};\n\n/**\nMount a component and render the output.\n*/\nconst render = (\n\tnode: ReactNode,\n\toptions?: NodeJS.WriteStream | RenderOptions,\n): Instance => {\n\tconst inkOptions: InkOptions = {\n\t\tstdout: process.stdout,\n\t\tstdin: process.stdin,\n\t\tstderr: process.stderr,\n\t\tdebug: false,\n\t\texitOnCtrlC: true,\n\t\tpatchConsole: true,\n\t\tmaxFps: 30,\n\t\tincrementalRendering: false,\n\t\tconcurrent: false,\n\t\talternateScreen: false,\n\t\t...getOptions(options),\n\t};\n\n\tconst instance: Ink = getInstance(\n\t\tinkOptions.stdout,\n\t\t() => new Ink(inkOptions),\n\t);\n\tinstance.render(node);\n\n\treturn {\n\t\trerender: instance.render,\n\t\tunmount() {\n\t\t\tinstance.unmount();\n\t\t},\n\t\twaitUntilExit: instance.waitUntilExit,\n\t\twaitUntilRenderFlush: instance.waitUntilRenderFlush,\n\t\tcleanup() {\n\t\t\tinstance.unmount();\n\t\t},\n\t\tclear: instance.clear,\n\t};\n};\n\nexport default render;\n\nconst getOptions = (\n\tstdout: NodeJS.WriteStream | RenderOptions | undefined = {},\n): RenderOptions => {\n\tif (stdout instanceof Stream) {\n\t\treturn {\n\t\t\tstdout,\n\t\t\tstdin: process.stdin,\n\t\t};\n\t}\n\n\treturn stdout;\n};\n\nconst getInstance = (\n\tstdout: NodeJS.WriteStream,\n\tcreateInstance: () => Ink,\n): Ink => {\n\tconst instance = instances.get(stdout);\n\n\tif (instance === undefined) {\n\t\tconst newInstance = createInstance();\n\t\tinstances.set(stdout, newInstance);\n\t\treturn newInstance;\n\t}\n\n\t// Ink keeps one live renderer per stdout. Reusing the same stream without\n\t// unmounting is unsupported, but return the existing instance so we don't\n\t// create two renderers that compete for the same output. Write the warning\n\t// directly to native stderr so an existing alternate-screen renderer cannot\n\t// swallow it via patchConsole.\n\tprocess.stderr.write(\n\t\t'Warning: render() was called again for the same stdout before the previous Ink instance was unmounted. Reusing stdout across multiple render() calls is unsupported. Call unmount() first.\\n',\n\t);\n\n\treturn instance;\n};\n"
  },
  {
    "path": "src/renderer.ts",
    "content": "import renderNodeToOutput, {\n\trenderNodeToScreenReaderOutput,\n} from './render-node-to-output.js';\nimport Output from './output.js';\nimport {type DOMElement} from './dom.js';\n\ntype Result = {\n\toutput: string;\n\toutputHeight: number;\n\tstaticOutput: string;\n};\n\nconst renderer = (node: DOMElement, isScreenReaderEnabled: boolean): Result => {\n\tif (node.yogaNode) {\n\t\tif (isScreenReaderEnabled) {\n\t\t\tconst output = renderNodeToScreenReaderOutput(node, {\n\t\t\t\tskipStaticElements: true,\n\t\t\t});\n\n\t\t\tconst outputHeight = output === '' ? 0 : output.split('\\n').length;\n\n\t\t\tlet staticOutput = '';\n\n\t\t\tif (node.staticNode) {\n\t\t\t\tstaticOutput = renderNodeToScreenReaderOutput(node.staticNode, {\n\t\t\t\t\tskipStaticElements: false,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\toutput,\n\t\t\t\toutputHeight,\n\t\t\t\tstaticOutput: staticOutput ? `${staticOutput}\\n` : '',\n\t\t\t};\n\t\t}\n\n\t\tconst output = new Output({\n\t\t\twidth: node.yogaNode.getComputedWidth(),\n\t\t\theight: node.yogaNode.getComputedHeight(),\n\t\t});\n\n\t\trenderNodeToOutput(node, output, {\n\t\t\tskipStaticElements: true,\n\t\t});\n\n\t\tlet staticOutput;\n\n\t\tif (node.staticNode?.yogaNode) {\n\t\t\tstaticOutput = new Output({\n\t\t\t\twidth: node.staticNode.yogaNode.getComputedWidth(),\n\t\t\t\theight: node.staticNode.yogaNode.getComputedHeight(),\n\t\t\t});\n\n\t\t\trenderNodeToOutput(node.staticNode, staticOutput, {\n\t\t\t\tskipStaticElements: false,\n\t\t\t});\n\t\t}\n\n\t\tconst {output: generatedOutput, height: outputHeight} = output.get();\n\n\t\treturn {\n\t\t\toutput: generatedOutput,\n\t\t\toutputHeight,\n\t\t\t// Newline at the end is needed, because static output doesn't have one, so\n\t\t\t// interactive output will override last line of static output\n\t\t\tstaticOutput: staticOutput ? `${staticOutput.get().output}\\n` : '',\n\t\t};\n\t}\n\n\treturn {\n\t\toutput: '',\n\t\toutputHeight: 0,\n\t\tstaticOutput: '',\n\t};\n};\n\nexport default renderer;\n"
  },
  {
    "path": "src/sanitize-ansi.ts",
    "content": "import {hasAnsiControlCharacters, tokenizeAnsi} from './ansi-tokenizer.js';\n\nconst sgrParametersRegex = /^[\\d:;]*$/;\n\n// Strip ANSI escape sequences that would conflict with Ink's layout.\n// Preserved: SGR sequences (colors, bold, etc. - end with 'm') and\n// OSC sequences (hyperlinks, etc. - ESC ] or C1 OSC).\n// Stripped: cursor movement, screen clearing, and other control sequences.\nconst sanitizeAnsi = (text: string): string => {\n\tif (!hasAnsiControlCharacters(text)) {\n\t\treturn text;\n\t}\n\n\tlet output = '';\n\n\tfor (const token of tokenizeAnsi(text)) {\n\t\tif (token.type === 'text' || token.type === 'osc') {\n\t\t\toutput += token.value;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (\n\t\t\ttoken.type === 'csi' &&\n\t\t\ttoken.finalCharacter === 'm' &&\n\t\t\ttoken.intermediateString === '' &&\n\t\t\tsgrParametersRegex.test(token.parameterString)\n\t\t) {\n\t\t\toutput += token.value;\n\t\t}\n\t}\n\n\treturn output;\n};\n\nexport default sanitizeAnsi;\n"
  },
  {
    "path": "src/squash-text-nodes.ts",
    "content": "import {type DOMElement} from './dom.js';\nimport sanitizeAnsi from './sanitize-ansi.js';\n\n// Squashing text nodes allows to combine multiple text nodes into one and write\n// to `Output` instance only once. For example, <Text>hello{' '}world</Text>\n// is actually 3 text nodes, which would result 3 writes to `Output`.\n//\n// Also, this is necessary for libraries like ink-link (https://github.com/sindresorhus/ink-link),\n// which need to wrap all children at once, instead of wrapping 3 text nodes separately.\nconst squashTextNodes = (node: DOMElement): string => {\n\tlet text = '';\n\n\tfor (let index = 0; index < node.childNodes.length; index++) {\n\t\tconst childNode = node.childNodes[index];\n\n\t\tif (childNode === undefined) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet nodeText = '';\n\n\t\tif (childNode.nodeName === '#text') {\n\t\t\tnodeText = childNode.nodeValue;\n\t\t} else {\n\t\t\tif (\n\t\t\t\tchildNode.nodeName === 'ink-text' ||\n\t\t\t\tchildNode.nodeName === 'ink-virtual-text'\n\t\t\t) {\n\t\t\t\tnodeText = squashTextNodes(childNode);\n\t\t\t}\n\n\t\t\t// Since these text nodes are being concatenated, `Output` instance won't be able to\n\t\t\t// apply children transform, so we have to do it manually here for each text node\n\t\t\tif (\n\t\t\t\tnodeText.length > 0 &&\n\t\t\t\ttypeof childNode.internal_transform === 'function'\n\t\t\t) {\n\t\t\t\tnodeText = childNode.internal_transform(nodeText, index);\n\t\t\t}\n\t\t}\n\n\t\ttext += nodeText;\n\t}\n\n\treturn sanitizeAnsi(text);\n};\n\nexport default squashTextNodes;\n"
  },
  {
    "path": "src/styles.ts",
    "content": "/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */\nimport {type Boxes, type BoxStyle} from 'cli-boxes';\nimport {type LiteralUnion} from 'type-fest';\nimport {type ForegroundColorName} from 'ansi-styles'; // Note: We import directly from `ansi-styles` to avoid a bug in TypeScript.\nimport Yoga, {type Node as YogaNode} from 'yoga-layout';\n\nexport type Styles = {\n\treadonly textWrap?:\n\t\t| 'wrap'\n\t\t| 'end'\n\t\t| 'middle'\n\t\t| 'truncate-end'\n\t\t| 'truncate'\n\t\t| 'truncate-middle'\n\t\t| 'truncate-start';\n\n\t/**\n\tControls how the element is positioned.\n\n\tWhen `position` is `static`, `top`, `right`, `bottom`, and `left` are ignored.\n\t*/\n\treadonly position?: 'absolute' | 'relative' | 'static';\n\n\t/**\n\tTop offset for positioned elements.\n\t*/\n\treadonly top?: number | string;\n\n\t/**\n\tRight offset for positioned elements.\n\t*/\n\treadonly right?: number | string;\n\n\t/**\n\tBottom offset for positioned elements.\n\t*/\n\treadonly bottom?: number | string;\n\n\t/**\n\tLeft offset for positioned elements.\n\t*/\n\treadonly left?: number | string;\n\n\t/**\n\tSize of the gap between an element's columns.\n\t*/\n\treadonly columnGap?: number;\n\n\t/**\n\tSize of the gap between an element's rows.\n\t*/\n\treadonly rowGap?: number;\n\n\t/**\n\tSize of the gap between an element's columns and rows. A shorthand for `columnGap` and `rowGap`.\n\t*/\n\treadonly gap?: number;\n\n\t/**\n\tMargin on all sides. Equivalent to setting `marginTop`, `marginBottom`, `marginLeft`, and `marginRight`.\n\t*/\n\treadonly margin?: number;\n\n\t/**\n\tHorizontal margin. Equivalent to setting `marginLeft` and `marginRight`.\n\t*/\n\treadonly marginX?: number;\n\n\t/**\n\tVertical margin. Equivalent to setting `marginTop` and `marginBottom`.\n\t*/\n\treadonly marginY?: number;\n\n\t/**\n\tTop margin.\n\t*/\n\treadonly marginTop?: number;\n\n\t/**\n\tBottom margin.\n\t*/\n\treadonly marginBottom?: number;\n\n\t/**\n\tLeft margin.\n\t*/\n\treadonly marginLeft?: number;\n\n\t/**\n\tRight margin.\n\t*/\n\treadonly marginRight?: number;\n\n\t/**\n\tPadding on all sides. Equivalent to setting `paddingTop`, `paddingBottom`, `paddingLeft`, and `paddingRight`.\n\t*/\n\treadonly padding?: number;\n\n\t/**\n\tHorizontal padding. Equivalent to setting `paddingLeft` and `paddingRight`.\n\t*/\n\treadonly paddingX?: number;\n\n\t/**\n\tVertical padding. Equivalent to setting `paddingTop` and `paddingBottom`.\n\t*/\n\treadonly paddingY?: number;\n\n\t/**\n\tTop padding.\n\t*/\n\treadonly paddingTop?: number;\n\n\t/**\n\tBottom padding.\n\t*/\n\treadonly paddingBottom?: number;\n\n\t/**\n\tLeft padding.\n\t*/\n\treadonly paddingLeft?: number;\n\n\t/**\n\tRight padding.\n\t*/\n\treadonly paddingRight?: number;\n\n\t/**\n\tThis property defines the ability for a flex item to grow if necessary.\n\tSee [flex-grow](https://css-tricks.com/almanac/properties/f/flex-grow/).\n\t*/\n\treadonly flexGrow?: number;\n\n\t/**\n\tIt specifies the “flex shrink factor”, which determines how much the flex item will shrink relative to the rest of the flex items in the flex container when there isn’t enough space on the row.\n\tSee [flex-shrink](https://css-tricks.com/almanac/properties/f/flex-shrink/).\n\t*/\n\treadonly flexShrink?: number;\n\n\t/**\n\tIt establishes the main-axis, thus defining the direction flex items are placed in the flex container.\n\tSee [flex-direction](https://css-tricks.com/almanac/properties/f/flex-direction/).\n\t*/\n\treadonly flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';\n\n\t/**\n\tIt specifies the initial size of the flex item, before any available space is distributed according to the flex factors.\n\tSee [flex-basis](https://css-tricks.com/almanac/properties/f/flex-basis/).\n\t*/\n\treadonly flexBasis?: number | string;\n\n\t/**\n\tIt defines whether the flex items are forced in a single line or can be flowed into multiple lines. If set to multiple lines, it also defines the cross-axis which determines the direction new lines are stacked in.\n\tSee [flex-wrap](https://css-tricks.com/almanac/properties/f/flex-wrap/).\n\t*/\n\treadonly flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse';\n\n\t/**\n\tThe align-items property defines the default behavior for how items are laid out along the cross axis (perpendicular to the main axis).\n\tSee [align-items](https://css-tricks.com/almanac/properties/a/align-items/).\n\t*/\n\treadonly alignItems?:\n\t\t| 'flex-start'\n\t\t| 'center'\n\t\t| 'flex-end'\n\t\t| 'stretch'\n\t\t| 'baseline';\n\n\t/**\n\tIt makes possible to override the align-items value for specific flex items.\n\tSee [align-self](https://css-tricks.com/almanac/properties/a/align-self/).\n\t*/\n\treadonly alignSelf?:\n\t\t| 'flex-start'\n\t\t| 'center'\n\t\t| 'flex-end'\n\t\t| 'auto'\n\t\t| 'stretch'\n\t\t| 'baseline';\n\n\t/**\n\tIt defines the alignment along the cross axis when there are multiple lines of flex items (when using flex-wrap).\n\tSee [align-content](https://css-tricks.com/almanac/properties/a/align-content/).\n\t*/\n\treadonly alignContent?:\n\t\t| 'flex-start'\n\t\t| 'flex-end'\n\t\t| 'center'\n\t\t| 'stretch'\n\t\t| 'space-between'\n\t\t| 'space-around'\n\t\t| 'space-evenly';\n\n\t/**\n\tIt defines the alignment along the main axis.\n\tSee [justify-content](https://css-tricks.com/almanac/properties/j/justify-content/).\n\t*/\n\treadonly justifyContent?:\n\t\t| 'flex-start'\n\t\t| 'flex-end'\n\t\t| 'space-between'\n\t\t| 'space-around'\n\t\t| 'space-evenly'\n\t\t| 'center';\n\n\t/**\n\tWidth 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.\n\t*/\n\treadonly width?: number | string;\n\n\t/**\n\tHeight 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.\n\t*/\n\treadonly height?: number | string;\n\n\t/**\n\tSets a minimum width of the element.\n\tPercentages aren't supported yet; see https://github.com/facebook/yoga/issues/872.\n\t*/\n\treadonly minWidth?: number | string;\n\n\t/**\n\tSets 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.\n\t*/\n\treadonly minHeight?: number | string;\n\n\t/**\n\tSets a maximum width of the element.\n\tPercentages aren't supported yet; see https://github.com/facebook/yoga/issues/872.\n\t*/\n\treadonly maxWidth?: number | string;\n\n\t/**\n\tSets 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.\n\t*/\n\treadonly maxHeight?: number | string;\n\n\t/**\n\tDefines the aspect ratio (width/height) for the element.\n\n\tUse it with at least one size constraint (`width`, `height`, `minHeight`, or `maxHeight`) so Ink can derive the missing dimension.\n\t*/\n\treadonly aspectRatio?: number;\n\n\t/**\n\tSet this property to `none` to hide the element.\n\t*/\n\treadonly display?: 'flex' | 'none';\n\n\t/**\n\tAdd a border with a specified style. If `borderStyle` is `undefined` (the default), no border will be added.\n\t*/\n\treadonly borderStyle?: keyof Boxes | BoxStyle;\n\n\t/**\n\tDetermines whether the top border is visible.\n\n\t@default true\n\t*/\n\treadonly borderTop?: boolean;\n\n\t/**\n\tDetermines whether the bottom border is visible.\n\n\t@default true\n\t*/\n\treadonly borderBottom?: boolean;\n\n\t/**\n\tDetermines whether the left border is visible.\n\n\t@default true\n\t*/\n\treadonly borderLeft?: boolean;\n\n\t/**\n\tDetermines whether the right border is visible.\n\n\t@default true\n\t*/\n\treadonly borderRight?: boolean;\n\n\t/**\n\tChange border color. A shorthand for setting `borderTopColor`, `borderRightColor`, `borderBottomColor`, and `borderLeftColor`.\n\t*/\n\treadonly borderColor?: LiteralUnion<ForegroundColorName, string>;\n\n\t/**\n\tChange the top border color. Accepts the same values as `color` in `Text` component.\n\t*/\n\treadonly borderTopColor?: LiteralUnion<ForegroundColorName, string>;\n\n\t/**\n\tChange the bottom border color. Accepts the same values as `color` in `Text` component.\n\t*/\n\treadonly borderBottomColor?: LiteralUnion<ForegroundColorName, string>;\n\n\t/**\n\tChange the left border color. Accepts the same values as `color` in `Text` component.\n\t*/\n\treadonly borderLeftColor?: LiteralUnion<ForegroundColorName, string>;\n\n\t/**\n\tChange the right border color. Accepts the same values as `color` in `Text` component.\n\t*/\n\treadonly borderRightColor?: LiteralUnion<ForegroundColorName, string>;\n\n\t/**\n\tDim the border color. A shorthand for setting `borderTopDimColor`, `borderBottomDimColor`, `borderLeftDimColor`, and `borderRightDimColor`.\n\n\t@default false\n\t*/\n\treadonly borderDimColor?: boolean;\n\n\t/**\n\tDim the top border color.\n\n\t@default false\n\t*/\n\treadonly borderTopDimColor?: boolean;\n\n\t/**\n\tDim the bottom border color.\n\n\t@default false\n\t*/\n\treadonly borderBottomDimColor?: boolean;\n\n\t/**\n\tDim the left border color.\n\n\t@default false\n\t*/\n\treadonly borderLeftDimColor?: boolean;\n\n\t/**\n\tDim the right border color.\n\n\t@default false\n\t*/\n\treadonly borderRightDimColor?: boolean;\n\n\t/**\n\tBehavior for an element's overflow in both directions.\n\n\t@default 'visible'\n\t*/\n\treadonly overflow?: 'visible' | 'hidden';\n\n\t/**\n\tBehavior for an element's overflow in the horizontal direction.\n\n\t@default 'visible'\n\t*/\n\treadonly overflowX?: 'visible' | 'hidden';\n\n\t/**\n\tBehavior for an element's overflow in the vertical direction.\n\n\t@default 'visible'\n\t*/\n\treadonly overflowY?: 'visible' | 'hidden';\n\n\t/**\n\tBackground color for the element.\n\n\tAccepts the same values as `color` in the `<Text>` component.\n\t*/\n\treadonly backgroundColor?: LiteralUnion<ForegroundColorName, string>;\n};\n\nconst positionEdges = [\n\t['top', Yoga.EDGE_TOP],\n\t['right', Yoga.EDGE_RIGHT],\n\t['bottom', Yoga.EDGE_BOTTOM],\n\t['left', Yoga.EDGE_LEFT],\n] as const;\n\nconst applyPositionStyles = (node: YogaNode, style: Styles): void => {\n\tif ('position' in style) {\n\t\tlet positionType = Yoga.POSITION_TYPE_RELATIVE;\n\n\t\tif (style.position === 'absolute') {\n\t\t\tpositionType = Yoga.POSITION_TYPE_ABSOLUTE;\n\t\t} else if (style.position === 'static') {\n\t\t\tpositionType = Yoga.POSITION_TYPE_STATIC;\n\t\t}\n\n\t\tnode.setPositionType(positionType);\n\t}\n\n\tfor (const [property, edge] of positionEdges) {\n\t\tif (!(property in style)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst value = style[property];\n\n\t\tif (typeof value === 'string') {\n\t\t\tnode.setPositionPercent(edge, Number.parseFloat(value));\n\t\t\tcontinue;\n\t\t}\n\n\t\tnode.setPosition(edge, value);\n\t}\n};\n\nconst applyMarginStyles = (node: YogaNode, style: Styles): void => {\n\tif ('margin' in style) {\n\t\tnode.setMargin(Yoga.EDGE_ALL, style.margin ?? 0);\n\t}\n\n\tif ('marginX' in style) {\n\t\tnode.setMargin(Yoga.EDGE_HORIZONTAL, style.marginX ?? 0);\n\t}\n\n\tif ('marginY' in style) {\n\t\tnode.setMargin(Yoga.EDGE_VERTICAL, style.marginY ?? 0);\n\t}\n\n\tif ('marginLeft' in style) {\n\t\tnode.setMargin(Yoga.EDGE_START, style.marginLeft || 0);\n\t}\n\n\tif ('marginRight' in style) {\n\t\tnode.setMargin(Yoga.EDGE_END, style.marginRight || 0);\n\t}\n\n\tif ('marginTop' in style) {\n\t\tnode.setMargin(Yoga.EDGE_TOP, style.marginTop || 0);\n\t}\n\n\tif ('marginBottom' in style) {\n\t\tnode.setMargin(Yoga.EDGE_BOTTOM, style.marginBottom || 0);\n\t}\n};\n\nconst applyPaddingStyles = (node: YogaNode, style: Styles): void => {\n\tif ('padding' in style) {\n\t\tnode.setPadding(Yoga.EDGE_ALL, style.padding ?? 0);\n\t}\n\n\tif ('paddingX' in style) {\n\t\tnode.setPadding(Yoga.EDGE_HORIZONTAL, style.paddingX ?? 0);\n\t}\n\n\tif ('paddingY' in style) {\n\t\tnode.setPadding(Yoga.EDGE_VERTICAL, style.paddingY ?? 0);\n\t}\n\n\tif ('paddingLeft' in style) {\n\t\tnode.setPadding(Yoga.EDGE_LEFT, style.paddingLeft || 0);\n\t}\n\n\tif ('paddingRight' in style) {\n\t\tnode.setPadding(Yoga.EDGE_RIGHT, style.paddingRight || 0);\n\t}\n\n\tif ('paddingTop' in style) {\n\t\tnode.setPadding(Yoga.EDGE_TOP, style.paddingTop || 0);\n\t}\n\n\tif ('paddingBottom' in style) {\n\t\tnode.setPadding(Yoga.EDGE_BOTTOM, style.paddingBottom || 0);\n\t}\n};\n\nconst applyFlexStyles = (node: YogaNode, style: Styles): void => {\n\tif ('flexGrow' in style) {\n\t\tnode.setFlexGrow(style.flexGrow ?? 0);\n\t}\n\n\tif ('flexShrink' in style) {\n\t\tnode.setFlexShrink(\n\t\t\ttypeof style.flexShrink === 'number' ? style.flexShrink : 1,\n\t\t);\n\t}\n\n\tif ('flexWrap' in style) {\n\t\tif (style.flexWrap === 'nowrap') {\n\t\t\tnode.setFlexWrap(Yoga.WRAP_NO_WRAP);\n\t\t}\n\n\t\tif (style.flexWrap === 'wrap') {\n\t\t\tnode.setFlexWrap(Yoga.WRAP_WRAP);\n\t\t}\n\n\t\tif (style.flexWrap === 'wrap-reverse') {\n\t\t\tnode.setFlexWrap(Yoga.WRAP_WRAP_REVERSE);\n\t\t}\n\t}\n\n\tif ('flexDirection' in style) {\n\t\tif (style.flexDirection === 'row') {\n\t\t\tnode.setFlexDirection(Yoga.FLEX_DIRECTION_ROW);\n\t\t}\n\n\t\tif (style.flexDirection === 'row-reverse') {\n\t\t\tnode.setFlexDirection(Yoga.FLEX_DIRECTION_ROW_REVERSE);\n\t\t}\n\n\t\tif (style.flexDirection === 'column') {\n\t\t\tnode.setFlexDirection(Yoga.FLEX_DIRECTION_COLUMN);\n\t\t}\n\n\t\tif (style.flexDirection === 'column-reverse') {\n\t\t\tnode.setFlexDirection(Yoga.FLEX_DIRECTION_COLUMN_REVERSE);\n\t\t}\n\t}\n\n\tif ('flexBasis' in style) {\n\t\tif (typeof style.flexBasis === 'number') {\n\t\t\tnode.setFlexBasis(style.flexBasis);\n\t\t} else if (typeof style.flexBasis === 'string') {\n\t\t\tnode.setFlexBasisPercent(Number.parseInt(style.flexBasis, 10));\n\t\t} else {\n\t\t\t// This should be replaced with node.setFlexBasisAuto() when new Yoga release is out\n\t\t\tnode.setFlexBasis(Number.NaN);\n\t\t}\n\t}\n\n\tif ('alignItems' in style) {\n\t\tif (style.alignItems === 'stretch' || !style.alignItems) {\n\t\t\tnode.setAlignItems(Yoga.ALIGN_STRETCH);\n\t\t}\n\n\t\tif (style.alignItems === 'flex-start') {\n\t\t\tnode.setAlignItems(Yoga.ALIGN_FLEX_START);\n\t\t}\n\n\t\tif (style.alignItems === 'center') {\n\t\t\tnode.setAlignItems(Yoga.ALIGN_CENTER);\n\t\t}\n\n\t\tif (style.alignItems === 'flex-end') {\n\t\t\tnode.setAlignItems(Yoga.ALIGN_FLEX_END);\n\t\t}\n\n\t\tif (style.alignItems === 'baseline') {\n\t\t\tnode.setAlignItems(Yoga.ALIGN_BASELINE);\n\t\t}\n\t}\n\n\tif ('alignSelf' in style) {\n\t\tif (style.alignSelf === 'auto' || !style.alignSelf) {\n\t\t\tnode.setAlignSelf(Yoga.ALIGN_AUTO);\n\t\t}\n\n\t\tif (style.alignSelf === 'flex-start') {\n\t\t\tnode.setAlignSelf(Yoga.ALIGN_FLEX_START);\n\t\t}\n\n\t\tif (style.alignSelf === 'center') {\n\t\t\tnode.setAlignSelf(Yoga.ALIGN_CENTER);\n\t\t}\n\n\t\tif (style.alignSelf === 'flex-end') {\n\t\t\tnode.setAlignSelf(Yoga.ALIGN_FLEX_END);\n\t\t}\n\n\t\tif (style.alignSelf === 'stretch') {\n\t\t\tnode.setAlignSelf(Yoga.ALIGN_STRETCH);\n\t\t}\n\n\t\tif (style.alignSelf === 'baseline') {\n\t\t\tnode.setAlignSelf(Yoga.ALIGN_BASELINE);\n\t\t}\n\t}\n\n\tif ('alignContent' in style) {\n\t\t// Keep wrapped lines top-packed by default; stretch can add surprising empty rows in fixed-height boxes.\n\t\tif (style.alignContent === 'flex-start' || !style.alignContent) {\n\t\t\tnode.setAlignContent(Yoga.ALIGN_FLEX_START);\n\t\t}\n\n\t\tif (style.alignContent === 'center') {\n\t\t\tnode.setAlignContent(Yoga.ALIGN_CENTER);\n\t\t}\n\n\t\tif (style.alignContent === 'flex-end') {\n\t\t\tnode.setAlignContent(Yoga.ALIGN_FLEX_END);\n\t\t}\n\n\t\tif (style.alignContent === 'space-between') {\n\t\t\tnode.setAlignContent(Yoga.ALIGN_SPACE_BETWEEN);\n\t\t}\n\n\t\tif (style.alignContent === 'space-around') {\n\t\t\tnode.setAlignContent(Yoga.ALIGN_SPACE_AROUND);\n\t\t}\n\n\t\tif (style.alignContent === 'space-evenly') {\n\t\t\tnode.setAlignContent(Yoga.ALIGN_SPACE_EVENLY);\n\t\t}\n\n\t\tif (style.alignContent === 'stretch') {\n\t\t\tnode.setAlignContent(Yoga.ALIGN_STRETCH);\n\t\t}\n\t}\n\n\tif ('justifyContent' in style) {\n\t\tif (style.justifyContent === 'flex-start' || !style.justifyContent) {\n\t\t\tnode.setJustifyContent(Yoga.JUSTIFY_FLEX_START);\n\t\t}\n\n\t\tif (style.justifyContent === 'center') {\n\t\t\tnode.setJustifyContent(Yoga.JUSTIFY_CENTER);\n\t\t}\n\n\t\tif (style.justifyContent === 'flex-end') {\n\t\t\tnode.setJustifyContent(Yoga.JUSTIFY_FLEX_END);\n\t\t}\n\n\t\tif (style.justifyContent === 'space-between') {\n\t\t\tnode.setJustifyContent(Yoga.JUSTIFY_SPACE_BETWEEN);\n\t\t}\n\n\t\tif (style.justifyContent === 'space-around') {\n\t\t\tnode.setJustifyContent(Yoga.JUSTIFY_SPACE_AROUND);\n\t\t}\n\n\t\tif (style.justifyContent === 'space-evenly') {\n\t\t\tnode.setJustifyContent(Yoga.JUSTIFY_SPACE_EVENLY);\n\t\t}\n\t}\n};\n\nconst applyDimensionStyles = (node: YogaNode, style: Styles): void => {\n\tif ('width' in style) {\n\t\tif (typeof style.width === 'number') {\n\t\t\tnode.setWidth(style.width);\n\t\t} else if (typeof style.width === 'string') {\n\t\t\tnode.setWidthPercent(Number.parseInt(style.width, 10));\n\t\t} else {\n\t\t\tnode.setWidthAuto();\n\t\t}\n\t}\n\n\tif ('height' in style) {\n\t\tif (typeof style.height === 'number') {\n\t\t\tnode.setHeight(style.height);\n\t\t} else if (typeof style.height === 'string') {\n\t\t\tnode.setHeightPercent(Number.parseInt(style.height, 10));\n\t\t} else {\n\t\t\tnode.setHeightAuto();\n\t\t}\n\t}\n\n\tif ('minWidth' in style) {\n\t\tif (typeof style.minWidth === 'string') {\n\t\t\tnode.setMinWidthPercent(Number.parseInt(style.minWidth, 10));\n\t\t} else {\n\t\t\tnode.setMinWidth(style.minWidth ?? 0);\n\t\t}\n\t}\n\n\tif ('minHeight' in style) {\n\t\tif (typeof style.minHeight === 'string') {\n\t\t\tnode.setMinHeightPercent(Number.parseInt(style.minHeight, 10));\n\t\t} else {\n\t\t\tnode.setMinHeight(style.minHeight ?? 0);\n\t\t}\n\t}\n\n\tif ('maxWidth' in style) {\n\t\tif (typeof style.maxWidth === 'string') {\n\t\t\tnode.setMaxWidthPercent(Number.parseInt(style.maxWidth, 10));\n\t\t} else {\n\t\t\tnode.setMaxWidth(style.maxWidth);\n\t\t}\n\t}\n\n\tif ('maxHeight' in style) {\n\t\tif (typeof style.maxHeight === 'string') {\n\t\t\tnode.setMaxHeightPercent(Number.parseInt(style.maxHeight, 10));\n\t\t} else {\n\t\t\tnode.setMaxHeight(style.maxHeight);\n\t\t}\n\t}\n\n\tif ('aspectRatio' in style) {\n\t\tnode.setAspectRatio(style.aspectRatio);\n\t}\n};\n\nconst applyDisplayStyles = (node: YogaNode, style: Styles): void => {\n\tif ('display' in style) {\n\t\tnode.setDisplay(\n\t\t\tstyle.display === 'flex' ? Yoga.DISPLAY_FLEX : Yoga.DISPLAY_NONE,\n\t\t);\n\t}\n};\n\nconst applyBorderStyles = (\n\tnode: YogaNode,\n\tstyle: Styles,\n\tcurrentStyle: Styles,\n): void => {\n\tconst hasBorderChanges =\n\t\t'borderStyle' in style ||\n\t\t'borderTop' in style ||\n\t\t'borderBottom' in style ||\n\t\t'borderLeft' in style ||\n\t\t'borderRight' in style;\n\n\tif (!hasBorderChanges) {\n\t\treturn;\n\t}\n\n\tconst borderWidth = currentStyle.borderStyle ? 1 : 0;\n\n\tnode.setBorder(\n\t\tYoga.EDGE_TOP,\n\t\tcurrentStyle.borderTop === false ? 0 : borderWidth,\n\t);\n\tnode.setBorder(\n\t\tYoga.EDGE_BOTTOM,\n\t\tcurrentStyle.borderBottom === false ? 0 : borderWidth,\n\t);\n\tnode.setBorder(\n\t\tYoga.EDGE_LEFT,\n\t\tcurrentStyle.borderLeft === false ? 0 : borderWidth,\n\t);\n\tnode.setBorder(\n\t\tYoga.EDGE_RIGHT,\n\t\tcurrentStyle.borderRight === false ? 0 : borderWidth,\n\t);\n};\n\nconst applyGapStyles = (node: YogaNode, style: Styles): void => {\n\tif ('gap' in style) {\n\t\tnode.setGap(Yoga.GUTTER_ALL, style.gap ?? 0);\n\t}\n\n\tif ('columnGap' in style) {\n\t\tnode.setGap(Yoga.GUTTER_COLUMN, style.columnGap ?? 0);\n\t}\n\n\tif ('rowGap' in style) {\n\t\tnode.setGap(Yoga.GUTTER_ROW, style.rowGap ?? 0);\n\t}\n};\n\nconst styles = (\n\tnode: YogaNode,\n\tstyle: Styles = {},\n\tcurrentStyle: Styles = style,\n): void => {\n\tapplyPositionStyles(node, style);\n\tapplyMarginStyles(node, style);\n\tapplyPaddingStyles(node, style);\n\tapplyFlexStyles(node, style);\n\tapplyDimensionStyles(node, style);\n\tapplyDisplayStyles(node, style);\n\tapplyBorderStyles(node, style, currentStyle);\n\tapplyGapStyles(node, style);\n};\n\nexport default styles;\n"
  },
  {
    "path": "src/utils.ts",
    "content": "import terminalSize from 'terminal-size';\n\n/**\nGet the effective terminal dimensions from the given stdout stream.\n\nFalls back to `terminal-size` for columns in piped processes where `stdout.columns` is 0, and uses standard defaults (80×24) when dimensions cannot be determined.\n*/\nexport const getWindowSize = (\n\tstdout: NodeJS.WriteStream,\n): {columns: number; rows: number} => {\n\t// `stdout.columns`/`rows` can be 0 or undefined in non-TTY environments.\n\tconst {columns, rows} = stdout;\n\n\tif (columns && rows) {\n\t\treturn {columns, rows};\n\t}\n\n\tconst fallbackSize = terminalSize();\n\treturn {\n\t\tcolumns: columns || fallbackSize.columns || 80,\n\t\trows: rows || fallbackSize.rows || 24,\n\t};\n};\n"
  },
  {
    "path": "src/wrap-text.ts",
    "content": "import wrapAnsi from 'wrap-ansi';\nimport cliTruncate from 'cli-truncate';\nimport {type Styles} from './styles.js';\n\nconst cache: Record<string, string> = {};\n\nconst wrapText = (\n\ttext: string,\n\tmaxWidth: number,\n\twrapType: Styles['textWrap'],\n): string => {\n\tconst cacheKey = text + String(maxWidth) + String(wrapType);\n\tconst cachedText = cache[cacheKey];\n\n\tif (cachedText) {\n\t\treturn cachedText;\n\t}\n\n\tlet wrappedText = text;\n\n\tif (wrapType === 'wrap') {\n\t\twrappedText = wrapAnsi(text, maxWidth, {\n\t\t\ttrim: false,\n\t\t\thard: true,\n\t\t});\n\t}\n\n\tif (wrapType!.startsWith('truncate')) {\n\t\tlet position: 'end' | 'middle' | 'start' = 'end';\n\n\t\tif (wrapType === 'truncate-middle') {\n\t\t\tposition = 'middle';\n\t\t}\n\n\t\tif (wrapType === 'truncate-start') {\n\t\t\tposition = 'start';\n\t\t}\n\n\t\twrappedText = cliTruncate(text, maxWidth, {position});\n\t}\n\n\tcache[cacheKey] = wrappedText;\n\n\treturn wrappedText;\n};\n\nexport default wrapText;\n"
  },
  {
    "path": "src/write-synchronized.ts",
    "content": "import {type Writable} from 'node:stream';\nimport isInCi from 'is-in-ci';\n\nexport const bsu = '\\u001B[?2026h';\nexport const esu = '\\u001B[?2026l';\n\nexport function shouldSynchronize(\n\tstream: Writable,\n\tinteractive?: boolean,\n): boolean {\n\treturn (\n\t\t'isTTY' in stream &&\n\t\t(stream as Writable & {isTTY: boolean}).isTTY &&\n\t\t(interactive ?? !isInCi)\n\t);\n}\n"
  },
  {
    "path": "test/alternate-screen-example.tsx",
    "content": "import {spawn as spawnProcess} from 'node:child_process';\nimport * as path from 'node:path';\nimport url from 'node:url';\nimport test from 'ava';\nimport {gameReducer} from '../examples/alternate-screen/alternate-screen.js';\n\nconst __dirname = url.fileURLToPath(new URL('.', import.meta.url));\n\ntest('snake can move into the tail cell when the tail moves away', t => {\n\tconst state = {\n\t\tsnake: [\n\t\t\t{x: 2, y: 1},\n\t\t\t{x: 1, y: 1},\n\t\t\t{x: 1, y: 2},\n\t\t\t{x: 2, y: 2},\n\t\t],\n\t\tfood: {x: 0, y: 0},\n\t\tscore: 3,\n\t\tgameOver: false,\n\t\twon: false,\n\t\tframe: 10,\n\t};\n\n\tconst nextState = gameReducer(state, {\n\t\ttype: 'tick',\n\t\tdirection: 'down',\n\t});\n\n\tt.false(nextState.gameOver);\n\tt.deepEqual(nextState.snake, [\n\t\t{x: 2, y: 2},\n\t\t{x: 2, y: 1},\n\t\t{x: 1, y: 1},\n\t\t{x: 1, y: 2},\n\t]);\n\tt.is(nextState.score, state.score);\n});\n\ntest('snake ends with a win when it fills the board', async t => {\n\tconst fixturePath = path.join(\n\t\t__dirname,\n\t\t'fixtures/alternate-screen-full-board-win.tsx',\n\t);\n\tconst childProcess = spawnProcess('node', ['--import=tsx', fixturePath], {\n\t\tcwd: __dirname,\n\t\tstdio: ['ignore', 'pipe', 'pipe'],\n\t});\n\n\tlet stdout = '';\n\tlet stderr = '';\n\n\tif (!childProcess.stdout || !childProcess.stderr) {\n\t\tt.fail('Fixture process did not expose stdout/stderr pipes');\n\t\treturn;\n\t}\n\n\tchildProcess.stdout.on('data', (data: Uint8Array | string) => {\n\t\tstdout += typeof data === 'string' ? data : data.toString();\n\t});\n\n\tchildProcess.stderr.on('data', (data: Uint8Array | string) => {\n\t\tstderr += typeof data === 'string' ? data : data.toString();\n\t});\n\n\tconst result = await new Promise<\n\t\t{timedOut: true} | {timedOut: false; exitCode: number}\n\t>((resolve, reject) => {\n\t\tconst timeout = setTimeout(() => {\n\t\t\tchildProcess.kill();\n\t\t\tresolve({timedOut: true});\n\t\t}, 1000);\n\n\t\tchildProcess.on('error', error => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(error);\n\t\t});\n\n\t\tchildProcess.on('close', exitCode => {\n\t\t\tclearTimeout(timeout);\n\t\t\tresolve({timedOut: false, exitCode: exitCode ?? 0});\n\t\t});\n\t});\n\n\tif (result.timedOut) {\n\t\tt.fail('Fixture hung instead of finishing the full-board win case');\n\t\treturn;\n\t}\n\n\tt.is(result.exitCode, 0, `Fixture exited with stderr: ${stderr}`);\n\n\tconst nextState = JSON.parse(stdout) as {\n\t\tgameOver: boolean;\n\t\twon: boolean;\n\t\tscore: number;\n\t\tsnakeLength: number;\n\t};\n\n\tt.true(nextState.gameOver);\n\tt.true(nextState.won);\n\tt.is(nextState.score, 297);\n\tt.is(nextState.snakeLength, 300);\n});\n"
  },
  {
    "path": "test/ansi-tokenizer.ts",
    "content": "import test from 'ava';\nimport {tokenizeAnsi} from '../src/ansi-tokenizer.js';\n\ntest('tokenize plain text', t => {\n\tt.deepEqual(tokenizeAnsi('hello'), [{type: 'text', value: 'hello'}]);\n});\n\ntest('tokenize ESC CSI SGR sequence', t => {\n\tconst tokens = tokenizeAnsi('A\\u001B[31mB');\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'csi', 'text'],\n\t);\n\tt.deepEqual(tokens[0], {type: 'text', value: 'A'});\n\tt.deepEqual(tokens[2], {type: 'text', value: 'B'});\n\n\tconst csiToken = tokens[1];\n\tif (csiToken?.type !== 'csi') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(csiToken.value, '\\u001B[31m');\n\tt.is(csiToken.parameterString, '31');\n\tt.is(csiToken.intermediateString, '');\n\tt.is(csiToken.finalCharacter, 'm');\n});\n\ntest('tokenize C1 CSI sequence', t => {\n\tconst tokens = tokenizeAnsi('A\\u009B2 qB');\n\tconst csiToken = tokens[1];\n\n\tif (csiToken?.type !== 'csi') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(csiToken.value, '\\u009B2 q');\n\tt.is(csiToken.parameterString, '2');\n\tt.is(csiToken.intermediateString, ' ');\n\tt.is(csiToken.finalCharacter, 'q');\n});\n\ntest('tokenize OSC control string with ST terminator', t => {\n\tconst tokens = tokenizeAnsi('A\\u001B]8;;https://example.com\\u001B\\\\B');\n\tconst oscToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'osc', 'text'],\n\t);\n\tif (oscToken?.type !== 'osc') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(oscToken.value, '\\u001B]8;;https://example.com\\u001B\\\\');\n});\n\ntest('tokenize tmux DCS passthrough as one control string token', t => {\n\tconst tokens = tokenizeAnsi(\n\t\t'A\\u001BPtmux;\\u001B\\u001B]8;;https://example.com\\u001B\\u001B\\\\\\u001B\\\\B',\n\t);\n\tconst dcsToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'dcs', 'text'],\n\t);\n\tif (dcsToken?.type !== 'dcs') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.true(dcsToken.value.startsWith('\\u001BPtmux;'));\n\tt.true(dcsToken.value.endsWith('\\u001B\\\\'));\n});\n\ntest('tokenize incomplete CSI as invalid and stop', t => {\n\tconst tokens = tokenizeAnsi('A\\u001B[');\n\n\tt.deepEqual(tokens, [\n\t\t{type: 'text', value: 'A'},\n\t\t{type: 'invalid', value: '\\u001B['},\n\t]);\n});\n\ntest('tokenize incomplete ESC intermediate sequence as invalid and stop', t => {\n\tconst tokens = tokenizeAnsi('A\\u001B#');\n\n\tt.deepEqual(tokens, [\n\t\t{type: 'text', value: 'A'},\n\t\t{type: 'invalid', value: '\\u001B#'},\n\t]);\n});\n\ntest('ignore lone ESC before non-final byte', t => {\n\tconst tokens = tokenizeAnsi('A\\u001B\\u0007B');\n\n\tt.deepEqual(tokens, [\n\t\t{type: 'text', value: 'A'},\n\t\t{type: 'text', value: '\\u0007B'},\n\t]);\n});\n\ntest('tokenize ESC ST sequence as ESC token', t => {\n\tconst tokens = tokenizeAnsi('A\\u001B\\\\B');\n\tconst escToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'esc', 'text'],\n\t);\n\tif (escToken?.type !== 'esc') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(escToken.value, '\\u001B\\\\');\n\tt.is(escToken.intermediateString, '');\n\tt.is(escToken.finalCharacter, '\\\\');\n});\n\ntest('tokenize C1 OSC with C1 ST terminator', t => {\n\tconst tokens = tokenizeAnsi('A\\u009D8;;https://example.com\\u009CB');\n\tconst oscToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'osc', 'text'],\n\t);\n\tif (oscToken?.type !== 'osc') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(oscToken.value, '\\u009D8;;https://example.com\\u009C');\n});\n\ntest('tokenize C1 OSC with ESC ST terminator', t => {\n\tconst tokens = tokenizeAnsi('A\\u009D8;;https://example.com\\u001B\\\\B');\n\tconst oscToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'osc', 'text'],\n\t);\n\tif (oscToken?.type !== 'osc') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(oscToken.value, '\\u009D8;;https://example.com\\u001B\\\\');\n});\n\ntest('tokenize C1 SGR CSI sequence', t => {\n\tconst tokens = tokenizeAnsi('A\\u009B31mB');\n\tconst csiToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'csi', 'text'],\n\t);\n\tif (csiToken?.type !== 'csi') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(csiToken.value, '\\u009B31m');\n\tt.is(csiToken.parameterString, '31');\n\tt.is(csiToken.intermediateString, '');\n\tt.is(csiToken.finalCharacter, 'm');\n});\n\ntest('tokenize incomplete C1 CSI as invalid and stop', t => {\n\tconst tokens = tokenizeAnsi('A\\u009B31');\n\n\tt.deepEqual(tokens, [\n\t\t{type: 'text', value: 'A'},\n\t\t{type: 'invalid', value: '\\u009B31'},\n\t]);\n});\n\ntest('tokenize incomplete C1 OSC as invalid and stop', t => {\n\tconst tokens = tokenizeAnsi('A\\u009D8;;https://example.com');\n\n\tt.deepEqual(tokens, [\n\t\t{type: 'text', value: 'A'},\n\t\t{type: 'invalid', value: '\\u009D8;;https://example.com'},\n\t]);\n});\n\ntest('tokenize DCS with BEL in payload until ST terminator', t => {\n\tconst tokens = tokenizeAnsi('A\\u001BPpayload\\u0007still-payload\\u001B\\\\B');\n\tconst dcsToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'dcs', 'text'],\n\t);\n\tif (dcsToken?.type !== 'dcs') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.true(dcsToken.value.includes('\\u0007'));\n\tt.true(dcsToken.value.endsWith('\\u001B\\\\'));\n});\n\ntest('tokenize C1 OSC control string with BEL terminator', t => {\n\tconst tokens = tokenizeAnsi('A\\u009D8;;https://example.com\\u0007B');\n\tconst oscToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'osc', 'text'],\n\t);\n\tif (oscToken?.type !== 'osc') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(oscToken.value, '\\u009D8;;https://example.com\\u0007');\n});\n\ntest('tokenize ESC SOS control string with ST terminator', t => {\n\tconst tokens = tokenizeAnsi('A\\u001BXpayload\\u001B\\\\B');\n\tconst sosToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'sos', 'text'],\n\t);\n\tif (sosToken?.type !== 'sos') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(sosToken.value, '\\u001BXpayload\\u001B\\\\');\n});\n\ntest('tokenize ESC SOS control string with C1 ST terminator', t => {\n\tconst tokens = tokenizeAnsi('A\\u001BXpayload\\u009CB');\n\tconst sosToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'sos', 'text'],\n\t);\n\tif (sosToken?.type !== 'sos') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(sosToken.value, '\\u001BXpayload\\u009C');\n});\n\ntest('tokenize C1 SOS control string with C1 ST terminator', t => {\n\tconst tokens = tokenizeAnsi('A\\u0098payload\\u009CB');\n\tconst sosToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'sos', 'text'],\n\t);\n\tif (sosToken?.type !== 'sos') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(sosToken.value, '\\u0098payload\\u009C');\n});\n\ntest('tokenize C1 SOS control string with ESC ST terminator', t => {\n\tconst tokens = tokenizeAnsi('A\\u0098payload\\u001B\\\\B');\n\tconst sosToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'sos', 'text'],\n\t);\n\tif (sosToken?.type !== 'sos') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(sosToken.value, '\\u0098payload\\u001B\\\\');\n});\n\ntest('tokenize ESC SOS with BEL terminator as invalid and stop', t => {\n\tconst tokens = tokenizeAnsi('A\\u001BXpayload\\u0007B');\n\n\tt.deepEqual(tokens, [\n\t\t{type: 'text', value: 'A'},\n\t\t{type: 'invalid', value: '\\u001BXpayload\\u0007B'},\n\t]);\n});\n\ntest('tokenize C1 SOS with BEL terminator as invalid and stop', t => {\n\tconst tokens = tokenizeAnsi('A\\u0098payload\\u0007B');\n\n\tt.deepEqual(tokens, [\n\t\t{type: 'text', value: 'A'},\n\t\t{type: 'invalid', value: '\\u0098payload\\u0007B'},\n\t]);\n});\n\ntest('tokenize incomplete C1 SOS as invalid and stop', t => {\n\tconst tokens = tokenizeAnsi('A\\u0098payload');\n\n\tt.deepEqual(tokens, [\n\t\t{type: 'text', value: 'A'},\n\t\t{type: 'invalid', value: '\\u0098payload'},\n\t]);\n});\n\ntest('tokenize incomplete ESC SOS as invalid and stop', t => {\n\tconst tokens = tokenizeAnsi('A\\u001BXpayload');\n\n\tt.deepEqual(tokens, [\n\t\t{type: 'text', value: 'A'},\n\t\t{type: 'invalid', value: '\\u001BXpayload'},\n\t]);\n});\n\ntest('tokenize SOS with escaped ESC in payload until final ST terminator', t => {\n\tconst tokens = tokenizeAnsi('A\\u001BXfoo\\u001B\\u001B\\\\bar\\u001B\\\\B');\n\tconst sosToken = tokens[1];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'sos', 'text'],\n\t);\n\tif (sosToken?.type !== 'sos') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.true(sosToken.value.includes('\\u001B\\u001B\\\\'));\n\tt.true(sosToken.value.endsWith('\\u001B\\\\'));\n});\n\ntest('tokenize standalone C1 controls as c1 tokens', t => {\n\tconst tokens = tokenizeAnsi('A\\u0085B\\u008EC');\n\tconst c1Token1 = tokens[1];\n\tconst c1Token2 = tokens[3];\n\n\tt.deepEqual(\n\t\ttokens.map(token => token.type),\n\t\t['text', 'c1', 'text', 'c1', 'text'],\n\t);\n\tif (c1Token1?.type !== 'c1') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tif (c1Token2?.type !== 'c1') {\n\t\tt.fail();\n\t\treturn;\n\t}\n\n\tt.is(c1Token1.value, '\\u0085');\n\tt.is(c1Token2.value, '\\u008E');\n});\n"
  },
  {
    "path": "test/background.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport chalk from 'chalk';\nimport {render, Box, Text} from '../src/index.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\nimport createStdout from './helpers/create-stdout.js';\nimport {renderAsync} from './helpers/test-renderer.js';\nimport {enableTestColors, disableTestColors} from './helpers/force-colors.js';\n\n// ANSI escape sequences for background colors\n// Note: We test against raw ANSI codes rather than chalk predicates because:\n// 1. Different color reset patterns:\n//    - Chalk: '\\u001b[43mHello \\u001b[49m\\u001b[43mWorld\\u001b[49m' (individual resets)\n//    - Ink:   '\\u001b[43mHello World\\u001b[49m' (continuous blocks)\n// 2. Background space fills that chalk doesn't generate:\n//    - Ink: '\\u001b[41mHello     \\u001b[49m\\n\\u001b[41m          \\u001b[49m' (fills entire Box area)\n// 3. Context-aware color transitions:\n//    - Chalk: '\\u001b[43mOuter: \\u001b[49m\\u001b[44mInner: \\u001b[49m\\u001b[41mExplicit\\u001b[49m'\n//    - Ink:   '\\u001b[43mOuter: \\u001b[44mInner: \\u001b[41mExplicit\\u001b[49m' (no intermediate resets)\nconst ansi = {\n\t// Standard colors\n\tbgRed: '\\u001B[41m',\n\tbgGreen: '\\u001B[42m',\n\tbgYellow: '\\u001B[43m',\n\tbgBlue: '\\u001B[44m',\n\tbgMagenta: '\\u001B[45m',\n\tbgCyan: '\\u001B[46m',\n\n\t// Hex/RGB colors (24-bit)\n\tbgHexRed: '\\u001B[48;2;255;0;0m', // #FF0000 or rgb(255,0,0)\n\n\t// ANSI256 colors\n\tbgAnsi256Nine: '\\u001B[48;5;9m', // Ansi256(9)\n\n\t// Reset\n\tbgReset: '\\u001B[49m',\n} as const;\n\n// Enable colors for all tests\ntest.before(() => {\n\tenableTestColors();\n});\n\ntest.after(() => {\n\tdisableTestColors();\n});\n\n// Text inheritance tests (these work in non-TTY)\ntest('Text inherits parent Box background color', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"green\" alignSelf=\"flex-start\">\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgGreen('Hello World'));\n});\n\ntest('Text explicit background color overrides inherited', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"red\" alignSelf=\"flex-start\">\n\t\t\t<Text backgroundColor=\"blue\">Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgBlue('Hello World'));\n});\n\ntest('Nested Box background inheritance', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"red\" alignSelf=\"flex-start\">\n\t\t\t<Box backgroundColor=\"blue\">\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgBlue('Hello World'));\n});\n\ntest('Text without parent Box background has no inheritance', t => {\n\tconst output = renderToString(\n\t\t<Box alignSelf=\"flex-start\">\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello World');\n});\n\ntest('Multiple Text elements inherit same background', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"yellow\" alignSelf=\"flex-start\">\n\t\t\t<Text>Hello </Text>\n\t\t\t<Text>World</Text>\n\t\t</Box>,\n\t);\n\n\t// Text nodes are rendered as a single block with shared background\n\tt.is(output, chalk.bgYellow('Hello World'));\n});\n\ntest('Mixed text with and without background inheritance', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"green\" alignSelf=\"flex-start\">\n\t\t\t<Text>Inherited </Text>\n\t\t\t<Text backgroundColor=\"\">No BG </Text>\n\t\t\t<Text backgroundColor=\"red\">Red BG</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgGreen('Inherited ') + 'No BG ' + chalk.bgRed('Red BG'));\n});\n\ntest('Complex nested structure with background inheritance', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"yellow\" alignSelf=\"flex-start\">\n\t\t\t<Box>\n\t\t\t\t<Text>Outer: </Text>\n\t\t\t\t<Box backgroundColor=\"blue\">\n\t\t\t\t\t<Text>Inner: </Text>\n\t\t\t\t\t<Text backgroundColor=\"red\">Explicit</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\t// Colors transition without reset codes between them - actual behavior from debug output\n\tt.is(\n\t\toutput,\n\t\t`${ansi.bgYellow}Outer: ${ansi.bgBlue}Inner: ${ansi.bgRed}Explicit${ansi.bgReset}`,\n\t);\n});\n\n// Background color tests for different formats\ntest('Box background with standard color', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"red\" alignSelf=\"flex-start\">\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgRed('Hello'));\n});\n\ntest('Box background with hex color', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"#FF0000\" alignSelf=\"flex-start\">\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgHex('#FF0000')('Hello'));\n});\n\ntest('Box background with rgb color', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"rgb(255, 0, 0)\" alignSelf=\"flex-start\">\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgRgb(255, 0, 0)('Hello'));\n});\n\ntest('Box background with ansi256 color', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"ansi256(9)\" alignSelf=\"flex-start\">\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgAnsi256(9)('Hello'));\n});\n\ntest('Box background with wide characters', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"yellow\" alignSelf=\"flex-start\">\n\t\t\t<Text>こんにちは</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgYellow('こんにちは'));\n});\n\ntest('Box background with emojis', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"red\" alignSelf=\"flex-start\">\n\t\t\t<Text>🎉🎊</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgRed('🎉🎊'));\n});\n\n// Box background space fill tests - these should work with forced colors\ntest('Box background fills entire area with standard color', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"red\" width={10} height={3} alignSelf=\"flex-start\">\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t);\n\n\t// Should contain background color codes and fill spaces for entire Box area\n\tt.true(\n\t\toutput.includes(ansi.bgRed),\n\t\t'Should contain red background start code',\n\t);\n\tt.true(output.includes(ansi.bgReset), 'Should contain background reset code');\n\tt.true(output.includes('Hello'), 'Should contain the text');\n\tt.true(\n\t\toutput.includes(`${ansi.bgRed}          ${ansi.bgReset}`),\n\t\t'Should contain background fill line',\n\t);\n});\n\ntest('Box background fills with hex color', t => {\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"#FF0000\" width={10} height={3} alignSelf=\"flex-start\">\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t);\n\n\t// Should contain hex color background codes and fill spaces\n\tt.true(output.includes('Hello'), 'Should contain the text');\n\tt.true(\n\t\toutput.includes(ansi.bgHexRed),\n\t\t'Should contain hex RGB background code',\n\t);\n\tt.true(output.includes(ansi.bgReset), 'Should contain background reset code');\n});\n\ntest('Box background fills with rgb color', t => {\n\tconst output = renderToString(\n\t\t<Box\n\t\t\tbackgroundColor=\"rgb(255, 0, 0)\"\n\t\t\twidth={10}\n\t\t\theight={3}\n\t\t\talignSelf=\"flex-start\"\n\t\t>\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t);\n\n\t// Should contain RGB color background codes and fill spaces\n\tt.true(output.includes('Hello'), 'Should contain the text');\n\tt.true(output.includes(ansi.bgHexRed), 'Should contain RGB background code');\n\tt.true(output.includes(ansi.bgReset), 'Should contain background reset code');\n});\n\ntest('Box background fills with ansi256 color', t => {\n\tconst output = renderToString(\n\t\t<Box\n\t\t\tbackgroundColor=\"ansi256(9)\"\n\t\t\twidth={10}\n\t\t\theight={3}\n\t\t\talignSelf=\"flex-start\"\n\t\t>\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t);\n\n\t// Should contain ANSI256 color background codes and fill spaces\n\tt.true(output.includes('Hello'), 'Should contain the text');\n\tt.true(\n\t\toutput.includes(ansi.bgAnsi256Nine),\n\t\t'Should contain ANSI256 background code',\n\t);\n\tt.true(output.includes(ansi.bgReset), 'Should contain background reset code');\n});\n\ntest('Box background with border fills content area', t => {\n\tconst output = renderToString(\n\t\t<Box\n\t\t\tbackgroundColor=\"cyan\"\n\t\t\tborderStyle=\"round\"\n\t\t\twidth={10}\n\t\t\theight={5}\n\t\t\talignSelf=\"flex-start\"\n\t\t>\n\t\t\t<Text>Hi</Text>\n\t\t</Box>,\n\t);\n\n\t// Should have background fill inside the border and border characters\n\tt.true(output.includes('Hi'), 'Should contain the text');\n\tt.true(output.includes(ansi.bgCyan), 'Should contain cyan background code');\n\tt.true(output.includes(ansi.bgReset), 'Should contain background reset code');\n\tt.true(output.includes('╭'), 'Should contain top-left border');\n\tt.true(output.includes('╮'), 'Should contain top-right border');\n});\n\ntest('Box background with padding fills entire padded area', t => {\n\tconst output = renderToString(\n\t\t<Box\n\t\t\tbackgroundColor=\"magenta\"\n\t\t\tpadding={1}\n\t\t\twidth={10}\n\t\t\theight={5}\n\t\t\talignSelf=\"flex-start\"\n\t\t>\n\t\t\t<Text>Hi</Text>\n\t\t</Box>,\n\t);\n\n\t// Background should fill the entire Box area including padding\n\tt.true(output.includes('Hi'), 'Should contain the text');\n\tt.true(\n\t\toutput.includes(ansi.bgMagenta),\n\t\t'Should contain magenta background code',\n\t);\n\tt.true(output.includes(ansi.bgReset), 'Should contain background reset code');\n});\n\ntest('Box background with center alignment fills entire area', t => {\n\tconst output = renderToString(\n\t\t<Box\n\t\t\tbackgroundColor=\"blue\"\n\t\t\twidth={10}\n\t\t\theight={3}\n\t\t\tjustifyContent=\"center\"\n\t\t\talignSelf=\"flex-start\"\n\t\t>\n\t\t\t<Text>Hi</Text>\n\t\t</Box>,\n\t);\n\n\tt.true(output.includes('Hi'), 'Should contain centered text');\n\tt.true(output.includes(ansi.bgBlue), 'Should contain blue background code');\n\tt.true(output.includes(ansi.bgReset), 'Should contain background reset code');\n});\n\ntest('Box background with column layout fills entire area', t => {\n\tconst output = renderToString(\n\t\t<Box\n\t\t\tbackgroundColor=\"green\"\n\t\t\tflexDirection=\"column\"\n\t\t\twidth={10}\n\t\t\theight={5}\n\t\t\talignSelf=\"flex-start\"\n\t\t>\n\t\t\t<Text>Line 1</Text>\n\t\t\t<Text>Line 2</Text>\n\t\t</Box>,\n\t);\n\n\tt.true(output.includes('Line 1'), 'Should contain first line text');\n\tt.true(output.includes('Line 2'), 'Should contain second line text');\n\tt.true(output.includes(ansi.bgGreen), 'Should contain green background code');\n\tt.true(output.includes(ansi.bgReset), 'Should contain background reset code');\n});\n\n// Update tests using render() for comprehensive coverage\ntest('Box background updates on rerender', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({bgColor}: {readonly bgColor?: string}) {\n\t\treturn (\n\t\t\t<Box backgroundColor={bgColor} alignSelf=\"flex-start\">\n\t\t\t\t<Text>Hello</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is((stdout.write as any).lastCall.args[0], 'Hello');\n\n\trerender(<Test bgColor=\"green\" />);\n\tt.is((stdout.write as any).lastCall.args[0], chalk.bgGreen('Hello'));\n\n\trerender(<Test />);\n\tt.is((stdout.write as any).lastCall.args[0], 'Hello');\n});\n\n// Concurrent mode tests\ntest('Text inherits parent Box background color - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box backgroundColor=\"green\" alignSelf=\"flex-start\">\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgGreen('Hello World'));\n});\n\ntest('Nested Box background inheritance - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box backgroundColor=\"red\" alignSelf=\"flex-start\">\n\t\t\t<Box backgroundColor=\"blue\">\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgBlue('Hello World'));\n});\n\ntest('Box background with hex color - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box backgroundColor=\"#FF0000\" alignSelf=\"flex-start\">\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, chalk.bgHex('#FF0000')('Hello'));\n});\n\ntest('Box background updates on rerender - concurrent', async t => {\n\tfunction Test({bgColor}: {readonly bgColor?: string}) {\n\t\treturn (\n\t\t\t<Box backgroundColor={bgColor} alignSelf=\"flex-start\">\n\t\t\t\t<Text>Hello</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {getOutput, rerenderAsync} = await renderAsync(<Test />);\n\n\tt.is(getOutput(), 'Hello');\n\n\tawait rerenderAsync(<Test bgColor=\"green\" />);\n\tt.is(getOutput(), chalk.bgGreen('Hello'));\n\n\tawait rerenderAsync(<Test />);\n\tt.is(getOutput(), 'Hello');\n});\n\ntest('Box backgroundColor fills full width on every line when text wraps', t => {\n\t// \"Hello World!!\" is 13 chars, width=10 forces wrapping into 2 lines\n\tconst output = renderToString(\n\t\t<Box backgroundColor=\"red\" width={10} alignSelf=\"flex-start\">\n\t\t\t<Text>Hello World!!</Text>\n\t\t</Box>,\n\t);\n\n\t// Both lines are padded to the full 10-char Box width with background color\n\tt.is(\n\t\toutput,\n\t\t`${ansi.bgRed}Hello     ${ansi.bgReset}\\n${ansi.bgRed}World!!   ${ansi.bgReset}`,\n\t);\n});\n\ntest('Text-only backgroundColor colors text content but does not fill Box width', t => {\n\t// Without a Box backgroundColor, only the text characters are colored\n\tconst output = renderToString(\n\t\t<Box width={10} alignSelf=\"flex-start\">\n\t\t\t<Text backgroundColor=\"red\">Hello World!!</Text>\n\t\t</Box>,\n\t);\n\n\t// Text-only bg colors just the text, not the remaining space to fill Box width\n\tt.is(\n\t\toutput,\n\t\t`${ansi.bgRed}Hello ${ansi.bgReset}\\n${ansi.bgRed}World!!${ansi.bgReset}`,\n\t);\n});\n"
  },
  {
    "path": "test/borders.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport boxen from 'boxen';\nimport indentString from 'indent-string';\nimport cliBoxes from 'cli-boxes';\nimport chalk from 'chalk';\nimport {render, Box, Text} from '../src/index.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\nimport createStdout from './helpers/create-stdout.js';\nimport {renderAsync} from './helpers/test-renderer.js';\n\ntest('single node - full width box', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\">\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('Hello World', {width: 100, borderStyle: 'round'}));\n});\n\ntest('single node - full width box with colorful border', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" borderColor=\"green\">\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\tboxen('Hello World', {\n\t\t\twidth: 100,\n\t\t\tborderStyle: 'round',\n\t\t\tborderColor: 'green',\n\t\t}),\n\t);\n});\n\ntest('single node - fit-content box', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" alignSelf=\"flex-start\">\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('Hello World', {borderStyle: 'round'}));\n});\n\ntest('single node - fit-content box with wide characters', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" alignSelf=\"flex-start\">\n\t\t\t<Text>こんにちは</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('こんにちは', {borderStyle: 'round'}));\n});\n\ntest('single node - fit-content box with emojis', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" alignSelf=\"flex-start\">\n\t\t\t<Text>🌊🌊</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('🌊🌊', {borderStyle: 'round'}));\n});\n\n// Issue #733: Emojis with variation selectors (FE0F) should align properly\ntest('single node - fit-content box with variation selector emojis', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" alignSelf=\"flex-start\">\n\t\t\t<Text>🌡️⚠️✅</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('🌡️⚠️✅', {borderStyle: 'round'}));\n});\n\ntest('single node - fixed width box', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" width={20}>\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('Hello World'.padEnd(18, ' '), {borderStyle: 'round'}));\n});\n\ntest('single node - fixed width and height box', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" width={20} height={20}>\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\tboxen('Hello World'.padEnd(18, ' ') + '\\n'.repeat(17), {\n\t\t\tborderStyle: 'round',\n\t\t}),\n\t);\n});\n\ntest('single node - box with padding', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" padding={1} alignSelf=\"flex-start\">\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('\\n Hello World \\n', {borderStyle: 'round'}));\n});\n\ntest('single node - box with horizontal alignment', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" width={20} justifyContent=\"center\">\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('   Hello World    ', {borderStyle: 'round'}));\n});\n\ntest('single node - box with vertical alignment', t => {\n\tconst output = renderToString(\n\t\t<Box\n\t\t\tborderStyle=\"round\"\n\t\t\theight={20}\n\t\t\talignItems=\"center\"\n\t\t\talignSelf=\"flex-start\"\n\t\t>\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\tboxen('\\n'.repeat(8) + 'Hello World' + '\\n'.repeat(9), {\n\t\t\tborderStyle: 'round',\n\t\t}),\n\t);\n});\n\ntest('single node - box with wrapping', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" width={10}>\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('Hello   \\nWorld', {borderStyle: 'round'}));\n});\n\ntest('multiple nodes - full width box', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\">\n\t\t\t<Text>{'Hello '}World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('Hello World', {width: 100, borderStyle: 'round'}));\n});\n\ntest('multiple nodes - full width box with colorful border', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" borderColor=\"green\">\n\t\t\t<Text>{'Hello '}World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\tboxen('Hello World', {\n\t\t\twidth: 100,\n\t\t\tborderStyle: 'round',\n\t\t\tborderColor: 'green',\n\t\t}),\n\t);\n});\n\ntest('multiple nodes - fit-content box', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" alignSelf=\"flex-start\">\n\t\t\t<Text>{'Hello '}World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('Hello World', {borderStyle: 'round'}));\n});\n\ntest('multiple nodes - fixed width box', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" width={20}>\n\t\t\t<Text>{'Hello '}World</Text>\n\t\t</Box>,\n\t);\n\tt.is(output, boxen('Hello World'.padEnd(18, ' '), {borderStyle: 'round'}));\n});\n\ntest('multiple nodes - fixed width and height box', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" width={20} height={20}>\n\t\t\t<Text>{'Hello '}World</Text>\n\t\t</Box>,\n\t);\n\tt.is(\n\t\toutput,\n\t\tboxen('Hello World'.padEnd(18, ' ') + '\\n'.repeat(17), {\n\t\t\tborderStyle: 'round',\n\t\t}),\n\t);\n});\n\ntest('multiple nodes - box with padding', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" padding={1} alignSelf=\"flex-start\">\n\t\t\t<Text>{'Hello '}World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('\\n Hello World \\n', {borderStyle: 'round'}));\n});\n\ntest('multiple nodes - box with horizontal alignment', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" width={20} justifyContent=\"center\">\n\t\t\t<Text>{'Hello '}World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('   Hello World    ', {borderStyle: 'round'}));\n});\n\ntest('multiple nodes - box with vertical alignment', t => {\n\tconst output = renderToString(\n\t\t<Box\n\t\t\tborderStyle=\"round\"\n\t\t\theight={20}\n\t\t\talignItems=\"center\"\n\t\t\talignSelf=\"flex-start\"\n\t\t>\n\t\t\t<Text>{'Hello '}World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\tboxen('\\n'.repeat(8) + 'Hello World' + '\\n'.repeat(9), {\n\t\t\tborderStyle: 'round',\n\t\t}),\n\t);\n});\n\ntest('multiple nodes - box with wrapping', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" width={10}>\n\t\t\t<Text>{'Hello '}World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('Hello   \\nWorld', {borderStyle: 'round'}));\n});\n\ntest('multiple nodes - box with wrapping and long first node', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" width={10}>\n\t\t\t<Text>{'Helloooooo'} World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('Helloooo\\noo World', {borderStyle: 'round'}));\n});\n\ntest('multiple nodes - box with wrapping and very long first node', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" width={10}>\n\t\t\t<Text>{'Hellooooooooooooo'} World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('Helloooo\\noooooooo\\no World', {borderStyle: 'round'}));\n});\n\ntest('nested boxes', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" width={40} padding={1}>\n\t\t\t<Box borderStyle=\"round\" justifyContent=\"center\" padding={1}>\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tconst nestedBox = indentString(\n\t\tboxen('\\n Hello World \\n', {borderStyle: 'round'}),\n\t\t1,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\tboxen(`${' '.repeat(38)}\\n${nestedBox}\\n`, {borderStyle: 'round'}),\n\t);\n});\n\ntest('nested boxes - fit-content box with wide characters on flex-direction row', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" alignSelf=\"flex-start\">\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>ミスター</Text>\n\t\t\t</Box>\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>スポック</Text>\n\t\t\t</Box>\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>カーク船長</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tconst box1 = boxen('ミスター', {borderStyle: 'round'});\n\tconst box2 = boxen('スポック', {borderStyle: 'round'});\n\tconst box3 = boxen('カーク船長', {borderStyle: 'round'});\n\n\tconst expected = boxen(\n\t\tbox1\n\t\t\t.split('\\n')\n\t\t\t.map(\n\t\t\t\t(line, index) =>\n\t\t\t\t\tline + box2.split('\\n')[index]! + box3.split('\\n')[index]!,\n\t\t\t)\n\t\t\t.join('\\n'),\n\t\t{borderStyle: 'round'},\n\t);\n\n\tt.is(output, expected);\n});\n\ntest('nested boxes - fit-content box with emojis on flex-direction row', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" alignSelf=\"flex-start\">\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>🦾</Text>\n\t\t\t</Box>\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>🌏</Text>\n\t\t\t</Box>\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>😋</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tconst box1 = boxen('🦾', {borderStyle: 'round'});\n\tconst box2 = boxen('🌏', {borderStyle: 'round'});\n\tconst box3 = boxen('😋', {borderStyle: 'round'});\n\n\tconst expected = boxen(\n\t\tbox1\n\t\t\t.split('\\n')\n\t\t\t.map(\n\t\t\t\t(line, index) =>\n\t\t\t\t\tline + box2.split('\\n')[index]! + box3.split('\\n')[index]!,\n\t\t\t)\n\t\t\t.join('\\n'),\n\t\t{borderStyle: 'round'},\n\t);\n\n\tt.is(output, expected);\n});\n\ntest('nested boxes - fit-content box with wide characters on flex-direction column', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" alignSelf=\"flex-start\" flexDirection=\"column\">\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>ミスター</Text>\n\t\t\t</Box>\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>スポック</Text>\n\t\t\t</Box>\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>カーク船長</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tconst expected = boxen(\n\t\tboxen('ミスター  ', {borderStyle: 'round'}) +\n\t\t\t'\\n' +\n\t\t\tboxen('スポック  ', {borderStyle: 'round'}) +\n\t\t\t'\\n' +\n\t\t\tboxen('カーク船長', {borderStyle: 'round'}),\n\t\t{borderStyle: 'round'},\n\t);\n\n\tt.is(output, expected);\n});\n\ntest('nested boxes - fit-content box with emojis on flex-direction column', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"round\" alignSelf=\"flex-start\" flexDirection=\"column\">\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>🦾</Text>\n\t\t\t</Box>\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>🌏</Text>\n\t\t\t</Box>\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>😋</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tconst expected = boxen(\n\t\tboxen('🦾', {borderStyle: 'round'}) +\n\t\t\t'\\n' +\n\t\t\tboxen('🌏', {borderStyle: 'round'}) +\n\t\t\t'\\n' +\n\t\t\tboxen('😋', {borderStyle: 'round'}),\n\t\t{borderStyle: 'round'},\n\t);\n\n\tt.is(output, expected);\n});\n\ntest('render border after update', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({borderColor}: {readonly borderColor?: string}) {\n\t\treturn (\n\t\t\t<Box borderStyle=\"round\" borderColor={borderColor}>\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\tboxen('Hello World', {width: 100, borderStyle: 'round'}),\n\t);\n\n\trerender(<Test borderColor=\"green\" />);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\tboxen('Hello World', {\n\t\t\twidth: 100,\n\t\t\tborderStyle: 'round',\n\t\t\tborderColor: 'green',\n\t\t}),\n\t);\n\n\trerender(<Test />);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\tboxen('Hello World', {\n\t\t\twidth: 100,\n\t\t\tborderStyle: 'round',\n\t\t}),\n\t);\n});\n\ntest('render border edge changes after update when borderStyle is unchanged', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({borderTop}: {readonly borderTop?: boolean}) {\n\t\treturn (\n\t\t\t<Box borderStyle=\"round\" borderTop={borderTop} alignSelf=\"flex-start\">\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\tboxen('Content', {borderStyle: 'round'}),\n\t);\n\n\trerender(<Test borderTop={false} />);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t[\n\t\t\t`${cliBoxes.round.left}Content${cliBoxes.round.right}`,\n\t\t\t`${cliBoxes.round.bottomLeft}${cliBoxes.round.bottom.repeat(7)}${\n\t\t\t\tcliBoxes.round.bottomRight\n\t\t\t}`,\n\t\t].join('\\n'),\n\t);\n\n\trerender(<Test />);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\tboxen('Content', {borderStyle: 'round'}),\n\t);\n});\n\ntest('hide top border', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderStyle=\"round\" borderTop={false}>\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\t`${cliBoxes.round.left}Content${cliBoxes.round.right}`,\n\t\t\t`${cliBoxes.round.bottomLeft}${cliBoxes.round.bottom.repeat(7)}${\n\t\t\t\tcliBoxes.round.bottomRight\n\t\t\t}`,\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('hide bottom border', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderStyle=\"round\" borderBottom={false}>\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\t`${cliBoxes.round.topLeft}${cliBoxes.round.top.repeat(7)}${\n\t\t\t\tcliBoxes.round.topRight\n\t\t\t}`,\n\t\t\t`${cliBoxes.round.left}Content${cliBoxes.round.right}`,\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('hide top and bottom borders', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderStyle=\"round\" borderTop={false} borderBottom={false}>\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\t`${cliBoxes.round.left}Content${cliBoxes.round.right}`,\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('hide left border', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderStyle=\"round\" borderLeft={false}>\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\t`${cliBoxes.round.top.repeat(7)}${cliBoxes.round.topRight}`,\n\t\t\t`Content${cliBoxes.round.right}`,\n\t\t\t`${cliBoxes.round.bottom.repeat(7)}${cliBoxes.round.bottomRight}`,\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('hide right border', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderStyle=\"round\" borderRight={false}>\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\t`${cliBoxes.round.topLeft}${cliBoxes.round.top.repeat(7)}`,\n\t\t\t`${cliBoxes.round.left}Content`,\n\t\t\t`${cliBoxes.round.bottomLeft}${cliBoxes.round.bottom.repeat(7)}`,\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('hide left and right border', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderStyle=\"round\" borderLeft={false} borderRight={false}>\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\tcliBoxes.round.top.repeat(7),\n\t\t\t'Content',\n\t\t\tcliBoxes.round.bottom.repeat(7),\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('hide all borders', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box\n\t\t\t\tborderStyle=\"round\"\n\t\t\t\tborderTop={false}\n\t\t\t\tborderBottom={false}\n\t\t\t\tborderLeft={false}\n\t\t\t\tborderRight={false}\n\t\t\t>\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, ['Above', 'Content', 'Below'].join('\\n'));\n});\n\ntest('change color of top border', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderStyle=\"round\" borderTopColor=\"green\">\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\tchalk.green(\n\t\t\t\t`${cliBoxes.round.topLeft}${cliBoxes.round.top.repeat(7)}${\n\t\t\t\t\tcliBoxes.round.topRight\n\t\t\t\t}`,\n\t\t\t),\n\t\t\t`${cliBoxes.round.left}Content${cliBoxes.round.right}`,\n\t\t\t`${cliBoxes.round.bottomLeft}${cliBoxes.round.bottom.repeat(7)}${\n\t\t\t\tcliBoxes.round.bottomRight\n\t\t\t}`,\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('change color of bottom border', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderStyle=\"round\" borderBottomColor=\"green\">\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\t`${cliBoxes.round.topLeft}${cliBoxes.round.top.repeat(7)}${\n\t\t\t\tcliBoxes.round.topRight\n\t\t\t}`,\n\t\t\t`${cliBoxes.round.left}Content${cliBoxes.round.right}`,\n\t\t\tchalk.green(\n\t\t\t\t`${cliBoxes.round.bottomLeft}${cliBoxes.round.bottom.repeat(7)}${\n\t\t\t\t\tcliBoxes.round.bottomRight\n\t\t\t\t}`,\n\t\t\t),\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('change color of left border', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderStyle=\"round\" borderLeftColor=\"green\">\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\t`${cliBoxes.round.topLeft}${cliBoxes.round.top.repeat(7)}${\n\t\t\t\tcliBoxes.round.topRight\n\t\t\t}`,\n\t\t\t`${chalk.green(cliBoxes.round.left)}Content${cliBoxes.round.right}`,\n\t\t\t`${cliBoxes.round.bottomLeft}${cliBoxes.round.bottom.repeat(7)}${\n\t\t\t\tcliBoxes.round.bottomRight\n\t\t\t}`,\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('change color of right border', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderStyle=\"round\" borderRightColor=\"green\">\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\t`${cliBoxes.round.topLeft}${cliBoxes.round.top.repeat(7)}${\n\t\t\t\tcliBoxes.round.topRight\n\t\t\t}`,\n\t\t\t`${cliBoxes.round.left}Content${chalk.green(cliBoxes.round.right)}`,\n\t\t\t`${cliBoxes.round.bottomLeft}${cliBoxes.round.bottom.repeat(7)}${\n\t\t\t\tcliBoxes.round.bottomRight\n\t\t\t}`,\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('custom border style', t => {\n\tconst output = renderToString(\n\t\t<Box\n\t\t\tborderStyle={{\n\t\t\t\ttopLeft: '↘',\n\t\t\t\ttop: '↓',\n\t\t\t\ttopRight: '↙',\n\t\t\t\tleft: '→',\n\t\t\t\tbottomLeft: '↗',\n\t\t\t\tbottom: '↑',\n\t\t\t\tbottomRight: '↖',\n\t\t\t\tright: '←',\n\t\t\t}}\n\t\t>\n\t\t\t<Text>Content</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('Content', {width: 100, borderStyle: 'arrow'}));\n});\n\ntest('dim border color', t => {\n\tconst output = renderToString(\n\t\t<Box borderDimColor borderStyle=\"round\">\n\t\t\t<Text>Content</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\tboxen('Content', {\n\t\t\twidth: 100,\n\t\t\tborderStyle: 'round',\n\t\t\tdimBorder: true,\n\t\t}),\n\t);\n});\n\ntest('dim top border color', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderTopDimColor borderStyle=\"round\">\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\tchalk.dim(\n\t\t\t\t`${cliBoxes.round.topLeft}${cliBoxes.round.top.repeat(7)}${\n\t\t\t\t\tcliBoxes.round.topRight\n\t\t\t\t}`,\n\t\t\t),\n\t\t\t`${cliBoxes.round.left}Content${cliBoxes.round.right}`,\n\t\t\t`${cliBoxes.round.bottomLeft}${cliBoxes.round.bottom.repeat(7)}${\n\t\t\t\tcliBoxes.round.bottomRight\n\t\t\t}`,\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('dim bottom border color', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderBottomDimColor borderStyle=\"round\">\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\t`${cliBoxes.round.topLeft}${cliBoxes.round.top.repeat(7)}${\n\t\t\t\tcliBoxes.round.topRight\n\t\t\t}`,\n\t\t\t`${cliBoxes.round.left}Content${cliBoxes.round.right}`,\n\t\t\tchalk.dim(\n\t\t\t\t`${cliBoxes.round.bottomLeft}${cliBoxes.round.bottom.repeat(7)}${\n\t\t\t\t\tcliBoxes.round.bottomRight\n\t\t\t\t}`,\n\t\t\t),\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('dim left border color', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderLeftDimColor borderStyle=\"round\">\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\t`${cliBoxes.round.topLeft}${cliBoxes.round.top.repeat(7)}${\n\t\t\t\tcliBoxes.round.topRight\n\t\t\t}`,\n\t\t\t`${chalk.dim(cliBoxes.round.left)}Content${cliBoxes.round.right}`,\n\t\t\t`${cliBoxes.round.bottomLeft}${cliBoxes.round.bottom.repeat(7)}${\n\t\t\t\tcliBoxes.round.bottomRight\n\t\t\t}`,\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\ntest('dim right border color', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-start\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Box borderRightDimColor borderStyle=\"round\">\n\t\t\t\t<Text>Content</Text>\n\t\t\t</Box>\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t[\n\t\t\t'Above',\n\t\t\t`${cliBoxes.round.topLeft}${cliBoxes.round.top.repeat(7)}${\n\t\t\t\tcliBoxes.round.topRight\n\t\t\t}`,\n\t\t\t`${cliBoxes.round.left}Content${chalk.dim(cliBoxes.round.right)}`,\n\t\t\t`${cliBoxes.round.bottomLeft}${cliBoxes.round.bottom.repeat(7)}${\n\t\t\t\tcliBoxes.round.bottomRight\n\t\t\t}`,\n\t\t\t'Below',\n\t\t].join('\\n'),\n\t);\n});\n\n// Regression test for https://github.com/vadimdemedes/ink/issues/840\n// borderDimColor should not dim styled child Text components touching the left edge\ntest('borderDimColor does not dim styled child Text touching left edge', t => {\n\tconst output = renderToString(\n\t\t<Box borderDimColor borderStyle=\"round\" alignSelf=\"flex-start\">\n\t\t\t<Text bold color=\"blue\">\n\t\t\t\tstyled text\n\t\t\t</Text>\n\t\t</Box>,\n\t);\n\n\t// The styled text should be bold and blue (not dimmed)\n\t// Note: Text component applies color first then bold, so the escape code order is bold+blue\n\tconst styledText = chalk.bold(chalk.blue('styled text'));\n\tt.true(\n\t\toutput.includes(styledText),\n\t\t'Child text should retain its color and bold styling, not be dimmed',\n\t);\n\n\t// The border should be dimmed (entire top border line is dimmed as a unit)\n\tconst dimmedTopBorder = chalk.dim(\n\t\tcliBoxes.round.topLeft +\n\t\t\tcliBoxes.round.top.repeat(11) +\n\t\t\tcliBoxes.round.topRight,\n\t);\n\tt.true(output.includes(dimmedTopBorder), 'Border should be dimmed');\n});\n\n// Concurrent mode tests\ntest('single node - full width box - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box borderStyle=\"round\">\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('Hello World', {width: 100, borderStyle: 'round'}));\n});\n\ntest('single node - fit-content box - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box borderStyle=\"round\" alignSelf=\"flex-start\">\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, boxen('Hello World', {borderStyle: 'round'}));\n});\n\ntest('nested boxes - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box borderStyle=\"round\" width={40} padding={1}>\n\t\t\t<Box borderStyle=\"round\" justifyContent=\"center\" padding={1}>\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tconst nestedBox = indentString(\n\t\tboxen('\\n Hello World \\n', {borderStyle: 'round'}),\n\t\t1,\n\t);\n\n\tt.is(\n\t\toutput,\n\t\tboxen(`${' '.repeat(38)}\\n${nestedBox}\\n`, {borderStyle: 'round'}),\n\t);\n});\n\ntest('render border after update - concurrent', async t => {\n\tfunction Test({borderColor}: {readonly borderColor?: string}) {\n\t\treturn (\n\t\t\t<Box borderStyle=\"round\" borderColor={borderColor}>\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {getOutput, rerenderAsync} = await renderAsync(<Test />);\n\n\tt.is(getOutput(), boxen('Hello World', {width: 100, borderStyle: 'round'}));\n\n\tawait rerenderAsync(<Test borderColor=\"green\" />);\n\n\tt.is(\n\t\tgetOutput(),\n\t\tboxen('Hello World', {\n\t\t\twidth: 100,\n\t\t\tborderStyle: 'round',\n\t\t\tborderColor: 'green',\n\t\t}),\n\t);\n});\n"
  },
  {
    "path": "test/components.tsx",
    "content": "import EventEmitter from 'node:events';\nimport process from 'node:process';\nimport test from 'ava';\nimport chalk from 'chalk';\nimport stripAnsi from 'strip-ansi';\nimport React, {Component, useEffect, useState} from 'react';\nimport {spy, stub} from 'sinon';\nimport ansiEscapes from 'ansi-escapes';\nimport {\n\tBox,\n\tNewline,\n\trender,\n\tSpacer,\n\tStatic,\n\tText,\n\tTransform,\n\tuseApp,\n\tuseInput,\n\tuseStdin,\n} from '../src/index.js';\nimport createStdout from './helpers/create-stdout.js';\nimport {emitReadable} from './helpers/create-stdin.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\nimport {run} from './helpers/run.js';\nimport {renderAsync} from './helpers/test-renderer.js';\n\ntest('text', t => {\n\tconst output = renderToString(<Text>Hello World</Text>);\n\n\tt.is(output, 'Hello World');\n});\n\ntest('text with variable', t => {\n\tconst output = renderToString(<Text>Count: {1}</Text>);\n\n\tt.is(output, 'Count: 1');\n});\n\ntest('multiple text nodes', t => {\n\tconst output = renderToString(\n\t\t<Text>\n\t\t\t{'Hello'}\n\t\t\t{' World'}\n\t\t</Text>,\n\t);\n\n\tt.is(output, 'Hello World');\n});\n\ntest('text with component', t => {\n\tfunction World() {\n\t\treturn <Text>World</Text>;\n\t}\n\n\tconst output = renderToString(\n\t\t<Text>\n\t\t\tHello <World />\n\t\t</Text>,\n\t);\n\n\tt.is(output, 'Hello World');\n});\n\ntest('text with fragment', t => {\n\tconst output = renderToString(\n\t\t<Text>\n\t\t\tHello <>World</> {/* eslint-disable-line react/jsx-no-useless-fragment */}\n\t\t</Text>,\n\t);\n\n\tt.is(output, 'Hello World');\n});\n\ntest('wrap text', t => {\n\tconst output = renderToString(\n\t\t<Box width={7}>\n\t\t\t<Text wrap=\"wrap\">Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello\\nWorld');\n});\n\ntest('don’t wrap text if there is enough space', t => {\n\tconst output = renderToString(\n\t\t<Box width={20}>\n\t\t\t<Text wrap=\"wrap\">Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello World');\n});\n\ntest('truncate text in the end', t => {\n\tconst output = renderToString(\n\t\t<Box width={7}>\n\t\t\t<Text wrap=\"truncate\">Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello …');\n});\n\ntest('truncate text in the middle', t => {\n\tconst output = renderToString(\n\t\t<Box width={7}>\n\t\t\t<Text wrap=\"truncate-middle\">Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hel…rld');\n});\n\ntest('truncate text in the beginning', t => {\n\tconst output = renderToString(\n\t\t<Box width={7}>\n\t\t\t<Text wrap=\"truncate-start\">Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '… World');\n});\n\n// See https://github.com/vadimdemedes/ink/issues/633\ntest('do not wrap text with BEL-terminated OSC hyperlinks', t => {\n\t// \"Click here\" is 10 chars, box is 20 wide - should not wrap\n\tconst hyperlink =\n\t\t'\\u001B]8;;https://example.com\\u0007Click here\\u001B]8;;\\u0007';\n\tconst output = renderToString(\n\t\t<Box width={20}>\n\t\t\t<Text wrap=\"wrap\">{hyperlink}</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(stripAnsi(output), 'Click here');\n});\n\n// See https://github.com/vadimdemedes/ink/issues/633\ntest('do not wrap text with ST-terminated OSC hyperlinks', t => {\n\tconst hyperlink =\n\t\t'\\u001B]8;;https://example.com\\u001B\\\\Click here\\u001B]8;;\\u001B\\\\';\n\tconst output = renderToString(\n\t\t<Box width={20}>\n\t\t\t<Text wrap=\"wrap\">{hyperlink}</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(stripAnsi(output), 'Click here');\n});\n\n// See https://github.com/vadimdemedes/ink/issues/633\ntest('do not wrap text with non-hyperlink OSC sequences', t => {\n\t// Title-setting OSC followed by visible text\n\tconst text = '\\u001B]0;My Title\\u0007Some text';\n\tconst output = renderToString(\n\t\t<Box width={20}>\n\t\t\t<Text wrap=\"wrap\">{text}</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(stripAnsi(output), 'Some text');\n});\n\n// See https://github.com/vadimdemedes/ink/issues/633\ntest('hard-wrap single-word BEL-terminated OSC hyperlink', t => {\n\t// \"abcdefghij\" is 10 chars, box is 5 wide - forces wrapWord codepath\n\tconst hyperlink =\n\t\t'\\u001B]8;;https://example.com\\u0007abcdefghij\\u001B]8;;\\u0007';\n\tconst output = renderToString(\n\t\t<Box width={5}>\n\t\t\t<Text wrap=\"wrap\">{hyperlink}</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(stripAnsi(output), 'abcde\\nfghij');\n});\n\n// See https://github.com/vadimdemedes/ink/issues/633\ntest('hard-wrap single-word ST-terminated OSC hyperlink', t => {\n\tconst hyperlink =\n\t\t'\\u001B]8;;https://example.com\\u001B\\\\abcdefghij\\u001B]8;;\\u001B\\\\';\n\tconst output = renderToString(\n\t\t<Box width={5}>\n\t\t\t<Text wrap=\"wrap\">{hyperlink}</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(stripAnsi(output), 'abcde\\nfghij');\n});\n\ntest('ignore empty text node', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Box>\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t\t<Text>{''}</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello World');\n});\n\ntest('render a single empty text node', t => {\n\tconst output = renderToString(<Text>{''}</Text>);\n\tt.is(output, '');\n});\n\ntest('number', t => {\n\tconst output = renderToString(<Text>{1}</Text>);\n\n\tt.is(output, '1');\n});\n\ntest('fail when text nodes are not within <Text> component', t => {\n\tlet error: Error | undefined;\n\n\tclass ErrorBoundary extends Component<{children?: React.ReactNode}> {\n\t\toverride render(): React.ReactNode {\n\t\t\treturn this.props.children;\n\t\t}\n\n\t\toverride componentDidCatch(reactError: Error): void {\n\t\t\terror = reactError;\n\t\t}\n\t}\n\n\trenderToString(\n\t\t<ErrorBoundary>\n\t\t\t<Box>\n\t\t\t\tHello\n\t\t\t\t<Text>World</Text>\n\t\t\t</Box>\n\t\t</ErrorBoundary>,\n\t);\n\n\tt.truthy(error);\n\tt.is(\n\t\terror?.message,\n\t\t'Text string \"Hello\" must be rendered inside <Text> component',\n\t);\n});\n\ntest('fail when text node is not within <Text> component', t => {\n\tlet error: Error | undefined;\n\n\tclass ErrorBoundary extends Component<{children?: React.ReactNode}> {\n\t\toverride render(): React.ReactNode {\n\t\t\treturn this.props.children;\n\t\t}\n\n\t\toverride componentDidCatch(reactError: Error): void {\n\t\t\terror = reactError;\n\t\t}\n\t}\n\n\trenderToString(\n\t\t<ErrorBoundary>\n\t\t\t<Box>Hello World</Box>\n\t\t</ErrorBoundary>,\n\t);\n\n\tt.truthy(error);\n\tt.is(\n\t\terror?.message,\n\t\t'Text string \"Hello World\" must be rendered inside <Text> component',\n\t);\n});\n\ntest('fail when <Box> is inside <Text> component', t => {\n\tlet error: Error | undefined;\n\n\tclass ErrorBoundary extends Component<{children?: React.ReactNode}> {\n\t\toverride render(): React.ReactNode {\n\t\t\treturn this.props.children;\n\t\t}\n\n\t\toverride componentDidCatch(reactError: Error): void {\n\t\t\terror = reactError;\n\t\t}\n\t}\n\n\trenderToString(\n\t\t<ErrorBoundary>\n\t\t\t<Text>\n\t\t\t\tHello World\n\t\t\t\t<Box />\n\t\t\t</Text>\n\t\t</ErrorBoundary>,\n\t);\n\n\tt.truthy(error);\n\tt.is((error as any).message, '<Box> can’t be nested inside <Text> component');\n});\n\ntest('remeasure text dimensions on text change', t => {\n\tconst stdout = createStdout();\n\n\tconst {rerender} = render(\n\t\t<Box>\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t\t{stdout, debug: true},\n\t);\n\n\tt.is((stdout.write as any).lastCall.args[0], 'Hello');\n\n\trerender(\n\t\t<Box>\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is((stdout.write as any).lastCall.args[0], 'Hello World');\n});\n\ntest('fragment', t => {\n\tconst output = renderToString(\n\t\t// eslint-disable-next-line react/jsx-no-useless-fragment\n\t\t<>\n\t\t\t<Text>Hello World</Text>\n\t\t</>,\n\t);\n\n\tt.is(output, 'Hello World');\n});\n\ntest('transform children', t => {\n\tconst output = renderToString(\n\t\t<Transform\n\t\t\ttransform={(string: string, index: number) => `[${index}: ${string}]`}\n\t\t>\n\t\t\t<Text>\n\t\t\t\t<Transform\n\t\t\t\t\ttransform={(string: string, index: number) => `{${index}: ${string}}`}\n\t\t\t\t>\n\t\t\t\t\t<Text>test</Text>\n\t\t\t\t</Transform>\n\t\t\t</Text>\n\t\t</Transform>,\n\t);\n\n\tt.is(output, '[0: {0: test}]');\n});\n\ntest('squash multiple text nodes', t => {\n\tconst output = renderToString(\n\t\t<Transform\n\t\t\ttransform={(string: string, index: number) => `[${index}: ${string}]`}\n\t\t>\n\t\t\t<Text>\n\t\t\t\t<Transform\n\t\t\t\t\ttransform={(string: string, index: number) => `{${index}: ${string}}`}\n\t\t\t\t>\n\t\t\t\t\t{/* prettier-ignore */}\n\t\t\t\t\t<Text>hello{' '}world</Text>\n\t\t\t\t</Transform>\n\t\t\t</Text>\n\t\t</Transform>,\n\t);\n\n\tt.is(output, '[0: {0: hello world}]');\n});\n\ntest('transform with multiple lines', t => {\n\tconst output = renderToString(\n\t\t<Transform\n\t\t\ttransform={(string: string, index: number) => `[${index}: ${string}]`}\n\t\t>\n\t\t\t{/* prettier-ignore */}\n\t\t\t<Text>hello{' '}world{'\\n'}goodbye{' '}world</Text>\n\t\t</Transform>,\n\t);\n\n\tt.is(output, '[0: hello world]\\n[1: goodbye world]');\n});\n\ntest('squash multiple nested text nodes', t => {\n\tconst output = renderToString(\n\t\t<Transform\n\t\t\ttransform={(string: string, index: number) => `[${index}: ${string}]`}\n\t\t>\n\t\t\t<Text>\n\t\t\t\t<Transform\n\t\t\t\t\ttransform={(string: string, index: number) => `{${index}: ${string}}`}\n\t\t\t\t>\n\t\t\t\t\thello\n\t\t\t\t\t<Text> world</Text>\n\t\t\t\t</Transform>\n\t\t\t</Text>\n\t\t</Transform>,\n\t);\n\n\tt.is(output, '[0: {0: hello world}]');\n});\n\ntest('squash empty `<Text>` nodes', t => {\n\tconst output = renderToString(\n\t\t<Transform transform={(string: string) => `[${string}]`}>\n\t\t\t<Text>\n\t\t\t\t<Transform transform={(string: string) => `{${string}}`}>\n\t\t\t\t\t<Text>{[]}</Text>\n\t\t\t\t</Transform>\n\t\t\t</Text>\n\t\t</Transform>,\n\t);\n\n\tt.is(output, '');\n});\n\ntest('<Transform> with undefined children', t => {\n\tconst output = renderToString(<Transform transform={children => children} />);\n\tt.is(output, '');\n});\n\ntest('<Transform> with null children', t => {\n\tconst output = renderToString(<Transform transform={children => children} />);\n\tt.is(output, '');\n});\n\ntest('hooks', t => {\n\tfunction WithHooks() {\n\t\tconst [value, setValue] = useState('Hello');\n\n\t\treturn <Text>{value}</Text>;\n\t}\n\n\tconst output = renderToString(<WithHooks />);\n\tt.is(output, 'Hello');\n});\n\ntest('static output', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Static items={['A', 'B', 'C']} style={{paddingBottom: 1}}>\n\t\t\t\t{letter => <Text key={letter}>{letter}</Text>}\n\t\t\t</Static>\n\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\nB\\nC\\n\\n\\nX');\n});\n\ntest('skip previous output when rendering new static output', t => {\n\tconst stdout = createStdout();\n\n\tfunction Dynamic({items}: {readonly items: string[]}) {\n\t\treturn (\n\t\t\t<Static items={items}>{item => <Text key={item}>{item}</Text>}</Static>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Dynamic items={['A']} />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is((stdout.write as any).lastCall.args[0], 'A\\n');\n\n\trerender(<Dynamic items={['A', 'B']} />);\n\tt.is((stdout.write as any).lastCall.args[0], 'A\\nB\\n');\n});\n\ntest('render only new items in static output on final render', t => {\n\tconst stdout = createStdout();\n\n\tfunction Dynamic({items}: {readonly items: string[]}) {\n\t\treturn (\n\t\t\t<Static items={items}>{item => <Text key={item}>{item}</Text>}</Static>\n\t\t);\n\t}\n\n\tconst {rerender, unmount} = render(<Dynamic items={[]} />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is((stdout.write as any).lastCall.args[0], '');\n\n\trerender(<Dynamic items={['A']} />);\n\tt.is((stdout.write as any).lastCall.args[0], 'A\\n');\n\n\trerender(<Dynamic items={['A', 'B']} />);\n\tunmount();\n\n\t// Filter out cursor management escapes (show/hide) to check content writes.\n\t// With isTTY=true, cli-cursor writes a show-cursor sequence on unmount.\n\tconst allWrites = stdout.getWrites();\n\tconst lastContentWrite = allWrites.findLast(w => !w.startsWith('\\u001B[?25'));\n\tt.is(lastContentWrite, 'A\\nB\\n');\n});\n\n// See https://github.com/chalk/wrap-ansi/issues/27\ntest('ensure wrap-ansi doesn’t trim leading whitespace', t => {\n\tconst output = renderToString(<Text color=\"red\">{' ERROR '}</Text>);\n\n\tt.is(output, chalk.red(' ERROR '));\n});\n\ntest('replace child node with text', t => {\n\tconst stdout = createStdout();\n\n\tfunction Dynamic({replace}: {readonly replace?: boolean}) {\n\t\treturn <Text>{replace ? 'x' : <Text color=\"green\">test</Text>}</Text>;\n\t}\n\n\tconst {rerender} = render(<Dynamic />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is((stdout.write as any).lastCall.args[0], chalk.green('test'));\n\n\trerender(<Dynamic replace />);\n\tt.is((stdout.write as any).lastCall.args[0], 'x');\n});\n\n// See https://github.com/vadimdemedes/ink/issues/145\ntest('disable raw mode when all input components are unmounted', t => {\n\tconst stdout = createStdout();\n\n\tconst stdin = new EventEmitter() as NodeJS.WriteStream;\n\tstdin.setEncoding = () => {};\n\tstdin.setRawMode = spy();\n\tstdin.isTTY = true; // Without this, setRawMode will throw\n\tstdin.ref = spy();\n\tstdin.unref = spy();\n\n\tconst options = {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t};\n\n\tclass Input extends React.Component<{setRawMode: (mode: boolean) => void}> {\n\t\toverride render() {\n\t\t\treturn <Text>Test</Text>;\n\t\t}\n\n\t\toverride componentDidMount() {\n\t\t\tthis.props.setRawMode(true);\n\t\t}\n\n\t\toverride componentWillUnmount() {\n\t\t\tthis.props.setRawMode(false);\n\t\t}\n\t}\n\n\tfunction Test({\n\t\trenderFirstInput,\n\t\trenderSecondInput,\n\t}: {\n\t\treadonly renderFirstInput?: boolean;\n\t\treadonly renderSecondInput?: boolean;\n\t}) {\n\t\tconst {setRawMode} = useStdin();\n\n\t\treturn (\n\t\t\t<>\n\t\t\t\t{renderFirstInput ? <Input setRawMode={setRawMode} /> : null}\n\t\t\t\t{renderSecondInput ? <Input setRawMode={setRawMode} /> : null}\n\t\t\t</>\n\t\t);\n\t}\n\n\tconst {rerender} = render(\n\t\t<Test renderFirstInput renderSecondInput />,\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n\t\toptions as any,\n\t);\n\n\tt.true(stdin.setRawMode.calledOnce);\n\tt.true(stdin.ref.calledOnce);\n\tt.deepEqual(stdin.setRawMode.firstCall.args, [true]);\n\n\trerender(<Test renderFirstInput />);\n\n\tt.true(stdin.setRawMode.calledOnce);\n\tt.true(stdin.ref.calledOnce);\n\tt.true(stdin.unref.notCalled);\n\n\trerender(<Test />);\n\n\tt.true(stdin.setRawMode.calledTwice);\n\tt.true(stdin.ref.calledOnce);\n\tt.true(stdin.unref.calledOnce);\n\tt.deepEqual(stdin.setRawMode.lastCall.args, [false]);\n});\n\ntest('re-ref stdin when input is used after previous unmount', t => {\n\tconst stdin = new EventEmitter() as NodeJS.WriteStream;\n\tstdin.setEncoding = () => {};\n\tstdin.read = stub();\n\tstdin.setRawMode = spy();\n\tstdin.isTTY = true; // Without this, setRawMode will throw\n\tstdin.ref = spy();\n\tstdin.unref = spy();\n\n\tconst options = {\n\t\tstdout: createStdout(),\n\t\tstdin,\n\t\tdebug: true,\n\t};\n\n\tclass Input extends React.Component<{setRawMode: (mode: boolean) => void}> {\n\t\toverride render() {\n\t\t\treturn <Text>Test</Text>;\n\t\t}\n\n\t\toverride componentDidMount() {\n\t\t\tthis.props.setRawMode(true);\n\t\t}\n\n\t\toverride componentWillUnmount() {\n\t\t\tthis.props.setRawMode(false);\n\t\t}\n\t}\n\n\tfunction Test({onInput}: {readonly onInput: (input: string) => void}) {\n\t\tconst {setRawMode} = useStdin();\n\t\tuseInput(input => {\n\t\t\tonInput(input);\n\t\t});\n\n\t\treturn <Input setRawMode={setRawMode} />;\n\t}\n\n\tconst onFirstMountInput = spy();\n\tconst onSecondMountInput = spy();\n\n\t// First render\n\tconst {unmount} = render(\n\t\t<Test onInput={onFirstMountInput} />,\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n\t\toptions as any,\n\t);\n\n\tt.true(stdin.ref.calledOnce);\n\tt.true(stdin.setRawMode.calledOnce);\n\tt.deepEqual(stdin.setRawMode.firstCall.args, [true]);\n\temitReadable(stdin, 'a');\n\tt.is(onFirstMountInput.callCount, 1);\n\tt.deepEqual(onFirstMountInput.firstCall.args, ['a']);\n\n\t// Unmount first instance\n\tunmount();\n\n\tt.true(stdin.unref.calledOnce);\n\tt.true(stdin.setRawMode.calledTwice);\n\tt.deepEqual(stdin.setRawMode.lastCall.args, [false]);\n\n\t// Second render with new Ink instance reusing the same stdin\n\tconst {unmount: unmount2} = render(\n\t\t<Test onInput={onSecondMountInput} />,\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n\t\toptions as any,\n\t);\n\n\tt.true(stdin.ref.calledTwice);\n\tt.true(stdin.setRawMode.calledThrice);\n\tt.deepEqual(stdin.setRawMode.lastCall.args, [true]);\n\temitReadable(stdin, 'b');\n\tt.is(onSecondMountInput.callCount, 1);\n\tt.deepEqual(onSecondMountInput.firstCall.args, ['b']);\n\tt.is(onFirstMountInput.callCount, 1);\n\n\t// Unmount second instance\n\tunmount2();\n\n\tt.true(stdin.unref.calledTwice);\n\tt.is(stdin.setRawMode.callCount, 4);\n\tt.deepEqual(stdin.setRawMode.lastCall.args, [false]);\n});\n\ntest('setRawMode() should throw if raw mode is not supported', t => {\n\tconst stdout = createStdout();\n\n\tconst stdin = new EventEmitter() as NodeJS.ReadStream;\n\tstdin.setEncoding = () => {};\n\tstdin.setRawMode = spy();\n\tstdin.isTTY = false;\n\n\tconst didCatchInMount = spy();\n\tconst didCatchInUnmount = spy();\n\n\tconst options = {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t};\n\n\tclass Input extends React.Component<{setRawMode: (mode: boolean) => void}> {\n\t\toverride render() {\n\t\t\treturn <Text>Test</Text>;\n\t\t}\n\n\t\toverride componentDidMount() {\n\t\t\ttry {\n\t\t\t\tthis.props.setRawMode(true);\n\t\t\t} catch (error: unknown) {\n\t\t\t\tdidCatchInMount(error);\n\t\t\t}\n\t\t}\n\n\t\toverride componentWillUnmount() {\n\t\t\ttry {\n\t\t\t\tthis.props.setRawMode(false);\n\t\t\t} catch (error: unknown) {\n\t\t\t\tdidCatchInUnmount(error);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction Test() {\n\t\tconst {setRawMode} = useStdin();\n\t\treturn <Input setRawMode={setRawMode} />;\n\t}\n\n\tconst {unmount} = render(<Test />, options);\n\tunmount();\n\n\tt.is(didCatchInMount.callCount, 1);\n\tt.is(didCatchInUnmount.callCount, 1);\n\tt.false(stdin.setRawMode.called);\n});\n\ntest('render different component based on whether stdin is a TTY or not', t => {\n\tconst stdout = createStdout();\n\n\tconst stdin = new EventEmitter() as NodeJS.WriteStream;\n\tstdin.setEncoding = () => {};\n\tstdin.setRawMode = spy();\n\tstdin.isTTY = false;\n\n\tconst options = {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t};\n\n\tclass Input extends React.Component<{setRawMode: (mode: boolean) => void}> {\n\t\toverride render() {\n\t\t\treturn <Text>Test</Text>;\n\t\t}\n\n\t\toverride componentDidMount() {\n\t\t\tthis.props.setRawMode(true);\n\t\t}\n\n\t\toverride componentWillUnmount() {\n\t\t\tthis.props.setRawMode(false);\n\t\t}\n\t}\n\n\tfunction Test({\n\t\trenderFirstInput,\n\t\trenderSecondInput,\n\t}: {\n\t\treadonly renderFirstInput?: boolean;\n\t\treadonly renderSecondInput?: boolean;\n\t}) {\n\t\tconst {isRawModeSupported, setRawMode} = useStdin();\n\n\t\treturn (\n\t\t\t<>\n\t\t\t\t{isRawModeSupported && renderFirstInput ? (\n\t\t\t\t\t<Input setRawMode={setRawMode} />\n\t\t\t\t) : null}\n\t\t\t\t{isRawModeSupported && renderSecondInput ? (\n\t\t\t\t\t<Input setRawMode={setRawMode} />\n\t\t\t\t) : null}\n\t\t\t</>\n\t\t);\n\t}\n\n\tconst {rerender} = render(\n\t\t<Test renderFirstInput renderSecondInput />,\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n\t\toptions as any,\n\t);\n\n\tt.false(stdin.setRawMode.called);\n\n\trerender(<Test renderFirstInput />);\n\n\tt.false(stdin.setRawMode.called);\n\n\trerender(<Test />);\n\n\tt.false(stdin.setRawMode.called);\n});\n\ntest('render only last frame when run in CI', async t => {\n\tconst output = await run('ci', {\n\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\tenv: {CI: 'true'},\n\t\tcolumns: 0,\n\t});\n\n\tfor (const num of [0, 1, 2, 3, 4]) {\n\t\tt.false(output.includes(`Counter: ${num}`));\n\t}\n\n\tt.true(output.includes('Counter: 5'));\n});\n\ntest('render all frames if CI environment variable equals false', async t => {\n\tconst output = await run('ci', {\n\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\tenv: {CI: 'false'},\n\t\tcolumns: 0,\n\t});\n\n\tfor (const num of [0, 1, 2, 3, 4, 5]) {\n\t\tt.true(output.includes(`Counter: ${num}`));\n\t}\n});\n\ntest('debug mode in CI does not replay final frame during unmount teardown', async t => {\n\tconst output = await run('ci-debug', {\n\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\tenv: {CI: 'true'},\n\t\tcolumns: 0,\n\t});\n\n\tconst plainOutput = stripAnsi(output).replaceAll('\\r', '');\n\tconst helloCount = plainOutput.match(/Hello/g)?.length ?? 0;\n\n\tt.is(helloCount, 2);\n});\n\ntest('debug mode in CI keeps final newline separation after waitUntilExit', async t => {\n\tconst output = await run('ci-debug-after-exit', {\n\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\tenv: {CI: 'true'},\n\t\tcolumns: 0,\n\t});\n\n\tconst plainOutput = stripAnsi(output).replaceAll('\\r', '');\n\tt.is(plainOutput, 'HelloHello\\nDONE');\n});\n\ntest('render only last frame when stdout is not a TTY', async t => {\n\tconst stdout = createStdout(100, false);\n\n\tfunction Counter() {\n\t\tconst [count, setCount] = useState(0);\n\n\t\tReact.useEffect(() => {\n\t\t\tif (count < 3) {\n\t\t\t\tconst timer = setTimeout(() => {\n\t\t\t\t\tsetCount(c => c + 1);\n\t\t\t\t}, 10);\n\n\t\t\t\treturn () => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t};\n\t\t\t}\n\t\t}, [count]);\n\n\t\treturn <Text>Count: {count}</Text>;\n\t}\n\n\tconst {unmount, waitUntilExit} = render(<Counter />, {\n\t\tstdout,\n\t\tdebug: false,\n\t});\n\n\tawait new Promise(resolve => {\n\t\tsetTimeout(resolve, 200);\n\t});\n\n\tunmount();\n\tawait waitUntilExit();\n\n\tconst allWrites = stdout.getWrites();\n\n\t// Verify no intermediate frames were written\n\tconst contentWrites = allWrites.map(w => stripAnsi(w));\n\tfor (const intermediate of ['Count: 0', 'Count: 1', 'Count: 2']) {\n\t\tt.false(\n\t\t\tcontentWrites.some(w => w.includes(intermediate)),\n\t\t\t`Intermediate frame \"${intermediate}\" should not be written in non-interactive mode`,\n\t\t);\n\t}\n\n\t// Verify no erase/cursor ANSI sequences were emitted\n\tconst hasEraseSequence = allWrites.some(w =>\n\t\tw.includes(ansiEscapes.eraseLines(1)),\n\t);\n\tt.false(hasEraseSequence);\n\n\t// Verify the final frame is written\n\tconst lastWrite = allWrites.at(-1) ?? '';\n\tt.true(lastWrite.includes('Count: 3'));\n});\n\ntest('render all frames when interactive is explicitly true', async t => {\n\tconst stdout = createStdout(100, false);\n\n\tfunction Counter() {\n\t\tconst [count, setCount] = useState(0);\n\n\t\tReact.useEffect(() => {\n\t\t\tif (count < 2) {\n\t\t\t\tconst timer = setTimeout(() => {\n\t\t\t\t\tsetCount(c => c + 1);\n\t\t\t\t}, 50);\n\n\t\t\t\treturn () => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t};\n\t\t\t}\n\t\t}, [count]);\n\n\t\treturn <Text>Count: {count}</Text>;\n\t}\n\n\tconst {unmount, waitUntilExit} = render(<Counter />, {\n\t\tstdout,\n\t\tdebug: false,\n\t\tinteractive: true,\n\t});\n\n\tawait new Promise(resolve => {\n\t\tsetTimeout(resolve, 500);\n\t});\n\n\tunmount();\n\tawait waitUntilExit();\n\n\tconst contentWrites = stdout.getWrites().filter(w => w.length > 0);\n\tt.true(contentWrites.length > 1);\n\tconst joined = contentWrites.join('');\n\tt.true(joined.includes('Count: 0'));\n\tt.true(joined.includes('Count: 1'));\n\tt.true(joined.includes('Count: 2'));\n});\n\ntest('interactive option overrides TTY detection', async t => {\n\tconst stdout = createStdout(100, true);\n\n\tfunction Counter() {\n\t\tconst [count, setCount] = useState(0);\n\n\t\tReact.useEffect(() => {\n\t\t\tif (count < 3) {\n\t\t\t\tconst timer = setTimeout(() => {\n\t\t\t\t\tsetCount(c => c + 1);\n\t\t\t\t}, 10);\n\n\t\t\t\treturn () => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t};\n\t\t\t}\n\t\t}, [count]);\n\n\t\treturn <Text>Count: {count}</Text>;\n\t}\n\n\tconst {unmount, waitUntilExit} = render(<Counter />, {\n\t\tstdout,\n\t\tdebug: false,\n\t\tinteractive: false,\n\t});\n\n\tawait new Promise(resolve => {\n\t\tsetTimeout(resolve, 200);\n\t});\n\n\tunmount();\n\tawait waitUntilExit();\n\n\tconst allWrites = stdout.getWrites();\n\n\t// Verify no intermediate frames were written\n\tconst contentWrites = allWrites.map(w => stripAnsi(w));\n\tfor (const intermediate of ['Count: 0', 'Count: 1', 'Count: 2']) {\n\t\tt.false(\n\t\t\tcontentWrites.some(w => w.includes(intermediate)),\n\t\t\t`Intermediate frame \"${intermediate}\" should not be written when interactive=false overrides TTY`,\n\t\t);\n\t}\n\n\t// Verify no erase/cursor ANSI sequences were emitted\n\tconst hasEraseSequence = allWrites.some(w =>\n\t\tw.includes(ansiEscapes.eraseLines(1)),\n\t);\n\tt.false(hasEraseSequence);\n\n\t// Verify only the final frame is written\n\tconst lastWrite = allWrites.at(-1) ?? '';\n\tt.true(lastWrite.includes('Count: 3'));\n});\n\ntest('alternate screen - enters on mount and exits on unmount', async t => {\n\tconst stdout = createStdout(100, true);\n\n\tconst {unmount, waitUntilExit} = render(<Text>Hello</Text>, {\n\t\tstdout,\n\t\talternateScreen: true,\n\t\tinteractive: true,\n\t});\n\n\tunmount();\n\tawait waitUntilExit();\n\n\tconst allWrites = stdout.getWrites();\n\n\tconst enterIndex = allWrites.findIndex(w =>\n\t\tw.includes(ansiEscapes.enterAlternativeScreen),\n\t);\n\tconst exitIndex = allWrites.findLastIndex(w =>\n\t\tw.includes(ansiEscapes.exitAlternativeScreen),\n\t);\n\n\tt.not(enterIndex, -1, 'Should write enterAlternativeScreen on mount');\n\tt.not(exitIndex, -1, 'Should write exitAlternativeScreen on unmount');\n\tt.true(\n\t\tenterIndex < exitIndex,\n\t\t'enterAlternativeScreen must come before exitAlternativeScreen',\n\t);\n\tt.is(enterIndex, 0, 'enterAlternativeScreen should be the first write');\n});\n\ntest.serial(\n\t'primary screen - cleanup console output follows the native console during unmount',\n\tasync t => {\n\t\tconst stdout = createStdout(100, true);\n\t\tconst processStdoutWriteStub = stub(process.stdout, 'write').callsFake(((\n\t\t\t_chunk: string | Uint8Array,\n\t\t\tencoding?: BufferEncoding | ((error?: Error) => void),\n\t\t\tcallback?: (error?: Error) => void,\n\t\t) => {\n\t\t\tif (typeof encoding === 'function') {\n\t\t\t\tencoding();\n\t\t\t}\n\n\t\t\tif (typeof callback === 'function') {\n\t\t\t\tcallback();\n\t\t\t}\n\n\t\t\treturn true;\n\t\t}) as typeof process.stdout.write);\n\t\tt.teardown(() => {\n\t\t\tprocessStdoutWriteStub.restore();\n\t\t});\n\n\t\tfunction Test() {\n\t\t\tuseEffect(() => {\n\t\t\t\treturn () => {\n\t\t\t\t\tconsole.log('primary cleanup');\n\t\t\t\t};\n\t\t\t}, []);\n\n\t\t\treturn <Text>Hello</Text>;\n\t\t}\n\n\t\tconst {unmount, waitUntilExit} = render(<Test />, {\n\t\t\tstdout,\n\t\t\tinteractive: true,\n\t\t});\n\n\t\tunmount();\n\t\tawait waitUntilExit();\n\n\t\tconst output = stdout.getWrites().join('');\n\t\tconst nativeConsoleLog = processStdoutWriteStub\n\t\t\t.getCalls()\n\t\t\t.some(call => String(call.args[0]).includes('primary cleanup'));\n\n\t\tt.false(\n\t\t\toutput.includes('primary cleanup'),\n\t\t\t'Should keep cleanup console output out of Ink-managed stdout writes',\n\t\t);\n\t\tt.true(\n\t\t\tnativeConsoleLog,\n\t\t\t'Should restore the native console before React cleanup runs',\n\t\t);\n\t},\n);\n\ntest.serial(\n\t'alternate screen - does not replay exit(Error) output on the primary screen during unmount',\n\tasync t => {\n\t\tconst stdout = createStdout(100, true);\n\n\t\tfunction Test() {\n\t\t\tconst {exit} = useApp();\n\n\t\t\tuseEffect(() => {\n\t\t\t\texit(new Error('Done'));\n\t\t\t}, [exit]);\n\n\t\t\treturn <Text>Done</Text>;\n\t\t}\n\n\t\tconst {waitUntilExit} = render(<Test />, {\n\t\t\tstdout,\n\t\t\talternateScreen: true,\n\t\t\tinteractive: true,\n\t\t});\n\n\t\tawait t.throwsAsync(waitUntilExit());\n\n\t\tconst allWrites = stdout.getWrites();\n\t\tconst exitIndex = allWrites.findLastIndex(write =>\n\t\t\twrite.includes(ansiEscapes.exitAlternativeScreen),\n\t\t);\n\t\tconst replayedErrorOutput = allWrites.slice(exitIndex + 1).some(write => {\n\t\t\tconst plainWrite = stripAnsi(write);\n\t\t\treturn (\n\t\t\t\tplainWrite.includes('Error: Done') ||\n\t\t\t\tplainWrite.includes('Done\\n    at')\n\t\t\t);\n\t\t});\n\n\t\tt.not(exitIndex, -1, 'Should exit the alternate screen on unmount');\n\t\tt.false(\n\t\t\treplayedErrorOutput,\n\t\t\t'Should not replay alternate-screen diagnostics onto the primary screen',\n\t\t);\n\t},\n);\n\ntest.serial(\n\t'alternate screen - does not replay teardown output on the primary screen during unmount',\n\tasync t => {\n\t\tconst stdout = createStdout(100, true);\n\n\t\tfunction Test() {\n\t\t\tconst {exit} = useApp();\n\n\t\t\tuseEffect(() => {\n\t\t\t\texit(new Error('Done'));\n\t\t\t}, [exit]);\n\n\t\t\treturn <Text>normal ERROR banner</Text>;\n\t\t}\n\n\t\tconst {waitUntilExit} = render(<Test />, {\n\t\t\tstdout,\n\t\t\talternateScreen: true,\n\t\t\tinteractive: true,\n\t\t});\n\n\t\tawait t.throwsAsync(waitUntilExit());\n\n\t\tconst allWrites = stdout.getWrites();\n\t\tconst exitIndex = allWrites.findLastIndex(write =>\n\t\t\twrite.includes(ansiEscapes.exitAlternativeScreen),\n\t\t);\n\t\tconst replayedOutput = stripAnsi(allWrites.slice(exitIndex + 1).join(''));\n\n\t\tt.not(exitIndex, -1, 'Should exit the alternate screen on unmount');\n\t\tt.false(\n\t\t\treplayedOutput.includes('normal ERROR banner') ||\n\t\t\t\treplayedOutput.includes('Error: Done') ||\n\t\t\t\treplayedOutput.includes('Done\\n    at'),\n\t\t\t'Should not replay alternate-screen teardown output onto the primary screen',\n\t\t);\n\t},\n);\n\ntest.serial(\n\t'alternate screen - cleanup console output follows the native console during unmount',\n\tasync t => {\n\t\tconst stdout = createStdout(100, true);\n\t\tconst processStdoutWriteStub = stub(process.stdout, 'write').callsFake(((\n\t\t\t_chunk: string | Uint8Array,\n\t\t\tencoding?: BufferEncoding | ((error?: Error) => void),\n\t\t\tcallback?: (error?: Error) => void,\n\t\t) => {\n\t\t\tif (typeof encoding === 'function') {\n\t\t\t\tencoding();\n\t\t\t}\n\n\t\t\tif (typeof callback === 'function') {\n\t\t\t\tcallback();\n\t\t\t}\n\n\t\t\treturn true;\n\t\t}) as typeof process.stdout.write);\n\t\tt.teardown(() => {\n\t\t\tprocessStdoutWriteStub.restore();\n\t\t});\n\n\t\tfunction Test() {\n\t\t\tuseEffect(() => {\n\t\t\t\treturn () => {\n\t\t\t\t\tconsole.log('cleanup log');\n\t\t\t\t};\n\t\t\t}, []);\n\n\t\t\treturn <Text>Hello</Text>;\n\t\t}\n\n\t\tconst {unmount, waitUntilExit} = render(<Test />, {\n\t\t\tstdout,\n\t\t\talternateScreen: true,\n\t\t\tinteractive: true,\n\t\t});\n\n\t\tunmount();\n\t\tawait waitUntilExit();\n\n\t\tconst output = stdout.getWrites().join('');\n\t\tconst nativeConsoleLog = processStdoutWriteStub\n\t\t\t.getCalls()\n\t\t\t.some(call => String(call.args[0]).includes('cleanup log'));\n\n\t\tt.false(\n\t\t\toutput.includes('cleanup log'),\n\t\t\t'Should keep cleanup console output out of the alternate-screen stream',\n\t\t);\n\t\tt.true(\n\t\t\tnativeConsoleLog,\n\t\t\t'Should restore the native console before React cleanup runs',\n\t\t);\n\t},\n);\n\ntest.serial(\n\t'alternate screen - cleanup() exits the alternate screen',\n\tasync t => {\n\t\tconst stdout = createStdout(100, true);\n\n\t\tconst {cleanup, waitUntilExit} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t\talternateScreen: true,\n\t\t\tinteractive: true,\n\t\t});\n\n\t\tcleanup();\n\t\tawait waitUntilExit();\n\n\t\tconst allWrites = stdout.getWrites();\n\t\tconst exitIndex = allWrites.findLastIndex(write =>\n\t\t\twrite.includes(ansiEscapes.exitAlternativeScreen),\n\t\t);\n\n\t\tt.not(exitIndex, -1, 'Should exit the alternate screen during cleanup()');\n\t},\n);\n\ntest.serial(\n\t'alternate screen - debug concurrent teardown restores the cursor before the first commit',\n\tasync t => {\n\t\tconst stdout = createStdout(100, true);\n\t\tconst showCursorEscape = '\\u001B[?25h';\n\n\t\tconst {unmount, waitUntilExit} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t\talternateScreen: true,\n\t\t\tconcurrent: true,\n\t\t\tdebug: true,\n\t\t});\n\n\t\tunmount();\n\t\tawait waitUntilExit();\n\n\t\tconst output = stdout.getWrites().join('');\n\t\tconst exitIndex = output.lastIndexOf(ansiEscapes.exitAlternativeScreen);\n\t\tconst showCursorIndex = output.lastIndexOf(showCursorEscape);\n\n\t\tt.not(exitIndex, -1, 'Should exit the alternate screen on unmount');\n\t\tt.true(\n\t\t\tshowCursorIndex > exitIndex,\n\t\t\t'Should restore the cursor after leaving the alternate screen',\n\t\t);\n\t},\n);\n\ntest('render warns when stdout is reused before unmount', async t => {\n\tconst stdout = createStdout(100, true);\n\tconst processStderrWriteStub = stub(process.stderr, 'write').callsFake(((\n\t\t_chunk: string | Uint8Array,\n\t\tencoding?: BufferEncoding | ((error?: Error) => void),\n\t\tcallback?: (error?: Error) => void,\n\t) => {\n\t\tif (typeof encoding === 'function') {\n\t\t\tencoding();\n\t\t}\n\n\t\tif (typeof callback === 'function') {\n\t\t\tcallback();\n\t\t}\n\n\t\treturn true;\n\t}) as typeof process.stderr.write);\n\tt.teardown(() => {\n\t\tprocessStderrWriteStub.restore();\n\t});\n\n\trender(<Text>Primary screen</Text>, {\n\t\tstdout,\n\t\tinteractive: true,\n\t\talternateScreen: true,\n\t\tpatchConsole: false,\n\t});\n\n\tconst {unmount, waitUntilExit} = render(<Text>Second render</Text>, {\n\t\tstdout,\n\t});\n\n\tt.true(\n\t\tprocessStderrWriteStub.calledOnceWithExactly(\n\t\t\t'Warning: render() was called again for the same stdout before the previous Ink instance was unmounted. Reusing stdout across multiple render() calls is unsupported. Call unmount() first.\\n',\n\t\t),\n\t);\n\n\tunmount();\n\tawait waitUntilExit();\n});\n\ntest('alternate screen - ignored when non-interactive', async t => {\n\tconst stdout = createStdout(100, true);\n\n\tconst {unmount, waitUntilExit} = render(<Text>Hello</Text>, {\n\t\tstdout,\n\t\talternateScreen: true,\n\t\tinteractive: false,\n\t});\n\n\tunmount();\n\tawait waitUntilExit();\n\n\tconst allWrites = stdout.getWrites();\n\n\tt.false(\n\t\tallWrites.some(w => w.includes(ansiEscapes.enterAlternativeScreen)),\n\t\t'Should not write enterAlternativeScreen in non-interactive mode',\n\t);\n\tt.false(\n\t\tallWrites.some(w => w.includes(ansiEscapes.exitAlternativeScreen)),\n\t\t'Should not write exitAlternativeScreen in non-interactive mode',\n\t);\n});\n\ntest('alternate screen - disabled by default', async t => {\n\tconst stdout = createStdout(100, true);\n\n\tconst {unmount, waitUntilExit} = render(<Text>Hello</Text>, {\n\t\tstdout,\n\t\tinteractive: true,\n\t});\n\n\tunmount();\n\tawait waitUntilExit();\n\n\tconst allWrites = stdout.getWrites();\n\n\tt.false(\n\t\tallWrites.some(w => w.includes(ansiEscapes.enterAlternativeScreen)),\n\t\t'Should not write enterAlternativeScreen by default',\n\t);\n\tt.false(\n\t\tallWrites.some(w => w.includes(ansiEscapes.exitAlternativeScreen)),\n\t\t'Should not write exitAlternativeScreen by default',\n\t);\n});\n\ntest('alternate screen - content is rendered between enter and exit', async t => {\n\tconst stdout = createStdout(100, true);\n\n\tconst {unmount, waitUntilExit} = render(<Text>Hello</Text>, {\n\t\tstdout,\n\t\talternateScreen: true,\n\t\tinteractive: true,\n\t});\n\n\tunmount();\n\tawait waitUntilExit();\n\n\tconst allWrites = stdout.getWrites();\n\n\tconst enterIndex = allWrites.findIndex(w =>\n\t\tw.includes(ansiEscapes.enterAlternativeScreen),\n\t);\n\tconst exitIndex = allWrites.findLastIndex(w =>\n\t\tw.includes(ansiEscapes.exitAlternativeScreen),\n\t);\n\n\tt.not(enterIndex, -1);\n\tt.not(exitIndex, -1);\n\tt.true(enterIndex < exitIndex);\n\n\tconst contentBetween = allWrites\n\t\t.slice(enterIndex + 1, exitIndex)\n\t\t.some(w => stripAnsi(w).includes('Hello'));\n\tt.true(\n\t\tcontentBetween,\n\t\t'Rendered content should appear between enter and exit',\n\t);\n});\n\ntest('alternate screen - ignored when isTTY is false', async t => {\n\tconst stdout = createStdout(100, false);\n\n\tconst {unmount, waitUntilExit} = render(<Text>Hello</Text>, {\n\t\tstdout,\n\t\talternateScreen: true,\n\t});\n\n\tunmount();\n\tawait waitUntilExit();\n\n\tconst allWrites = stdout.getWrites();\n\n\tt.false(\n\t\tallWrites.some(w => w.includes(ansiEscapes.enterAlternativeScreen)),\n\t\t'Should not write enterAlternativeScreen when isTTY is false',\n\t);\n\tt.false(\n\t\tallWrites.some(w => w.includes(ansiEscapes.exitAlternativeScreen)),\n\t\t'Should not write exitAlternativeScreen when isTTY is false',\n\t);\n});\n\ntest('alternate screen - ignored when isTTY is false even if interactive is true', async t => {\n\tconst stdout = createStdout(100, false);\n\n\tconst {unmount, waitUntilExit} = render(<Text>Hello</Text>, {\n\t\tstdout,\n\t\talternateScreen: true,\n\t\tinteractive: true,\n\t});\n\n\tunmount();\n\tawait waitUntilExit();\n\n\tconst allWrites = stdout.getWrites();\n\n\tt.false(\n\t\tallWrites.some(w => w.includes(ansiEscapes.enterAlternativeScreen)),\n\t\t'Should not write enterAlternativeScreen when isTTY is false, even with interactive=true',\n\t);\n\tt.false(\n\t\tallWrites.some(w => w.includes(ansiEscapes.exitAlternativeScreen)),\n\t\t'Should not write exitAlternativeScreen when isTTY is false, even with interactive=true',\n\t);\n});\n\ntest('static output is written immediately in non-interactive mode', async t => {\n\tconst stdout = createStdout(100, false);\n\n\tfunction App() {\n\t\tconst [items, setItems] = useState(['A']);\n\n\t\tReact.useEffect(() => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tsetItems(['A', 'B']);\n\t\t\t}, 10);\n\n\t\t\treturn () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t};\n\t\t}, []);\n\n\t\treturn (\n\t\t\t<Box>\n\t\t\t\t<Static items={items}>{item => <Text key={item}>{item}</Text>}</Static>\n\t\t\t\t<Text>Dynamic</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {unmount, waitUntilExit} = render(<App />, {\n\t\tstdout,\n\t\tdebug: false,\n\t});\n\n\tawait new Promise(resolve => {\n\t\tsetTimeout(resolve, 200);\n\t});\n\n\t// Capture writes BEFORE unmount — static items must already be here\n\tconst writesBeforeUnmount = stdout.getWrites().map(w => stripAnsi(w));\n\tconst preUnmountJoined = writesBeforeUnmount.join('');\n\tt.true(\n\t\tpreUnmountJoined.includes('A'),\n\t\t'Static item A was written before unmount',\n\t);\n\tt.true(\n\t\tpreUnmountJoined.includes('B'),\n\t\t'Static item B was written before unmount',\n\t);\n\n\tunmount();\n\tawait waitUntilExit();\n\n\t// Verify the dynamic content was deferred to unmount (not written before it)\n\tt.false(\n\t\tpreUnmountJoined.includes('Dynamic'),\n\t\t'Dynamic content was not written before unmount',\n\t);\n\n\t// Verify dynamic content was eventually written\n\tconst allWrites = stdout.getWrites().map(w => stripAnsi(w));\n\tt.true(\n\t\tallWrites.join('').includes('Dynamic'),\n\t\t'Dynamic content was eventually written',\n\t);\n});\n\ntest('reset prop when it’s removed from the element', t => {\n\tconst stdout = createStdout();\n\n\tfunction Dynamic({remove}: {readonly remove?: boolean}) {\n\t\treturn (\n\t\t\t<Box\n\t\t\t\tflexDirection=\"column\"\n\t\t\t\tjustifyContent=\"flex-end\"\n\t\t\t\theight={remove ? undefined : 4}\n\t\t\t>\n\t\t\t\t<Text>x</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Dynamic />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is((stdout.write as any).lastCall.args[0], '\\n\\n\\nx');\n\n\trerender(<Dynamic remove />);\n\tt.is((stdout.write as any).lastCall.args[0], 'x');\n});\n\ntest('newline', t => {\n\tconst output = renderToString(\n\t\t<Text>\n\t\t\tHello\n\t\t\t<Newline />\n\t\t\tWorld\n\t\t</Text>,\n\t);\n\tt.is(output, 'Hello\\nWorld');\n});\n\ntest('multiple newlines', t => {\n\tconst output = renderToString(\n\t\t<Text>\n\t\t\tHello\n\t\t\t<Newline count={2} />\n\t\t\tWorld\n\t\t</Text>,\n\t);\n\tt.is(output, 'Hello\\n\\nWorld');\n});\n\ntest('horizontal spacer', t => {\n\tconst output = renderToString(\n\t\t<Box width={20}>\n\t\t\t<Text>Left</Text>\n\t\t\t<Spacer />\n\t\t\t<Text>Right</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Left           Right');\n});\n\ntest('vertical spacer', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" height={6}>\n\t\t\t<Text>Top</Text>\n\t\t\t<Spacer />\n\t\t\t<Text>Bottom</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Top\\n\\n\\n\\n\\nBottom');\n});\n\ntest('link ansi escapes are closed properly', t => {\n\tconst output = renderToString(\n\t\t<Text>{ansiEscapes.link('Example', 'https://example.com')}</Text>,\n\t);\n\n\tt.is(output, '\u001b]8;;https://example.com\u0007Example\u001b]8;;\u0007');\n});\n\n// Concurrent mode tests\ntest('text - concurrent', async t => {\n\tconst output = await renderToStringAsync(<Text>Hello World</Text>);\n\tt.is(output, 'Hello World');\n});\n\ntest('multiple text nodes - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Text>\n\t\t\t{'Hello'}\n\t\t\t{' World'}\n\t\t</Text>,\n\t);\n\tt.is(output, 'Hello World');\n});\n\ntest('wrap text - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box width={7}>\n\t\t\t<Text wrap=\"wrap\">Hello World</Text>\n\t\t</Box>,\n\t);\n\tt.is(output, 'Hello\\nWorld');\n});\n\ntest('truncate text in the end - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box width={7}>\n\t\t\t<Text wrap=\"truncate\">Hello World</Text>\n\t\t</Box>,\n\t);\n\tt.is(output, 'Hello …');\n});\n\ntest('transform children - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Transform\n\t\t\ttransform={(string: string, index: number) => `[${index}: ${string}]`}\n\t\t>\n\t\t\t<Text>\n\t\t\t\t<Transform\n\t\t\t\t\ttransform={(string: string, index: number) => `{${index}: ${string}}`}\n\t\t\t\t>\n\t\t\t\t\t<Text>test</Text>\n\t\t\t\t</Transform>\n\t\t\t</Text>\n\t\t</Transform>,\n\t);\n\tt.is(output, '[0: {0: test}]');\n});\n\ntest('static output - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box>\n\t\t\t<Static items={['A', 'B', 'C']} style={{paddingBottom: 1}}>\n\t\t\t\t{letter => <Text key={letter}>{letter}</Text>}\n\t\t\t</Static>\n\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\tt.is(output, 'A\\nB\\nC\\n\\n\\nX');\n});\n\ntest('remeasure text dimensions on text change - concurrent', async t => {\n\tconst {getOutput, rerenderAsync} = await renderAsync(\n\t\t<Box>\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t);\n\tt.is(getOutput(), 'Hello');\n\n\tawait rerenderAsync(\n\t\t<Box>\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\tt.is(getOutput(), 'Hello World');\n});\n\ntest('newline - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Text>\n\t\t\tHello\n\t\t\t<Newline />\n\t\t\tWorld\n\t\t</Text>,\n\t);\n\tt.is(output, 'Hello\\nWorld');\n});\n\ntest('horizontal spacer - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box width={20}>\n\t\t\t<Text>Left</Text>\n\t\t\t<Spacer />\n\t\t\t<Text>Right</Text>\n\t\t</Box>,\n\t);\n\tt.is(output, 'Left           Right');\n});\n\ntest('vertical spacer - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box flexDirection=\"column\" height={6}>\n\t\t\t<Text>Top</Text>\n\t\t\t<Spacer />\n\t\t\t<Text>Bottom</Text>\n\t\t</Box>,\n\t);\n\tt.is(output, 'Top\\n\\n\\n\\n\\nBottom');\n});\n"
  },
  {
    "path": "test/cursor-helpers.tsx",
    "content": "import test from 'ava';\nimport ansiEscapes from 'ansi-escapes';\nimport {\n\tcursorPositionChanged,\n\tbuildCursorSuffix,\n\tbuildReturnToBottom,\n\tbuildCursorOnlySequence,\n\tbuildReturnToBottomPrefix,\n} from '../src/cursor-helpers.js';\n\nconst showCursorEscape = '\\u001B[?25h';\nconst hideCursorEscape = '\\u001B[?25l';\n\n// CursorPositionChanged\n\ntest('cursorPositionChanged - both undefined returns false', t => {\n\tt.false(cursorPositionChanged(undefined, undefined));\n});\n\ntest('cursorPositionChanged - same position returns false', t => {\n\tt.false(cursorPositionChanged({x: 1, y: 2}, {x: 1, y: 2}));\n});\n\ntest('cursorPositionChanged - different x returns true', t => {\n\tt.true(cursorPositionChanged({x: 1, y: 2}, {x: 3, y: 2}));\n});\n\ntest('cursorPositionChanged - different y returns true', t => {\n\tt.true(cursorPositionChanged({x: 1, y: 2}, {x: 1, y: 3}));\n});\n\ntest('cursorPositionChanged - undefined vs defined returns true', t => {\n\tt.true(cursorPositionChanged(undefined, {x: 0, y: 0}));\n\tt.true(cursorPositionChanged({x: 0, y: 0}, undefined));\n});\n\n// BuildCursorSuffix\n\ntest('buildCursorSuffix - returns empty string when cursorPosition is undefined', t => {\n\tt.is(buildCursorSuffix(3, undefined), '');\n});\n\ntest('buildCursorSuffix - moves up and positions cursor', t => {\n\tconst result = buildCursorSuffix(3, {x: 5, y: 1});\n\tt.is(\n\t\tresult,\n\t\tansiEscapes.cursorUp(2) + ansiEscapes.cursorTo(5) + showCursorEscape,\n\t);\n});\n\ntest('buildCursorSuffix - no cursorUp when cursor is at last visible line', t => {\n\tconst result = buildCursorSuffix(3, {x: 0, y: 3});\n\tt.is(result, ansiEscapes.cursorTo(0) + showCursorEscape);\n});\n\ntest('buildCursorSuffix - cursor at first line of single-line output', t => {\n\tconst result = buildCursorSuffix(1, {x: 4, y: 0});\n\tt.is(\n\t\tresult,\n\t\tansiEscapes.cursorUp(1) + ansiEscapes.cursorTo(4) + showCursorEscape,\n\t);\n});\n\n// BuildReturnToBottom\n\ntest('buildReturnToBottom - returns empty string when previousCursorPosition is undefined', t => {\n\tt.is(buildReturnToBottom(4, undefined), '');\n});\n\ntest('buildReturnToBottom - moves down to bottom', t => {\n\tconst result = buildReturnToBottom(4, {x: 5, y: 0});\n\tt.is(result, ansiEscapes.cursorDown(3) + ansiEscapes.cursorTo(0));\n});\n\ntest('buildReturnToBottom - no cursorDown when cursor already at bottom', t => {\n\tconst result = buildReturnToBottom(4, {x: 0, y: 3});\n\tt.is(result, ansiEscapes.cursorTo(0));\n});\n\n// BuildCursorOnlySequence\n\ntest('buildCursorOnlySequence - builds full sequence with hide prefix when cursor was shown', t => {\n\tconst result = buildCursorOnlySequence({\n\t\tcursorWasShown: true,\n\t\tpreviousLineCount: 2,\n\t\tpreviousCursorPosition: {x: 0, y: 0},\n\t\tvisibleLineCount: 1,\n\t\tcursorPosition: {x: 3, y: 0},\n\t});\n\tconst expected =\n\t\thideCursorEscape +\n\t\tbuildReturnToBottom(2, {x: 0, y: 0}) +\n\t\tbuildCursorSuffix(1, {x: 3, y: 0});\n\tt.is(result, expected);\n});\n\ntest('buildCursorOnlySequence - no hide prefix when cursor was not shown', t => {\n\tconst result = buildCursorOnlySequence({\n\t\tcursorWasShown: false,\n\t\tpreviousLineCount: 0,\n\t\tpreviousCursorPosition: undefined,\n\t\tvisibleLineCount: 1,\n\t\tcursorPosition: {x: 3, y: 0},\n\t});\n\tt.false(result.startsWith(hideCursorEscape));\n\tt.true(result.includes(showCursorEscape));\n});\n\n// BuildReturnToBottomPrefix\n\ntest('buildReturnToBottomPrefix - returns empty string when cursor was not shown', t => {\n\tt.is(buildReturnToBottomPrefix(false, 4, {x: 0, y: 0}), '');\n});\n\ntest('buildReturnToBottomPrefix - returns hide + returnToBottom when cursor was shown', t => {\n\tconst result = buildReturnToBottomPrefix(true, 4, {x: 0, y: 0});\n\tt.is(result, hideCursorEscape + buildReturnToBottom(4, {x: 0, y: 0}));\n});\n\ntest('buildReturnToBottomPrefix - with undefined previousCursorPosition still hides cursor', t => {\n\tconst result = buildReturnToBottomPrefix(true, 4, undefined);\n\tt.is(result, hideCursorEscape + buildReturnToBottom(4, undefined));\n});\n"
  },
  {
    "path": "test/cursor.tsx",
    "content": "import test, {type ExecutionContext} from 'ava';\nimport React, {Suspense, act, useEffect, useState} from 'react';\nimport ansiEscapes from 'ansi-escapes';\nimport delay from 'delay';\nimport {\n\trender,\n\tBox,\n\tText,\n\tuseInput,\n\tuseCursor,\n\tuseStdout,\n\tuseStderr,\n} from '../src/index.js';\nimport {createStdin, emitReadable} from './helpers/create-stdin.js';\nimport createStdout from './helpers/create-stdout.js';\n\nconst showCursorEscape = '\\u001B[?25h';\nconst hideCursorEscape = '\\u001B[?25l';\n\nconst getWriteCalls = (stream: NodeJS.WriteStream): string[] => {\n\tconst writes: string[] = [];\n\tfor (let i = 0; i < (stream.write as any).callCount; i++) {\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-call\n\t\twrites.push((stream.write as any).getCall(i).args[0] as string);\n\t}\n\n\treturn writes;\n};\n\nconst waitForCondition = async (condition: () => boolean): Promise<void> => {\n\tif (condition()) {\n\t\treturn;\n\t}\n\n\tconst timeoutMs = 2000;\n\tconst intervalMs = 10;\n\tconst maxAttempts = Math.ceil(timeoutMs / intervalMs);\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tlet attempts = 0;\n\t\tconst interval = setInterval(() => {\n\t\t\ttry {\n\t\t\t\tif (condition()) {\n\t\t\t\t\tclearInterval(interval);\n\t\t\t\t\tresolve();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tclearInterval(interval);\n\t\t\t\treject(\n\t\t\t\t\terror instanceof Error ? error : new Error('Condition check threw'),\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tattempts++;\n\t\t\tif (attempts >= maxAttempts) {\n\t\t\t\tclearInterval(interval);\n\t\t\t\treject(new Error(`Condition was not met in ${timeoutMs}ms`));\n\t\t\t}\n\t\t}, intervalMs);\n\t});\n};\n\nfunction InputApp() {\n\tconst [text, setText] = useState('');\n\tconst {setCursorPosition} = useCursor();\n\n\tuseInput((input, key) => {\n\t\tif (key.backspace || key.delete) {\n\t\t\tsetText(prev => prev.slice(0, -1));\n\t\t\treturn;\n\t\t}\n\n\t\tif (!key.ctrl && !key.meta && input) {\n\t\t\tsetText(prev => prev + input);\n\t\t}\n\t});\n\n\tsetCursorPosition({x: 2 + text.length, y: 0});\n\n\treturn (\n\t\t<Box>\n\t\t\t<Text>{`> ${text}`}</Text>\n\t\t</Box>\n\t);\n}\n\ntest.serial('cursor is shown at specified position after render', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\n\tconst {unmount} = render(<InputApp />, {stdout, stdin});\n\tawait delay(50);\n\n\t// With isTTY=true, cli-cursor writes cursor escape sequences as separate\n\t// stdout.write calls (synchronized output wrappers), so we check the\n\t// combined output of the first render rather than a single firstCall.\n\tconst firstRenderOutput = getWriteCalls(stdout).join('');\n\t// Cursor should be shown at x=2 (after \"> \")\n\tt.true(\n\t\tfirstRenderOutput.includes(showCursorEscape),\n\t\t'cursor should be visible after first render',\n\t);\n\tt.true(\n\t\tfirstRenderOutput.includes(ansiEscapes.cursorTo(2)),\n\t\t'cursor should be at column 2',\n\t);\n\n\tunmount();\n});\n\ntest.serial('cursor is not hidden by useEffect after first render', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\n\tconst {unmount} = render(<InputApp />, {stdout, stdin});\n\tawait delay(50);\n\n\t// Check all writes after the first render — none should be a bare hideCursorEscape\n\t// that would undo the showCursorEscape from log-update.\n\t// The last write to stdout should contain showCursorEscape (from log-update),\n\t// not be followed by a separate hideCursorEscape write from App.tsx useEffect.\n\tconst output = getWriteCalls(stdout).join('');\n\tconst lastShowIndex = output.lastIndexOf(showCursorEscape);\n\tconst lastHideIndex = output.lastIndexOf(hideCursorEscape);\n\n\tt.true(\n\t\tlastShowIndex > lastHideIndex,\n\t\t'last cursor visibility change should be SHOW, not HIDE',\n\t);\n\n\tunmount();\n});\n\ntest.serial('cursor follows text input', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\n\tconst {unmount} = render(<InputApp />, {stdout, stdin});\n\tawait delay(50);\n\n\temitReadable(stdin, 'a');\n\tawait delay(50);\n\n\t// With isTTY=true, stdout.get() (lastCall) may be a synchronized output\n\t// wrapper rather than the render content, so check all writes combined.\n\tconst allOutput = getWriteCalls(stdout).join('');\n\t// After typing 'a', cursor should be at x=3 (\"> a\" = 3 chars)\n\tt.true(allOutput.includes(showCursorEscape));\n\tt.true(\n\t\tallOutput.includes(ansiEscapes.cursorTo(3)),\n\t\t'cursor should move to column 3 after typing \"a\"',\n\t);\n\n\tunmount();\n});\n\ntest.serial(\n\t'cursor moves on space input even when output is identical',\n\tasync t => {\n\t\tconst stdout = createStdout();\n\t\tconst stdin = createStdin();\n\n\t\tconst {unmount} = render(<InputApp />, {stdout, stdin});\n\t\tawait delay(50);\n\n\t\temitReadable(stdin, 'a');\n\t\tawait delay(50);\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n\t\tconst afterA = (stdout.write as any).callCount;\n\n\t\temitReadable(stdin, ' ');\n\t\tawait delay(50);\n\n\t\t// Space adds to text, cursor should move even if Ink output looks the same (padded)\n\t\tt.true(\n\t\t\t(stdout.write as any).callCount > afterA,\n\t\t\t'should write to stdout after space input',\n\t\t);\n\n\t\t// With isTTY=true, stdout.get() (lastCall) may be a synchronized output\n\t\t// wrapper rather than the render content, so check all writes combined.\n\t\tconst allOutput = getWriteCalls(stdout).join('');\n\t\t// After \"a \", cursor should be at x=4\n\t\tt.true(\n\t\t\tallOutput.includes(ansiEscapes.cursorTo(4)),\n\t\t\t'cursor should be at column 4 after \"a \"',\n\t\t);\n\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'cursor is cleared when component using useCursor unmounts',\n\tasync t => {\n\t\tconst stdout = createStdout();\n\t\tconst stdin = createStdin();\n\n\t\tfunction CursorChild() {\n\t\t\tconst {setCursorPosition} = useCursor();\n\t\t\tsetCursorPosition({x: 5, y: 0});\n\t\t\treturn <Text>child</Text>;\n\t\t}\n\n\t\tfunction Parent() {\n\t\t\tconst [showChild, setShowChild] = useState(true);\n\n\t\t\tuseInput((_input, key) => {\n\t\t\t\tif (key.return) {\n\t\t\t\t\tsetShowChild(false);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\treturn <Box>{showChild ? <CursorChild /> : <Text>no cursor</Text>}</Box>;\n\t\t}\n\n\t\tconst {unmount} = render(<Parent />, {stdout, stdin});\n\t\tawait delay(50);\n\n\t\t// With isTTY=true, cli-cursor writes cursor escape sequences as separate\n\t\t// stdout.write calls, so check the combined initial render output.\n\t\tconst initialRenderOutput = getWriteCalls(stdout).join('');\n\t\tt.true(\n\t\t\tinitialRenderOutput.includes(showCursorEscape),\n\t\t\t'cursor should be visible initially',\n\t\t);\n\n\t\tconst writesBeforeEnter = (stdout.write as any).callCount as number;\n\n\t\t// Unmount the child by pressing Enter\n\t\temitReadable(stdin, '\\r');\n\t\tawait delay(50);\n\n\t\t// After child unmounts, cursor position should be cleared.\n\t\t// Only look at writes after the initial render to avoid counting\n\t\t// the initial render's cursor sequences.\n\t\tconst outputAfterChildUnmount = getWriteCalls(stdout)\n\t\t\t.slice(writesBeforeEnter)\n\t\t\t.join('');\n\t\tconst lastShowIndex = outputAfterChildUnmount.lastIndexOf(showCursorEscape);\n\t\tconst lastHideIndex = outputAfterChildUnmount.lastIndexOf(hideCursorEscape);\n\t\tt.true(\n\t\t\tlastHideIndex > lastShowIndex,\n\t\t\t'cursor should be hidden after child with useCursor unmounts',\n\t\t);\n\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'cursor position does not leak from suspended concurrent render to fallback',\n\tasync t => {\n\t\tconst stdout = createStdout();\n\t\tconst stdin = createStdin();\n\n\t\tlet resolvePromise: () => void;\n\t\tconst promise = new Promise<void>(resolve => {\n\t\t\tresolvePromise = resolve;\n\t\t});\n\n\t\tlet suspended = true;\n\n\t\tfunction CursorChild() {\n\t\t\tconst {setCursorPosition} = useCursor();\n\t\t\tsetCursorPosition({x: 5, y: 0}); // Render-phase side effect\n\t\t\tif (suspended) {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/only-throw-error\n\t\t\t\tthrow promise;\n\t\t\t}\n\n\t\t\treturn <Text>loaded</Text>;\n\t\t}\n\n\t\tfunction Test() {\n\t\t\treturn (\n\t\t\t\t<Suspense fallback={<Text>loading</Text>}>\n\t\t\t\t\t<CursorChild />\n\t\t\t\t</Suspense>\n\t\t\t);\n\t\t}\n\n\t\tawait act(async () => {\n\t\t\trender(<Test />, {stdout, stdin, concurrent: true});\n\t\t});\n\n\t\tconst fallbackOutput = getWriteCalls(stdout).join('');\n\t\tt.true(fallbackOutput.includes('loading'));\n\t\tt.false(\n\t\t\tfallbackOutput.includes(showCursorEscape),\n\t\t\t'fallback output should not contain show cursor escape from suspended concurrent render',\n\t\t);\n\n\t\t// Cleanup: resolve promise and unmount\n\t\tsuspended = false;\n\t\tresolvePromise!();\n\t\tawait act(async () => {\n\t\t\tawait delay(50);\n\t\t});\n\t},\n);\n\ntest.serial('screen does not scroll up on subsequent renders', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\n\tfunction MultiLineApp() {\n\t\tconst [text, setText] = useState('');\n\t\tconst {setCursorPosition} = useCursor();\n\n\t\tuseInput((input, key) => {\n\t\t\tif (!key.ctrl && !key.meta && input) {\n\t\t\t\tsetText(prev => prev + input);\n\t\t\t}\n\t\t});\n\n\t\tsetCursorPosition({x: 2 + text.length, y: 1});\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Text>Header</Text>\n\t\t\t\t<Text>{`> ${text}`}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {unmount} = render(<MultiLineApp />, {stdout, stdin});\n\tawait delay(50);\n\n\tconst writesBeforeInput = (stdout.write as any).callCount as number;\n\n\temitReadable(stdin, 'x');\n\tawait delay(50);\n\n\t// With isTTY=true, stdout.get() (lastCall) may be a synchronized output\n\t// wrapper rather than the render content, so check writes from the\n\t// second render combined.\n\tconst secondRenderOutput = getWriteCalls(stdout)\n\t\t.slice(writesBeforeInput)\n\t\t.join('');\n\t// When cursor was at y=1 (line 1), next render should first cursorDown to bottom,\n\t// then erase. The write should contain cursorDown to return to bottom.\n\t// It should NOT just erase from cursor position (which would scroll screen up).\n\tt.true(\n\t\tsecondRenderOutput.includes(hideCursorEscape),\n\t\t'should hide cursor before erase',\n\t);\n\t// The write should include the new text\n\tt.true(\n\t\tsecondRenderOutput.includes('x'),\n\t\t'should contain the typed character',\n\t);\n\n\tunmount();\n});\n\nfunction StdoutWriteApp() {\n\tconst {setCursorPosition} = useCursor();\n\tconst {write} = useStdout();\n\n\tsetCursorPosition({x: 2, y: 0});\n\n\tuseEffect(() => {\n\t\twrite('from stdout hook\\n');\n\t}, [write]);\n\n\treturn <Text>Hello</Text>;\n}\n\nfunction StderrWriteApp() {\n\tconst {setCursorPosition} = useCursor();\n\tconst {write} = useStderr();\n\n\tsetCursorPosition({x: 2, y: 0});\n\n\tuseEffect(() => {\n\t\twrite('from stderr hook\\n');\n\t}, [write]);\n\n\treturn <Text>Hello</Text>;\n}\n\ntype HookWriteCase = {\n\treadonly testName: string;\n\treadonly App: () => React.JSX.Element;\n\treadonly includeStderr?: boolean;\n\treadonly assertTargetWrite: (\n\t\tt: ExecutionContext,\n\t\toutput: string,\n\t\tstderr: NodeJS.WriteStream | undefined,\n\t) => void;\n};\n\nconst hookWriteCases: HookWriteCase[] = [\n\t{\n\t\ttestName: 'cursor remains visible after useStdout().write()',\n\t\tApp: StdoutWriteApp,\n\t\tassertTargetWrite(t, output) {\n\t\t\tt.true(output.includes('from stdout hook'));\n\t\t},\n\t},\n\t{\n\t\ttestName: 'cursor remains visible after useStderr().write()',\n\t\tApp: StderrWriteApp,\n\t\tincludeStderr: true,\n\t\tassertTargetWrite(t, _output, stderr) {\n\t\t\tt.true((stderr?.write as any).called);\n\t\t},\n\t},\n];\n\nfor (const testCase of hookWriteCases) {\n\ttest.serial(testCase.testName, async t => {\n\t\tconst stdout = createStdout();\n\t\tconst stdin = createStdin();\n\t\tconst stderr = testCase.includeStderr ? createStdout() : undefined;\n\n\t\tconst {unmount} = render(\n\t\t\t<testCase.App />,\n\t\t\tstderr ? {stdout, stderr, stdin} : {stdout, stdin},\n\t\t);\n\t\tawait delay(50);\n\n\t\tconst output = getWriteCalls(stdout).join('');\n\t\tconst lastShowIndex = output.lastIndexOf(showCursorEscape);\n\t\tconst lastHideIndex = output.lastIndexOf(hideCursorEscape);\n\n\t\ttestCase.assertTargetWrite(t, output, stderr);\n\t\tt.true(\n\t\t\tlastShowIndex > lastHideIndex,\n\t\t\t'last cursor visibility escape should be show after hook write',\n\t\t);\n\n\t\tunmount();\n\t});\n}\n\nfunction DebugStdoutWriteApp() {\n\tconst {write} = useStdout();\n\n\tuseEffect(() => {\n\t\twrite('from stdout hook\\n');\n\t}, [write]);\n\n\treturn <Text>Hello</Text>;\n}\n\nfunction DebugStderrWriteApp() {\n\tconst {write} = useStderr();\n\n\tuseEffect(() => {\n\t\twrite('from stderr hook\\n');\n\t}, [write]);\n\n\treturn <Text>Hello</Text>;\n}\n\ntest.serial('debug mode: useStdout().write() replays latest frame', async t => {\n\tconst stdout = createStdout();\n\tconst {unmount} = render(<DebugStdoutWriteApp />, {stdout, debug: true});\n\tawait waitForCondition(() =>\n\t\tgetWriteCalls(stdout).some(write =>\n\t\t\twrite.includes('from stdout hook\\nHello'),\n\t\t),\n\t);\n\n\tconst writes = getWriteCalls(stdout);\n\tconst hookWrite = writes.find(write =>\n\t\twrite.includes('from stdout hook\\nHello'),\n\t);\n\n\tt.truthy(hookWrite);\n\tt.false(writes.includes(''));\n\n\tunmount();\n});\n\ntest.serial(\n\t'debug mode: useStdout().write() does not leak into stderr',\n\tasync t => {\n\t\tconst stdout = createStdout();\n\t\tconst stderr = createStdout();\n\t\tconst {unmount} = render(<DebugStdoutWriteApp />, {\n\t\t\tstdout,\n\t\t\tstderr,\n\t\t\tdebug: true,\n\t\t});\n\t\tawait waitForCondition(() =>\n\t\t\tgetWriteCalls(stdout).some(write =>\n\t\t\t\twrite.includes('from stdout hook\\nHello'),\n\t\t\t),\n\t\t);\n\n\t\tconst stderrWrites = getWriteCalls(stderr);\n\t\tt.false(stderrWrites.some(write => write.includes('from stdout hook\\n')));\n\t\tt.false(stderrWrites.some(write => write.includes('Hello')));\n\t\tt.false(stderrWrites.includes(''));\n\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'debug mode: useStderr().write() replays latest frame without empty writes',\n\tasync t => {\n\t\tconst stdout = createStdout();\n\t\tconst stderr = createStdout();\n\t\tconst {unmount} = render(<DebugStderrWriteApp />, {\n\t\t\tstdout,\n\t\t\tstderr,\n\t\t\tdebug: true,\n\t\t});\n\t\tawait waitForCondition(() =>\n\t\t\tgetWriteCalls(stderr).some(write => write.includes('from stderr hook\\n')),\n\t\t);\n\t\tawait waitForCondition(() => getWriteCalls(stdout).length > 1);\n\n\t\tconst stdoutWrites = getWriteCalls(stdout);\n\t\tconst stderrWrites = getWriteCalls(stderr);\n\t\tconst stdoutWritesAfterInitialRender = stdoutWrites.slice(1);\n\n\t\tt.true(stderrWrites.some(write => write.includes('from stderr hook\\n')));\n\t\tt.false(stderrWrites.some(write => write.includes('Hello')));\n\t\tt.true(stdoutWritesAfterInitialRender.length > 0);\n\t\tt.true(\n\t\t\tstdoutWritesAfterInitialRender.some(write => write.includes('Hello')),\n\t\t);\n\t\tt.false(\n\t\t\tstdoutWritesAfterInitialRender.some(write =>\n\t\t\t\twrite.includes('from stderr hook\\n'),\n\t\t\t),\n\t\t);\n\t\tt.false(stdoutWrites.includes(''));\n\t\tt.false(stderrWrites.includes(''));\n\n\t\tunmount();\n\t},\n);\n\nfunction DebugStderrWriteAfterRerenderApp() {\n\tconst [text, setText] = useState('Initial');\n\tconst {write} = useStderr();\n\n\tuseEffect(() => {\n\t\tsetText('Updated');\n\t}, []);\n\n\tuseEffect(() => {\n\t\tif (text === 'Updated') {\n\t\t\twrite('from stderr hook\\n');\n\t\t}\n\t}, [text, write]);\n\n\treturn <Text>{text}</Text>;\n}\n\nfunction DebugStdoutWriteAfterRerenderApp() {\n\tconst [text, setText] = useState('Initial');\n\tconst {write} = useStdout();\n\n\tuseEffect(() => {\n\t\tsetText('Updated');\n\t}, []);\n\n\tuseEffect(() => {\n\t\tif (text === 'Updated') {\n\t\t\twrite('from stdout hook\\n');\n\t\t}\n\t}, [text, write]);\n\n\treturn <Text>{text}</Text>;\n}\n\ntest.serial(\n\t'debug mode: useStdout().write() replays rerendered frame',\n\tasync t => {\n\t\tconst stdout = createStdout();\n\t\tconst {unmount} = render(<DebugStdoutWriteAfterRerenderApp />, {\n\t\t\tstdout,\n\t\t\tdebug: true,\n\t\t});\n\t\tawait waitForCondition(() =>\n\t\t\tgetWriteCalls(stdout).some(write =>\n\t\t\t\twrite.includes('from stdout hook\\nUpdated'),\n\t\t\t),\n\t\t);\n\n\t\tconst stdoutWrites = getWriteCalls(stdout);\n\n\t\tt.true(\n\t\t\tstdoutWrites.some(write => write.includes('from stdout hook\\nUpdated')),\n\t\t);\n\t\tt.false(\n\t\t\tstdoutWrites.some(write => write.includes('from stdout hook\\nInitial')),\n\t\t);\n\t\tt.false(stdoutWrites.includes(''));\n\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'debug mode: useStderr().write() replays rerendered frame',\n\tasync t => {\n\t\tconst stdout = createStdout();\n\t\tconst stderr = createStdout();\n\t\tconst {unmount} = render(<DebugStderrWriteAfterRerenderApp />, {\n\t\t\tstdout,\n\t\t\tstderr,\n\t\t\tdebug: true,\n\t\t});\n\t\tawait waitForCondition(() =>\n\t\t\tgetWriteCalls(stderr).some(write => write.includes('from stderr hook\\n')),\n\t\t);\n\t\tawait waitForCondition(() =>\n\t\t\tgetWriteCalls(stdout)\n\t\t\t\t.slice(1)\n\t\t\t\t.some(write => write.includes('Updated')),\n\t\t);\n\n\t\tconst stdoutWrites = getWriteCalls(stdout);\n\t\tconst stderrWrites = getWriteCalls(stderr);\n\t\tconst stdoutWritesAfterInitialRender = stdoutWrites.slice(1);\n\n\t\tt.true(stderrWrites.some(write => write.includes('from stderr hook\\n')));\n\t\tt.false(stderrWrites.some(write => write.includes('Updated')));\n\t\tt.false(stderrWrites.some(write => write.includes('Initial')));\n\t\tt.true(\n\t\t\tstdoutWritesAfterInitialRender.some(write => write.includes('Updated')),\n\t\t);\n\t\tt.false(\n\t\t\tstdoutWritesAfterInitialRender.some(write => write.includes('Initial')),\n\t\t);\n\t\tt.false(\n\t\t\tstdoutWritesAfterInitialRender.some(write =>\n\t\t\t\twrite.includes('from stderr hook\\n'),\n\t\t\t),\n\t\t);\n\t\tt.false(stdoutWrites.includes(''));\n\t\tt.false(stderrWrites.includes(''));\n\n\t\tunmount();\n\t},\n);\n"
  },
  {
    "path": "test/display.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text} from '../src/index.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\n\ntest('display flex', t => {\n\tconst output = renderToString(\n\t\t<Box display=\"flex\">\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\tt.is(output, 'X');\n});\n\ntest('display none', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Box display=\"none\">\n\t\t\t\t<Text>Kitty!</Text>\n\t\t\t</Box>\n\t\t\t<Text>Doggo</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Doggo');\n});\n\n// Concurrent mode tests\ntest('display flex - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box display=\"flex\">\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\tt.is(output, 'X');\n});\n\ntest('display none - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Box display=\"none\">\n\t\t\t\t<Text>Kitty!</Text>\n\t\t\t</Box>\n\t\t\t<Text>Doggo</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Doggo');\n});\n"
  },
  {
    "path": "test/errors.tsx",
    "content": "import process from 'node:process';\nimport React, {useEffect} from 'react';\nimport test from 'ava';\nimport patchConsole from 'patch-console';\nimport stripAnsi from 'strip-ansi';\nimport {render, useStdin, Text} from '../src/index.js';\nimport createStdout from './helpers/create-stdout.js';\n\nlet restore = () => {};\n\ntest.before(() => {\n\trestore = patchConsole(() => {});\n});\n\ntest.after(() => {\n\trestore();\n});\n\ntest('catch and display error', t => {\n\tconst stdout = createStdout();\n\n\tconst Test = () => {\n\t\tthrow new Error('Oh no');\n\t};\n\n\trender(<Test />, {stdout});\n\n\t// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call\n\tconst writes: string[] = (stdout.write as any)\n\t\t.getCalls()\n\t\t.map((c: any) => c.args[0] as string)\n\t\t.filter(\n\t\t\t(w: string) =>\n\t\t\t\t!w.startsWith('\\u001B[?25') && !w.startsWith('\\u001B[?2026'),\n\t\t);\n\tconst lastContentWrite = writes.at(-1)!;\n\n\tt.deepEqual(stripAnsi(lastContentWrite).split('\\n').slice(0, 14), [\n\t\t'',\n\t\t'  ERROR  Oh no',\n\t\t'',\n\t\t' test/errors.tsx:23:9',\n\t\t'',\n\t\t' 20:   const stdout = createStdout();',\n\t\t' 21:',\n\t\t' 22:   const Test = () => {',\n\t\t\" 23:     throw new Error('Oh no');\",\n\t\t' 24:   };',\n\t\t' 25:',\n\t\t' 26:   render(<Test />, {stdout});',\n\t\t'',\n\t\t' - Test (test/errors.tsx:23:9)',\n\t]);\n});\n\ntest.serial(\n\t'does not emit unhandledRejection when render exits with an error and waitUntilExit is unused',\n\tasync t => {\n\t\tconst stdout = createStdout();\n\t\tconst unhandledRejectionReasons: unknown[] = [];\n\t\tconst onUnhandledRejection = (reason: unknown) => {\n\t\t\tunhandledRejectionReasons.push(reason);\n\t\t};\n\n\t\tprocess.on('unhandledRejection', onUnhandledRejection);\n\n\t\ttry {\n\t\t\tconst Test = () => {\n\t\t\t\tthrow new Error('Oh no');\n\t\t\t};\n\n\t\t\trender(<Test />, {stdout});\n\n\t\t\tawait new Promise<void>(resolve => {\n\t\t\t\tsetImmediate(resolve);\n\t\t\t});\n\t\t\tawait new Promise<void>(resolve => {\n\t\t\t\tsetImmediate(resolve);\n\t\t\t});\n\n\t\t\tt.is(unhandledRejectionReasons.length, 0);\n\t\t} finally {\n\t\t\tprocess.off('unhandledRejection', onUnhandledRejection);\n\t\t}\n\t},\n);\n\ntest('ErrorBoundary catches and displays nested component errors', t => {\n\tconst stdout = createStdout();\n\n\tconst NestedComponent = () => {\n\t\tthrow new Error('Nested component error');\n\t};\n\n\tfunction Parent() {\n\t\treturn (\n\t\t\t<Text>\n\t\t\t\tBefore error\n\t\t\t\t<NestedComponent />\n\t\t\t</Text>\n\t\t);\n\t}\n\n\trender(<Parent />, {stdout});\n\n\t// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call\n\tconst writes: string[] = (stdout.write as any)\n\t\t.getCalls()\n\t\t.map((c: any) => c.args[0] as string)\n\t\t.filter(\n\t\t\t(w: string) =>\n\t\t\t\t!w.startsWith('\\u001B[?25') && !w.startsWith('\\u001B[?2026'),\n\t\t);\n\tconst lastContentWrite = writes.at(-1)!;\n\tconst output = stripAnsi(lastContentWrite);\n\tt.true(output.includes('ERROR'), 'Error label should be displayed');\n\tt.true(\n\t\toutput.includes('Nested component error'),\n\t\t'Error message should be shown',\n\t);\n});\n\ntest('clean up raw mode when error is thrown', async t => {\n\tconst stdout = createStdout();\n\n\t// Track setRawMode calls\n\tconst setRawModeCalls: boolean[] = [];\n\tconst originalSetRawMode = process.stdin.setRawMode?.bind(process.stdin);\n\n\t// Only run this test if raw mode is supported\n\tif (!process.stdin.isTTY) {\n\t\tt.pass('Skipping test - stdin is not a TTY');\n\t\treturn;\n\t}\n\n\tprocess.stdin.setRawMode = (mode: boolean) => {\n\t\tsetRawModeCalls.push(mode);\n\n\t\treturn originalSetRawMode?.(mode) ?? process.stdin;\n\t};\n\n\tfunction Test() {\n\t\tconst {setRawMode} = useStdin();\n\n\t\tuseEffect(() => {\n\t\t\tsetRawMode(true);\n\t\t\t// Throw after enabling raw mode\n\t\t\tthrow new Error('Error after raw mode enabled');\n\t\t}, [setRawMode]);\n\n\t\treturn <Text>Test</Text>;\n\t}\n\n\tconst app = render(<Test />, {stdout});\n\n\tawait t.throwsAsync(app.waitUntilExit());\n\n\t// Restore original setRawMode\n\tif (originalSetRawMode) {\n\t\tprocess.stdin.setRawMode = originalSetRawMode;\n\t}\n\n\t// Verify raw mode was enabled then disabled\n\tt.true(setRawModeCalls.includes(true), 'Raw mode should have been enabled');\n\tt.true(\n\t\tsetRawModeCalls.includes(false),\n\t\t'Raw mode should have been disabled on cleanup',\n\t);\n});\n"
  },
  {
    "path": "test/exit.tsx",
    "content": "import process from 'node:process';\nimport * as path from 'node:path';\nimport url from 'node:url';\nimport {createRequire} from 'node:module';\nimport test from 'ava';\nimport stripAnsi from 'strip-ansi';\nimport {run} from './helpers/run.js';\n\nconst require = createRequire(import.meta.url);\n\n// eslint-disable-next-line @typescript-eslint/consistent-type-imports\nconst {spawn} = require('node-pty') as typeof import('node-pty');\n\nconst __dirname = url.fileURLToPath(new URL('.', import.meta.url));\n\ntest.serial('exit normally without unmount() or exit()', async t => {\n\tconst output = await run('exit-normally');\n\tt.true(output.includes('exited'));\n});\n\ntest.serial('exit on unmount()', async t => {\n\tconst output = await run('exit-on-unmount');\n\tt.true(output.includes('exited'));\n});\n\ntest.serial('exit when app finishes execution', async t => {\n\tconst ps = run('exit-on-finish');\n\tawait t.notThrowsAsync(ps);\n});\n\ntest.serial('exit on exit()', async t => {\n\tconst output = await run('exit-on-exit');\n\tt.true(output.includes('exited'));\n});\n\ntest.serial('exit on exit() with error', async t => {\n\tconst output = await run('exit-on-exit-with-error');\n\tt.true(output.includes('errored'));\n});\n\ntest.serial('exit on exit() with error with value property', async t => {\n\tconst output = await run('exit-on-exit-with-error-value-property');\n\tt.true(output.includes('errored'));\n});\n\ntest.serial('exit on exit() with result value', async t => {\n\tconst output = await run('exit-on-exit-with-result');\n\tt.true(output.includes('result:hello from ink'));\n});\n\ntest.serial('exit on exit() with object result', async t => {\n\tconst output = await run('exit-on-exit-with-value-object');\n\tt.true(output.includes('result:hello from ink object'));\n});\n\ntest.serial('exit on exit() with raw mode', async t => {\n\tconst output = await run('exit-raw-on-exit');\n\tt.true(output.includes('exited'));\n});\n\ntest.serial('exit on exit() with raw mode with error', async t => {\n\tconst output = await run('exit-raw-on-exit-with-error');\n\tt.true(output.includes('errored'));\n});\n\ntest.serial('exit on unmount() with raw mode', async t => {\n\tconst output = await run('exit-raw-on-unmount');\n\tt.true(output.includes('exited'));\n});\n\ntest.serial('exit with thrown error', async t => {\n\tconst output = await run('exit-with-thrown-error');\n\tt.true(output.includes('errored'));\n});\n\ntest.serial('don’t exit while raw mode is active', async t => {\n\tawait new Promise<void>((resolve, reject) => {\n\t\tconst env: Record<string, string> = {\n\t\t\t...process.env,\n\t\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\t\tNODE_NO_WARNINGS: '1',\n\t\t};\n\n\t\tconst term = spawn(\n\t\t\t'node',\n\t\t\t[\n\t\t\t\t'--import=tsx',\n\t\t\t\tpath.join(__dirname, './fixtures/exit-double-raw-mode.tsx'),\n\t\t\t],\n\t\t\t{\n\t\t\t\tname: 'xterm-color',\n\t\t\t\tcols: 100,\n\t\t\t\tcwd: __dirname,\n\t\t\t\tenv,\n\t\t\t},\n\t\t);\n\n\t\tlet output = '';\n\n\t\tterm.onData(data => {\n\t\t\tif (data === 's') {\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tt.false(isExited);\n\t\t\t\t\tterm.write('q');\n\t\t\t\t}, 500);\n\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tterm.kill();\n\t\t\t\t\treject(new Error('Test timed out - process did not exit in time'));\n\t\t\t\t}, 2000);\n\t\t\t} else {\n\t\t\t\toutput += data;\n\t\t\t}\n\t\t});\n\n\t\tlet isExited = false;\n\n\t\tterm.onExit(({exitCode}) => {\n\t\t\tisExited = true;\n\n\t\t\tif (exitCode === 0) {\n\t\t\t\tt.true(output.includes('exited'));\n\t\t\t\tt.pass();\n\t\t\t\tresolve();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\treject(new Error(`Process exited with code ${exitCode}`));\n\t\t});\n\t});\n});\n\ntest.serial('exit when DEV is set', async t => {\n\tconst output = await run('exit-normally', {\n\t\tenv: {\n\t\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\t\tDEV: 'true',\n\t\t},\n\t});\n\t// Warning output depends on whether a local React DevTools server is running.\n\tt.true(output.includes('exited'));\n});\n\ntest.serial('exit on exit() with error and static output', async t => {\n\tconst output = await run('exit-with-static');\n\t// Error is propagated, not swallowed\n\tt.true(output.includes('errored'));\n\t// Static items rendered\n\tt.true(output.includes('A'));\n\tt.true(output.includes('B'));\n\tt.true(output.includes('C'));\n\t// Static items NOT duplicated (the bug from #397)\n\tconst cleaned = stripAnsi(output);\n\tt.is(cleaned.split('A').length - 1, 1);\n});\n"
  },
  {
    "path": "test/fixtures/alternate-screen-full-board-win.tsx",
    "content": "import {gameReducer} from '../../examples/alternate-screen/alternate-screen.js';\n\nconst boardWidth = 20;\nconst boardHeight = 15;\nconst initialSnakeLength = 3;\n\nconst snake = [];\n\nfor (let y = 0; y < boardHeight; y++) {\n\tif (y % 2 === 0) {\n\t\tfor (let x = 0; x < boardWidth; x++) {\n\t\t\tif (x === 0 && y === 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tsnake.push({x, y});\n\t\t}\n\t} else {\n\t\tfor (let x = boardWidth - 1; x >= 0; x--) {\n\t\t\tsnake.push({x, y});\n\t\t}\n\t}\n}\n\nconst nextState = gameReducer(\n\t{\n\t\tsnake,\n\t\tfood: {x: 0, y: 0},\n\t\tscore: snake.length - initialSnakeLength,\n\t\tgameOver: false,\n\t\twon: false,\n\t\tframe: 42,\n\t},\n\t{\n\t\ttype: 'tick',\n\t\tdirection: 'left',\n\t},\n);\n\nconsole.log(\n\tJSON.stringify({\n\t\tgameOver: nextState.gameOver,\n\t\twon: nextState.won,\n\t\tscore: nextState.score,\n\t\tsnakeLength: nextState.snake.length,\n\t}),\n);\n"
  },
  {
    "path": "test/fixtures/ci-debug-after-exit.tsx",
    "content": "import process from 'node:process';\nimport React from 'react';\nimport {render, Text} from '../../src/index.js';\n\nconst app = render(<Text>Hello</Text>, {debug: true});\n\napp.unmount();\nawait app.waitUntilExit();\nprocess.stdout.write('DONE');\n"
  },
  {
    "path": "test/fixtures/ci-debug.tsx",
    "content": "import React from 'react';\nimport {render, Text} from '../../src/index.js';\n\nrender(<Text>Hello</Text>, {debug: true});\n"
  },
  {
    "path": "test/fixtures/ci.tsx",
    "content": "import React from 'react';\nimport {render, Static, Text} from '../../src/index.js';\n\ntype TestState = {\n\tcounter: number;\n\titems: string[];\n};\n\nclass Test extends React.Component<Record<string, unknown>, TestState> {\n\ttimer?: NodeJS.Timeout;\n\n\toverride state: TestState = {\n\t\titems: [],\n\t\tcounter: 0,\n\t};\n\n\toverride render() {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<Static items={this.state.items}>\n\t\t\t\t\t{item => <Text key={item}>{item}</Text>}\n\t\t\t\t</Static>\n\n\t\t\t\t<Text>Counter: {this.state.counter}</Text>\n\t\t\t</>\n\t\t);\n\t}\n\n\toverride componentDidMount() {\n\t\tconst onTimeout = () => {\n\t\t\tif (this.state.counter > 4) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.setState(prevState => ({\n\t\t\t\tcounter: prevState.counter + 1,\n\t\t\t\titems: [...prevState.items, `#${prevState.counter + 1}`],\n\t\t\t}));\n\n\t\t\tthis.timer = setTimeout(onTimeout, 20);\n\t\t};\n\n\t\tthis.timer = setTimeout(onTimeout, 20);\n\t}\n\n\toverride componentWillUnmount() {\n\t\tclearTimeout(this.timer);\n\t}\n}\n\nrender(<Test />);\n"
  },
  {
    "path": "test/fixtures/clear.tsx",
    "content": "import React from 'react';\nimport {Box, Text, render} from '../../src/index.js';\n\nfunction Clear() {\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t</Box>\n\t);\n}\n\nconst {clear} = render(<Clear />);\nclear();\n"
  },
  {
    "path": "test/fixtures/console.tsx",
    "content": "import React, {useEffect} from 'react';\nimport {Text, render} from '../../src/index.js';\n\nfunction App() {\n\tuseEffect(() => {\n\t\tconst timer = setTimeout(() => {}, 1000);\n\n\t\treturn () => {\n\t\t\tclearTimeout(timer);\n\t\t};\n\t}, []);\n\n\treturn <Text>Hello World</Text>;\n}\n\nconst {unmount} = render(<App />);\nconsole.log('First log');\nunmount();\nconsole.log('Second log');\n"
  },
  {
    "path": "test/fixtures/erase-with-state-change.tsx",
    "content": "import process from 'node:process';\nimport React, {useEffect, useState} from 'react';\nimport {Box, Text, render} from '../../src/index.js';\n\nfunction Erase() {\n\tconst [show, setShow] = useState(true);\n\n\tuseEffect(() => {\n\t\tconst timer = setTimeout(() => {\n\t\t\tsetShow(false);\n\t\t});\n\n\t\treturn () => {\n\t\t\tclearTimeout(timer);\n\t\t};\n\t}, []);\n\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t{show ? (\n\t\t\t\t<>\n\t\t\t\t\t<Text>A</Text>\n\t\t\t\t\t<Text>B</Text>\n\t\t\t\t\t<Text>C</Text>\n\t\t\t\t</>\n\t\t\t) : null}\n\t\t</Box>\n\t);\n}\n\nprocess.stdout.rows = Number(process.argv[2]);\nrender(<Erase />);\n"
  },
  {
    "path": "test/fixtures/erase-with-static.tsx",
    "content": "import process from 'node:process';\nimport React from 'react';\nimport {Static, Box, Text, render} from '../../src/index.js';\n\nfunction EraseWithStatic() {\n\treturn (\n\t\t<>\n\t\t\t<Static items={['A', 'B', 'C']}>\n\t\t\t\t{item => <Text key={item}>{item}</Text>}\n\t\t\t</Static>\n\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Text>D</Text>\n\t\t\t\t<Text>E</Text>\n\t\t\t\t<Text>F</Text>\n\t\t\t</Box>\n\t\t</>\n\t);\n}\n\nprocess.stdout.rows = Number(process.argv[3]);\nrender(<EraseWithStatic />);\n"
  },
  {
    "path": "test/fixtures/erase.tsx",
    "content": "import process from 'node:process';\nimport React from 'react';\nimport {Box, Text, render} from '../../src/index.js';\n\nfunction Erase() {\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t</Box>\n\t);\n}\n\nprocess.stdout.rows = Number(process.argv[2]);\nrender(<Erase />);\n"
  },
  {
    "path": "test/fixtures/exit-double-raw-mode.tsx",
    "content": "import process from 'node:process';\nimport React from 'react';\nimport {Text, render, useStdin} from '../../src/index.js';\n\nclass ExitDoubleRawMode extends React.Component<{\n\tsetRawMode: (value: boolean) => void;\n}> {\n\toverride render() {\n\t\treturn <Text>Hello World</Text>;\n\t}\n\n\toverride componentDidMount() {\n\t\tconst {setRawMode} = this.props;\n\n\t\tsetRawMode(true);\n\n\t\tsetTimeout(() => {\n\t\t\tsetRawMode(false);\n\t\t\tsetRawMode(true);\n\n\t\t\t// Start the test\n\t\t\tprocess.stdout.write('s');\n\t\t}, 500);\n\t}\n}\n\nfunction Test() {\n\tconst {setRawMode} = useStdin();\n\n\treturn <ExitDoubleRawMode setRawMode={setRawMode} />;\n}\n\nconst {unmount, waitUntilExit} = render(<Test />);\n\nprocess.stdin.on('data', data => {\n\tif (String(data) === 'q') {\n\t\tunmount();\n\t}\n});\n\nawait waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/exit-normally.tsx",
    "content": "import React from 'react';\nimport {Text, render} from '../../src/index.js';\n\nconst {waitUntilExit} = render(<Text>Hello World</Text>);\n\nawait waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/exit-on-exit-with-error-value-property.tsx",
    "content": "import React, {useEffect} from 'react';\nimport {render, Text, useApp} from '../../src/index.js';\n\nfunction Test() {\n\tconst {exit} = useApp();\n\n\tuseEffect(() => {\n\t\tsetTimeout(() => {\n\t\t\tconst error = new Error('errored');\n\t\t\t(error as Error & {value: string}).value = 'hello from error';\n\t\t\texit(error);\n\t\t}, 500);\n\t}, []);\n\n\treturn <Text>Testing</Text>;\n}\n\nconst app = render(<Test />);\n\ntry {\n\tawait app.waitUntilExit();\n} catch (error: unknown) {\n\tconsole.log((error as Error).message);\n}\n"
  },
  {
    "path": "test/fixtures/exit-on-exit-with-error.tsx",
    "content": "import React from 'react';\nimport {render, Text, useApp} from '../../src/index.js';\n\nclass Exit extends React.Component<\n\t{onExit: (error: Error) => void},\n\t{counter: number}\n> {\n\ttimer?: NodeJS.Timeout;\n\n\toverride state = {\n\t\tcounter: 0,\n\t};\n\n\toverride render() {\n\t\treturn <Text>Counter: {this.state.counter}</Text>;\n\t}\n\n\toverride componentDidMount() {\n\t\tsetTimeout(() => {\n\t\t\tthis.props.onExit(new Error('errored'));\n\t\t}, 500);\n\n\t\tthis.timer = setInterval(() => {\n\t\t\tthis.setState(prevState => ({\n\t\t\t\tcounter: prevState.counter + 1,\n\t\t\t}));\n\t\t}, 100);\n\t}\n\n\toverride componentWillUnmount() {\n\t\tclearInterval(this.timer);\n\t}\n}\n\nfunction Test() {\n\tconst {exit} = useApp();\n\treturn <Exit onExit={exit} />;\n}\n\nconst app = render(<Test />);\n\ntry {\n\tawait app.waitUntilExit();\n} catch (error: unknown) {\n\tconsole.log((error as any).message);\n}\n"
  },
  {
    "path": "test/fixtures/exit-on-exit-with-result.tsx",
    "content": "import React, {useEffect} from 'react';\nimport {render, Text, useApp} from '../../src/index.js';\n\nfunction Test() {\n\tconst {exit} = useApp();\n\n\tuseEffect(() => {\n\t\tsetTimeout(() => {\n\t\t\texit('hello from ink');\n\t\t}, 500);\n\t});\n\n\treturn <Text>Testing</Text>;\n}\n\nconst app = render(<Test />);\nconst result = await app.waitUntilExit();\nconsole.log(`result:${String(result)}`);\n"
  },
  {
    "path": "test/fixtures/exit-on-exit-with-value-object.tsx",
    "content": "import React, {useEffect} from 'react';\nimport {render, Text, useApp} from '../../src/index.js';\n\nfunction Test() {\n\tconst {exit} = useApp();\n\n\tuseEffect(() => {\n\t\tsetTimeout(() => {\n\t\t\texit({message: 'hello from ink object'});\n\t\t}, 500);\n\t});\n\n\treturn <Text>Testing</Text>;\n}\n\nconst app = render(<Test />);\nconst result = await app.waitUntilExit();\nconsole.log(`result:${(result as {message: string}).message}`);\n"
  },
  {
    "path": "test/fixtures/exit-on-exit.tsx",
    "content": "import React from 'react';\nimport {render, Text, useApp} from '../../src/index.js';\n\nclass Exit extends React.Component<\n\t{onExit: (error: Error) => void},\n\t{counter: number}\n> {\n\ttimer?: NodeJS.Timeout;\n\n\toverride state = {\n\t\tcounter: 0,\n\t};\n\n\toverride render() {\n\t\treturn <Text>Counter: {this.state.counter}</Text>;\n\t}\n\n\toverride componentDidMount() {\n\t\tsetTimeout(this.props.onExit, 500);\n\n\t\tthis.timer = setInterval(() => {\n\t\t\tthis.setState(prevState => ({\n\t\t\t\tcounter: prevState.counter + 1,\n\t\t\t}));\n\t\t}, 100);\n\t}\n\n\toverride componentWillUnmount() {\n\t\tclearInterval(this.timer);\n\t}\n}\n\nfunction Test() {\n\tconst {exit} = useApp();\n\treturn <Exit onExit={exit} />;\n}\n\nconst app = render(<Test />);\n\nawait app.waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/exit-on-finish.tsx",
    "content": "import React from 'react';\nimport {render, Text} from '../../src/index.js';\n\nclass Test extends React.Component<Record<string, unknown>, {counter: number}> {\n\ttimer?: NodeJS.Timeout;\n\n\toverride state = {\n\t\tcounter: 0,\n\t};\n\n\toverride render() {\n\t\treturn <Text>Counter: {this.state.counter}</Text>;\n\t}\n\n\toverride componentDidMount() {\n\t\tconst onTimeout = () => {\n\t\t\tif (this.state.counter > 4) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.setState(prevState => ({\n\t\t\t\tcounter: prevState.counter + 1,\n\t\t\t}));\n\n\t\t\tthis.timer = setTimeout(onTimeout, 20);\n\t\t};\n\n\t\tthis.timer = setTimeout(onTimeout, 20);\n\t}\n\n\toverride componentWillUnmount() {\n\t\tclearTimeout(this.timer);\n\t}\n}\n\nrender(<Test />);\n"
  },
  {
    "path": "test/fixtures/exit-on-unmount.tsx",
    "content": "import React from 'react';\nimport {render, Text} from '../../src/index.js';\n\nclass Test extends React.Component<Record<string, unknown>, {counter: number}> {\n\ttimer?: NodeJS.Timeout;\n\n\toverride state = {\n\t\tcounter: 0,\n\t};\n\n\toverride render() {\n\t\treturn <Text>Counter: {this.state.counter}</Text>;\n\t}\n\n\toverride componentDidMount() {\n\t\tthis.timer = setInterval(() => {\n\t\t\tthis.setState(prevState => ({\n\t\t\t\tcounter: prevState.counter + 1,\n\t\t\t}));\n\t\t}, 100);\n\t}\n\n\toverride componentWillUnmount() {\n\t\tclearInterval(this.timer);\n\t}\n}\n\nconst app = render(<Test />);\n\nsetTimeout(() => {\n\tapp.unmount();\n}, 500);\n\nawait app.waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/exit-raw-on-exit-with-error.tsx",
    "content": "import React from 'react';\nimport {render, Text, useApp, useStdin} from '../../src/index.js';\n\nclass Exit extends React.Component<{\n\tonSetRawMode: (value: boolean) => void;\n\tonExit: (error: Error) => void;\n}> {\n\toverride render() {\n\t\treturn <Text>Hello World</Text>;\n\t}\n\n\toverride componentDidMount() {\n\t\tthis.props.onSetRawMode(true);\n\n\t\tsetTimeout(() => {\n\t\t\tthis.props.onExit(new Error('errored'));\n\t\t}, 500);\n\t}\n}\n\nfunction Test() {\n\tconst {exit} = useApp();\n\tconst {setRawMode} = useStdin();\n\n\treturn <Exit onExit={exit} onSetRawMode={setRawMode} />;\n}\n\nconst app = render(<Test />);\n\ntry {\n\tawait app.waitUntilExit();\n} catch (error: unknown) {\n\tconsole.log((error as any).message);\n}\n"
  },
  {
    "path": "test/fixtures/exit-raw-on-exit.tsx",
    "content": "import React from 'react';\nimport {render, Text, useApp, useStdin} from '../../src/index.js';\n\nclass Exit extends React.Component<{\n\tonSetRawMode: (value: boolean) => void;\n\tonExit: (error: Error) => void;\n}> {\n\toverride render() {\n\t\treturn <Text>Hello World</Text>;\n\t}\n\n\toverride componentDidMount() {\n\t\tthis.props.onSetRawMode(true);\n\t\tsetTimeout(this.props.onExit, 500);\n\t}\n}\n\nfunction Test() {\n\tconst {exit} = useApp();\n\tconst {setRawMode} = useStdin();\n\n\treturn <Exit onExit={exit} onSetRawMode={setRawMode} />;\n}\n\nconst app = render(<Test />);\n\nawait app.waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/exit-raw-on-unmount.tsx",
    "content": "import React from 'react';\nimport {render, Text, useStdin} from '../../src/index.js';\n\nclass Exit extends React.Component<{\n\tonSetRawMode: (value: boolean) => void;\n}> {\n\toverride render() {\n\t\treturn <Text>Hello World</Text>;\n\t}\n\n\toverride componentDidMount() {\n\t\tthis.props.onSetRawMode(true);\n\t}\n}\n\nfunction Test() {\n\tconst {setRawMode} = useStdin();\n\treturn <Exit onSetRawMode={setRawMode} />;\n}\n\nconst app = render(<Test />);\n\nsetTimeout(() => {\n\tapp.unmount();\n}, 500);\n\nawait app.waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/exit-with-static.tsx",
    "content": "import React, {useEffect} from 'react';\nimport {render, Static, Text, useApp} from '../../src/index.js';\n\nfunction Test() {\n\tconst {exit} = useApp();\n\n\tuseEffect(() => {\n\t\texit(new Error('errored'));\n\t}, []);\n\n\treturn (\n\t\t<>\n\t\t\t<Static items={['A', 'B', 'C']}>\n\t\t\t\t{item => <Text key={item}>{item}</Text>}\n\t\t\t</Static>\n\t\t\t<Text>Dynamic</Text>\n\t\t</>\n\t);\n}\n\nconst app = render(<Test />);\n\ntry {\n\tawait app.waitUntilExit();\n} catch (error: unknown) {\n\tconsole.log((error as Error).message);\n}\n"
  },
  {
    "path": "test/fixtures/exit-with-thrown-error.tsx",
    "content": "import React from 'react';\nimport {render} from '../../src/index.js';\n\nconst Test = () => {\n\tthrow new Error('errored');\n};\n\nconst app = render(<Test />);\n\ntry {\n\tawait app.waitUntilExit();\n} catch (error: unknown) {\n\tconsole.log((error as any).message);\n}\n"
  },
  {
    "path": "test/fixtures/fullscreen-no-extra-newline.tsx",
    "content": "import process from 'node:process';\nimport React, {useEffect} from 'react';\nimport {Box, Text, render, useApp} from '../../src/index.js';\n\nfunction Fullscreen() {\n\tconst {exit} = useApp();\n\n\tuseEffect(() => {\n\t\t// Exit after first render to check the output\n\t\tconst timer = setTimeout(() => {\n\t\t\texit();\n\t\t}, 100);\n\n\t\treturn () => {\n\t\t\tclearTimeout(timer);\n\t\t};\n\t}, [exit]);\n\n\t// Force the root to occupy exactly terminal rows\n\tconst rows = Number(process.argv[2]) || 5;\n\n\treturn (\n\t\t<Box height={rows} flexDirection=\"column\">\n\t\t\t<Box flexGrow={1}>\n\t\t\t\t<Text>Full-screen: top</Text>\n\t\t\t</Box>\n\t\t\t<Text>Bottom line (should be usable)</Text>\n\t\t</Box>\n\t);\n}\n\n// Set terminal size from argument\nprocess.stdout.rows = Number(process.argv[2]) || 5;\n\nrender(<Fullscreen />);\n"
  },
  {
    "path": "test/fixtures/issue-442-full-height.tsx",
    "content": "import process from 'node:process';\nimport React, {useEffect} from 'react';\nimport {Box, Text, render, useApp} from '../../src/index.js';\n\nfunction App() {\n\tconst {exit} = useApp();\n\n\tuseEffect(() => {\n\t\tconst timer = setTimeout(() => {\n\t\t\texit();\n\t\t}, 100);\n\n\t\treturn () => {\n\t\t\tclearTimeout(timer);\n\t\t};\n\t}, [exit]);\n\n\tconst rows = Number(process.argv[2]) || 5;\n\tconst columns = process.stdout.columns || 100;\n\n\treturn (\n\t\t<Box width={columns} height={rows} flexDirection=\"column\">\n\t\t\t<Box flexGrow={1}>\n\t\t\t\t<Text>#442 top</Text>\n\t\t\t</Box>\n\t\t\t<Text>#442 bottom</Text>\n\t\t</Box>\n\t);\n}\n\nprocess.stdout.rows = Number(process.argv[2]) || 5;\n\nrender(<App />);\n"
  },
  {
    "path": "test/fixtures/issue-450-fixture-helpers.tsx",
    "content": "import process from 'node:process';\nimport React, {useEffect, useState} from 'react';\nimport {Box, Static, Text, render, useApp} from '../../src/index.js';\n\ntype RerenderFixtureOptions = {\n\treadonly completionMarker?: string;\n\treadonly frameLimit?: number;\n\treadonly includeStaticLine?: boolean;\n\treadonly rowsFallback?: number;\n\treadonly heightForFrame: (rows: number, frameCount: number) => number;\n};\n\nfunction Issue450RerenderFixtureComponent({\n\tcompletionMarker,\n\tframeLimit,\n\tincludeStaticLine,\n\theightForFrame,\n\trows,\n}: {\n\treadonly completionMarker?: string;\n\treadonly frameLimit: number;\n\treadonly includeStaticLine: boolean;\n\treadonly heightForFrame: (rows: number, frameCount: number) => number;\n\treadonly rows: number;\n}) {\n\tconst {exit} = useApp();\n\tconst [frameCount, setFrameCount] = useState(0);\n\tconst targetHeight = heightForFrame(rows, frameCount);\n\n\tuseEffect(() => {\n\t\tif (frameCount >= frameLimit) {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tif (completionMarker) {\n\t\t\t\t\tprocess.stdout.write(completionMarker);\n\t\t\t\t}\n\n\t\t\t\texit();\n\t\t\t}, 0);\n\n\t\t\treturn () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t};\n\t\t}\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tsetFrameCount(previousFrameCount => previousFrameCount + 1);\n\t\t}, 100);\n\n\t\treturn () => {\n\t\t\tclearTimeout(timer);\n\t\t};\n\t}, [completionMarker, exit, frameCount, frameLimit]);\n\n\treturn (\n\t\t<>\n\t\t\t{includeStaticLine ? (\n\t\t\t\t<Static items={['#450 static line']}>\n\t\t\t\t\t{item => <Text key={item}>{item}</Text>}\n\t\t\t\t</Static>\n\t\t\t) : null}\n\t\t\t<Box height={targetHeight} flexDirection=\"column\">\n\t\t\t\t<Text>#450 top</Text>\n\t\t\t\t<Box flexGrow={1}>\n\t\t\t\t\t<Text>{`frame ${frameCount}`}</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>#450 bottom</Text>\n\t\t\t</Box>\n\t\t</>\n\t);\n}\n\nexport const runIssue450RerenderFixture = ({\n\tcompletionMarker,\n\tframeLimit = 8,\n\tincludeStaticLine = false,\n\trowsFallback = 6,\n\theightForFrame,\n}: RerenderFixtureOptions): void => {\n\tconst rows = Number(process.argv[2]) || rowsFallback;\n\tprocess.stdout.rows = rows;\n\n\trender(\n\t\t<Issue450RerenderFixtureComponent\n\t\t\tcompletionMarker={completionMarker}\n\t\t\tframeLimit={frameLimit}\n\t\t\tincludeStaticLine={includeStaticLine}\n\t\t\theightForFrame={heightForFrame}\n\t\t\trows={rows}\n\t\t/>,\n\t);\n};\n\ntype InitialFixtureOptions = {\n\treadonly rowsFallback?: number;\n\treadonly renderedMarker: string;\n\treadonly lineCount: number;\n\treadonly linePrefix: string;\n};\n\nfunction Issue450InitialFixtureComponent({\n\trenderedMarker,\n\tlineCount,\n\tlinePrefix,\n}: {\n\treadonly renderedMarker: string;\n\treadonly lineCount: number;\n\treadonly linePrefix: string;\n}) {\n\tconst {exit} = useApp();\n\n\tuseEffect(() => {\n\t\tconst timer = setTimeout(() => {\n\t\t\tprocess.stdout.write(renderedMarker);\n\t\t\texit();\n\t\t}, 0);\n\n\t\treturn () => {\n\t\t\tclearTimeout(timer);\n\t\t};\n\t}, [exit, renderedMarker]);\n\n\tconst lines = [];\n\tfor (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {\n\t\tlines.push(\n\t\t\t<Text key={lineNumber}>{`${linePrefix} line ${lineNumber}`}</Text>,\n\t\t);\n\t}\n\n\treturn <Box flexDirection=\"column\">{lines}</Box>;\n}\n\nexport const runIssue450InitialFixture = ({\n\trowsFallback = 3,\n\trenderedMarker,\n\tlineCount,\n\tlinePrefix,\n}: InitialFixtureOptions): void => {\n\tconst rows = Number(process.argv[2]) || rowsFallback;\n\tprocess.stdout.rows = rows;\n\n\trender(\n\t\t<Issue450InitialFixtureComponent\n\t\t\trenderedMarker={renderedMarker}\n\t\t\tlineCount={lineCount}\n\t\t\tlinePrefix={linePrefix}\n\t\t/>,\n\t);\n};\n"
  },
  {
    "path": "test/fixtures/issue-450-full-height-rerender-with-marker.tsx",
    "content": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\tcompletionMarker: '__FULL_HEIGHT_RERENDER_COMPLETED__',\n\theightForFrame: rows => rows,\n});\n"
  },
  {
    "path": "test/fixtures/issue-450-full-height-rerender.tsx",
    "content": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\theightForFrame: rows => rows,\n});\n"
  },
  {
    "path": "test/fixtures/issue-450-full-height-with-static-rerender.tsx",
    "content": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\tincludeStaticLine: true,\n\theightForFrame: rows => rows,\n});\n"
  },
  {
    "path": "test/fixtures/issue-450-grow-to-fullscreen-rerender.tsx",
    "content": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\tcompletionMarker: '__GROW_TO_FULLSCREEN_RERENDER_COMPLETED__',\n\theightForFrame: (rows, frameCount) => (frameCount < 2 ? rows - 1 : rows),\n});\n"
  },
  {
    "path": "test/fixtures/issue-450-grow-to-overflow-rerender.tsx",
    "content": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\tframeLimit: 1,\n\trowsFallback: 3,\n\theightForFrame: (rows, frameCount) =>\n\t\tframeCount === 0 ? rows - 1 : rows + 1,\n});\n"
  },
  {
    "path": "test/fixtures/issue-450-height-minus-one-rerender.tsx",
    "content": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\theightForFrame: rows => rows - 1,\n});\n"
  },
  {
    "path": "test/fixtures/issue-450-initial-fullscreen.tsx",
    "content": "import {runIssue450InitialFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450InitialFixture({\n\trenderedMarker: '__INITIAL_FULLSCREEN_FRAME_RENDERED__',\n\tlineCount: 3,\n\tlinePrefix: '#450 initial fullscreen',\n});\n"
  },
  {
    "path": "test/fixtures/issue-450-initial-overflow.tsx",
    "content": "import {runIssue450InitialFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450InitialFixture({\n\trenderedMarker: '__INITIAL_OVERFLOW_FRAME_RENDERED__',\n\tlineCount: 4,\n\tlinePrefix: '#450 initial overflow',\n});\n"
  },
  {
    "path": "test/fixtures/issue-450-shrink-from-fullscreen-rerender.tsx",
    "content": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\theightForFrame: (rows, frameCount) => (frameCount < 2 ? rows : rows - 1),\n});\n"
  },
  {
    "path": "test/fixtures/issue-450-shrink-from-overflow-rerender.tsx",
    "content": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\theightForFrame: (rows, frameCount) =>\n\t\tframeCount === 0 ? rows + 1 : rows - 1,\n});\n"
  },
  {
    "path": "test/fixtures/issue-450-static-shrink-from-fullscreen-rerender.tsx",
    "content": "import {runIssue450RerenderFixture} from './issue-450-fixture-helpers.js';\n\nrunIssue450RerenderFixture({\n\tincludeStaticLine: true,\n\theightForFrame: (rows, frameCount) => (frameCount < 2 ? rows : rows - 1),\n});\n"
  },
  {
    "path": "test/fixtures/issue-725-child-process.tsx",
    "content": "import React from 'react';\nimport {Text, useStdin, render} from '../../src/index.js';\n\nfunction App() {\n\tconst {isRawModeSupported} = useStdin();\n\n\treturn <Text>{isRawModeSupported ? 'ready' : 'ready-stdin-not-tty'}</Text>;\n}\n\nconst {waitUntilExit} = render(<App />);\n\nawait waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/use-input-ctrl-c.tsx",
    "content": "import process from 'node:process';\nimport React from 'react';\nimport {render, useInput, useApp} from '../../src/index.js';\n\nfunction UserInput() {\n\tconst {exit} = useApp();\n\n\tuseInput((input, key) => {\n\t\tif (input === 'c' && key.ctrl) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tthrow new Error('Crash');\n\t});\n\n\tReact.useEffect(() => {\n\t\tprocess.stdout.write('__READY__');\n\t}, []);\n\n\treturn null;\n}\n\nconst app = render(<UserInput />, {exitOnCtrlC: false});\n\nawait app.waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/use-input-discrete-priority.tsx",
    "content": "import process from 'node:process';\nimport React, {\n\tuseState,\n\tuseTransition,\n\tuseMemo,\n\tuseEffect,\n\tuseRef,\n} from 'react';\nimport {render, Box, Text, useInput, useApp} from '../../src/index.js';\n\nfunction App() {\n\tconst {exit} = useApp();\n\tconst [query, setQuery] = useState('abcde');\n\tconst [, startTransition] = useTransition();\n\tconst [deferredQuery, setDeferredQuery] = useState('abcde');\n\tconst done = useRef(false);\n\n\tuseInput((input, key) => {\n\t\tif (key.return) {\n\t\t\tif (done.current) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tdone.current = true;\n\t\t\tprocess.stdout.write(\n\t\t\t\t`\\nFINAL query:${JSON.stringify(query)} deferred:${JSON.stringify(deferredQuery)}\\n`,\n\t\t\t);\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (key.backspace || key.delete) {\n\t\t\tsetQuery(previousQuery => previousQuery.slice(0, -1));\n\t\t\tstartTransition(() => {\n\t\t\t\tsetDeferredQuery(previousQuery => previousQuery.slice(0, -1));\n\t\t\t});\n\t\t}\n\t});\n\n\tconst filteredResult = useMemo(() => {\n\t\tif (!deferredQuery) {\n\t\t\treturn '';\n\t\t}\n\n\t\t// Simulate expensive computation that blocks the fiber\n\t\tconst start = Date.now();\n\t\twhile (Date.now() - start < 30) {\n\t\t\t// Artificial delay\n\t\t}\n\n\t\treturn deferredQuery;\n\t}, [deferredQuery]);\n\n\tuseEffect(() => {\n\t\tprocess.stdout.write('__READY__');\n\t}, []);\n\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>query:{query}</Text>\n\t\t\t<Text>deferred:{deferredQuery}</Text>\n\t\t\t<Text>filtered:{filteredResult}</Text>\n\t\t</Box>\n\t);\n}\n\nrender(<App />, {concurrent: true});\n"
  },
  {
    "path": "test/fixtures/use-input-kitty.tsx",
    "content": "import process from 'node:process';\nimport React from 'react';\nimport {render, useInput, useApp} from '../../src/index.js';\n\nfunction UserInput({test}: {readonly test: string | undefined}) {\n\tconst {exit} = useApp();\n\n\tuseInput((input, key) => {\n\t\t// Test super modifier (Cmd on Mac, Win on Windows)\n\t\tif (test === 'super' && key.super && input === 's') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Test hyper modifier\n\t\tif (test === 'hyper' && key.hyper && input === 'h') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Test capsLock\n\t\tif (test === 'capsLock' && key.capsLock) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Test numLock\n\t\tif (test === 'numLock' && key.numLock) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Test super+ctrl combination\n\t\tif (test === 'superCtrl' && key.super && key.ctrl && input === 's') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Test repeat event type\n\t\tif (test === 'repeat' && key.eventType === 'repeat') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Test release event type\n\t\tif (test === 'release' && key.eventType === 'release') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Test press event type (default)\n\t\tif (test === 'press' && key.eventType === 'press' && input === 'a') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Test escape with kitty protocol\n\t\tif (test === 'escapeKitty' && key.escape) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Test non-printable keys produce empty input\n\t\tif (test === 'nonPrintable' && input === '') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Test ctrl+letter via codepoint 1-26 form still provides input\n\t\tif (test === 'ctrlLetter' && input === 'a' && key.ctrl) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Test space produces space character as input\n\t\tif (test === 'space' && input === ' ') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Test return produces carriage return as input\n\t\tif (test === 'returnKey' && input === '\\r') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tthrow new Error(`Unexpected input: ${JSON.stringify({input, key})}`);\n\t});\n\n\tReact.useEffect(() => {\n\t\tprocess.stdout.write('__READY__');\n\t}, []);\n\n\treturn null;\n}\n\nconst app = render(<UserInput test={process.argv[2]} />, {\n\tkittyKeyboard: {mode: 'disabled'}, // Disable auto-detection for tests\n});\n\nawait app.waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/use-input-many.tsx",
    "content": "import process from 'node:process';\nimport React, {useEffect} from 'react';\nimport {render, useInput, useApp, Text} from '../../src/index.js';\n\n// Detect MaxListenersExceededWarning\nprocess.on('warning', warning => {\n\tif (warning.name === 'MaxListenersExceededWarning') {\n\t\tconsole.log('MaxListenersExceededWarning');\n\t}\n});\n\nfunction InputHandler() {\n\tuseInput(() => {});\n\treturn null;\n}\n\nfunction App() {\n\tconst {exit} = useApp();\n\n\tuseEffect(() => {\n\t\tsetTimeout(exit, 100);\n\t}, []);\n\n\treturn (\n\t\t<>\n\t\t\t<InputHandler />\n\t\t\t<InputHandler />\n\t\t\t<InputHandler />\n\t\t\t<InputHandler />\n\t\t\t<InputHandler />\n\t\t\t<InputHandler />\n\t\t\t<InputHandler />\n\t\t\t<InputHandler />\n\t\t\t<InputHandler />\n\t\t\t<InputHandler />\n\t\t\t<InputHandler />\n\t\t\t<Text>ready</Text>\n\t\t</>\n\t);\n}\n\nconst app = render(<App />);\nawait app.waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/use-input-multiple.tsx",
    "content": "import process from 'node:process';\nimport React, {useState, useCallback, useEffect} from 'react';\nimport {render, useInput, useApp, Text} from '../../src/index.js';\n\nfunction App() {\n\tconst {exit} = useApp();\n\tconst [input, setInput] = useState('');\n\n\tconst handleInput = useCallback((input: string) => {\n\t\tsetInput((previousInput: string) => previousInput + input);\n\t}, []);\n\n\tuseInput(handleInput);\n\tuseInput(handleInput, {isActive: false});\n\n\tuseEffect(() => {\n\t\tprocess.stdout.write('__READY__');\n\t}, []);\n\n\tuseEffect(() => {\n\t\tsetTimeout(exit, 100);\n\t}, []);\n\n\treturn <Text>{input}</Text>;\n}\n\nconst app = render(<App />);\n\nawait app.waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/use-input.tsx",
    "content": "import process from 'node:process';\nimport React from 'react';\nimport {render, useInput, useApp} from '../../src/index.js';\n\nfunction UserInput({test}: {readonly test: string | undefined}) {\n\tconst {exit} = useApp();\n\tconst rapidDownArrowCountRef = React.useRef(0);\n\n\tReact.useEffect(() => {\n\t\tif (test !== 'rapidArrowsEnter') {\n\t\t\treturn;\n\t\t}\n\n\t\tconst timeout = setTimeout(() => {\n\t\t\tthrow new Error(\n\t\t\t\t`Expected 3 down arrows and enter, received ${rapidDownArrowCountRef.current} down arrow events`,\n\t\t\t);\n\t\t}, 6000);\n\n\t\treturn () => {\n\t\t\tclearTimeout(timeout);\n\t\t};\n\t}, [test]);\n\n\tuseInput((input, key) => {\n\t\tif (test === 'rapidArrowsEnter') {\n\t\t\tif (key.downArrow) {\n\t\t\t\trapidDownArrowCountRef.current++;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (key.return) {\n\t\t\t\tif (rapidDownArrowCountRef.current === 3) {\n\t\t\t\t\texit();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Expected enter after 3 down arrows, received ${rapidDownArrowCountRef.current}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tthrow new Error('Expected only down arrows and enter');\n\t\t}\n\n\t\tif (test === 'lowercase' && input === 'q') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'uppercase' && input === 'Q' && key.shift) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'uppercase' && input === '\\r' && !key.shift) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'pastedCarriageReturn' && input === '\\rtest') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'pastedTab' && input === '\\ttest') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'bracketedPaste' && input === 'hello') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'escape' && key.escape) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'ctrl' && input === 'f' && key.ctrl) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'meta' && input === 'm' && key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'escapeBracketPrefix' && input === '[' && !key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'metaUpperO' && input === 'O' && key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'upArrow' && key.upArrow && !key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'downArrow' && key.downArrow && !key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'leftArrow' && key.leftArrow && !key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'rightArrow' && key.rightArrow && !key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'upArrowMeta' && key.upArrow && key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'downArrowMeta' && key.downArrow && key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'leftArrowMeta' && key.leftArrow && key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'rightArrowMeta' && key.rightArrow && key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'upArrowCtrl' && key.upArrow && key.ctrl) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'downArrowCtrl' && key.downArrow && key.ctrl) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'leftArrowCtrl' && key.leftArrow && key.ctrl) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'rightArrowCtrl' && key.rightArrow && key.ctrl) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'pageDown' && key.pageDown && !key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'pageUp' && key.pageUp && !key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'home' && key.home && !key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'end' && key.end && !key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'tab' && input === '' && key.tab && !key.ctrl) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'shiftTab' && input === '' && key.tab && key.shift) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'backspace' && input === '' && key.backspace) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'delete' && input === '' && key.delete) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'remove' && input === '' && key.delete) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'returnMeta' && key.return && key.meta) {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tthrow new Error('Crash');\n\t});\n\n\tReact.useEffect(() => {\n\t\tprocess.stdout.write('__READY__');\n\t}, []);\n\n\treturn null;\n}\n\nconst app = render(<UserInput test={process.argv[2]} />);\n\nawait app.waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/use-paste.tsx",
    "content": "import process from 'node:process';\nimport React from 'react';\nimport {render, useApp, useInput, usePaste} from '../../src/index.js';\n\nfunction PasteDemo({test}: {readonly test: string | undefined}) {\n\tconst {exit} = useApp();\n\n\tusePaste(text => {\n\t\tif (test === 'basic' && text === 'hello world') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'escapeSequences' && text === 'hello\\u001B[Aworld') {\n\t\t\texit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (test === 'noUseInput' && text === 'hello') {\n\t\t\texit();\n\t\t}\n\t});\n\n\tuseInput(\n\t\tinput => {\n\t\t\tthrow new Error(\n\t\t\t\t`useInput received input during paste: ${JSON.stringify(input)}`,\n\t\t\t);\n\t\t},\n\t\t{isActive: test === 'noUseInput'},\n\t);\n\n\tReact.useEffect(() => {\n\t\tprocess.stdout.write('__READY__');\n\t}, []);\n\n\treturn null;\n}\n\nfunction MultipleHooksDemo() {\n\tconst {exit} = useApp();\n\tconst receivedCount = React.useRef(0);\n\n\tconst onPaste = React.useCallback(\n\t\t(text: string) => {\n\t\t\tif (text === 'hello') {\n\t\t\t\treceivedCount.current++;\n\t\t\t\tif (receivedCount.current >= 2) {\n\t\t\t\t\texit();\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t[exit],\n\t);\n\n\tusePaste(onPaste);\n\tusePaste(onPaste);\n\n\tReact.useEffect(() => {\n\t\tprocess.stdout.write('__READY__');\n\t}, []);\n\n\treturn null;\n}\n\nconst test = process.argv[2];\nconst app = render(\n\ttest === 'multipleHooks' ? <MultipleHooksDemo /> : <PasteDemo test={test} />,\n);\n\nawait app.waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/fixtures/use-stdout.tsx",
    "content": "import React, {useEffect} from 'react';\nimport {render, useStdout, Text} from '../../src/index.js';\n\nfunction WriteToStdout() {\n\tconst {write} = useStdout();\n\n\tuseEffect(() => {\n\t\twrite('Hello from Ink to stdout\\n');\n\t}, []);\n\n\treturn <Text>Hello World</Text>;\n}\n\nconst app = render(<WriteToStdout />);\n\nawait app.waitUntilExit();\nconsole.log('exited');\n"
  },
  {
    "path": "test/flex-align-content.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text, render} from '../src/index.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\nimport createStdout from './helpers/create-stdout.js';\n\nconst renderWithAlignContent = (\n\talignContent: NonNullable<React.ComponentProps<typeof Box>['alignContent']>,\n): string =>\n\trenderToString(\n\t\t<Box width={2} height={6} flexWrap=\"wrap\" alignContent={alignContent}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t\t<Text>D</Text>\n\t\t</Box>,\n\t);\n\nfor (const [alignContent, expectedOutput] of [\n\t['flex-start', 'AB\\nCD\\n\\n\\n\\n'],\n\t['center', '\\n\\nAB\\nCD\\n\\n'],\n\t['flex-end', '\\n\\n\\n\\nAB\\nCD'],\n\t['space-between', 'AB\\n\\n\\n\\n\\nCD'],\n\t['space-around', '\\nAB\\n\\n\\nCD\\n'],\n\t['space-evenly', '\\nAB\\n\\nCD\\n\\n'],\n\t['stretch', 'AB\\n\\n\\nCD\\n\\n'],\n] as const) {\n\ttest(`align content ${alignContent}`, t => {\n\t\tconst output = renderWithAlignContent(alignContent);\n\t\tt.is(output, expectedOutput);\n\t});\n}\n\ntest('align content defaults to flex-start', t => {\n\tconst output = renderToString(\n\t\t<Box width={2} height={6} flexWrap=\"wrap\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t\t<Text>D</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'AB\\nCD\\n\\n\\n\\n');\n});\n\ntest('align content does not add extra spacing when there is no free cross-axis space', t => {\n\tconst output = renderToString(\n\t\t<Box width={2} height={2} flexWrap=\"wrap\" alignContent=\"center\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t\t<Text>D</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'AB\\nCD');\n});\n\ntest('clears alignContent on rerender to default flex-start', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({\n\t\talignContent,\n\t}: {\n\t\treadonly alignContent?: React.ComponentProps<typeof Box>['alignContent'];\n\t}) {\n\t\treturn (\n\t\t\t<Box width={2} height={6} flexWrap=\"wrap\" alignContent={alignContent}>\n\t\t\t\t<Text>A</Text>\n\t\t\t\t<Text>B</Text>\n\t\t\t\t<Text>C</Text>\n\t\t\t\t<Text>D</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test alignContent=\"center\" />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is(stdout.write.lastCall.args[0], '\\n\\nAB\\nCD\\n\\n');\n\n\trerender(<Test alignContent={undefined} />);\n\tt.is(stdout.write.lastCall.args[0], 'AB\\nCD\\n\\n\\n\\n');\n});\n\ntest('clears alignContent from stretch on rerender to default flex-start', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({\n\t\talignContent,\n\t}: {\n\t\treadonly alignContent?: React.ComponentProps<typeof Box>['alignContent'];\n\t}) {\n\t\treturn (\n\t\t\t<Box width={2} height={6} flexWrap=\"wrap\" alignContent={alignContent}>\n\t\t\t\t<Text>A</Text>\n\t\t\t\t<Text>B</Text>\n\t\t\t\t<Text>C</Text>\n\t\t\t\t<Text>D</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test alignContent=\"stretch\" />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is(stdout.write.lastCall.args[0], 'AB\\n\\n\\nCD\\n\\n');\n\n\trerender(<Test alignContent={undefined} />);\n\tt.is(stdout.write.lastCall.args[0], 'AB\\nCD\\n\\n\\n\\n');\n});\n\ntest('clears alignContent when prop is omitted on rerender', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({showAlignContent}: {readonly showAlignContent: boolean}) {\n\t\treturn (\n\t\t\t<Box\n\t\t\t\twidth={2}\n\t\t\t\theight={6}\n\t\t\t\tflexWrap=\"wrap\"\n\t\t\t\t{...(showAlignContent ? {alignContent: 'center' as const} : {})}\n\t\t\t>\n\t\t\t\t<Text>A</Text>\n\t\t\t\t<Text>B</Text>\n\t\t\t\t<Text>C</Text>\n\t\t\t\t<Text>D</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test showAlignContent />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is(stdout.write.lastCall.args[0], '\\n\\nAB\\nCD\\n\\n');\n\n\trerender(<Test showAlignContent={false} />);\n\tt.is(stdout.write.lastCall.args[0], 'AB\\nCD\\n\\n\\n\\n');\n});\n\ntest('align content center - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box width={2} height={6} flexWrap=\"wrap\" alignContent=\"center\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t\t<Text>D</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\nAB\\nCD\\n\\n');\n});\n"
  },
  {
    "path": "test/flex-align-items.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text, Newline} from '../src/index.js';\nimport {renderToString} from './helpers/render-to-string.js';\n\ntest('row - align text to center', t => {\n\tconst output = renderToString(\n\t\t<Box alignItems=\"center\" height={3}>\n\t\t\t<Text>Test</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\nTest\\n');\n});\n\ntest('row - align multiple text nodes to center', t => {\n\tconst output = renderToString(\n\t\t<Box alignItems=\"center\" height={3}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\nAB\\n');\n});\n\ntest('row - align text to bottom', t => {\n\tconst output = renderToString(\n\t\t<Box alignItems=\"flex-end\" height={3}>\n\t\t\t<Text>Test</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\nTest');\n});\n\ntest('row - align multiple text nodes to bottom', t => {\n\tconst output = renderToString(\n\t\t<Box alignItems=\"flex-end\" height={3}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\nAB');\n});\n\ntest('column - align text to center', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"center\" width={10}>\n\t\t\t<Text>Test</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '   Test');\n});\n\ntest('column - align text to right', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" alignItems=\"flex-end\" width={10}>\n\t\t\t<Text>Test</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '      Test');\n});\n\ntest('row - align items stretch', t => {\n\tconst output = renderToString(\n\t\t<Box alignItems=\"stretch\" height={5}>\n\t\t\t<Box borderStyle=\"single\">\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '┌─┐\\n│X│\\n│ │\\n│ │\\n└─┘');\n});\n\ntest('row - default align items stretches children', t => {\n\tconst output = renderToString(\n\t\t<Box height={5}>\n\t\t\t<Box borderStyle=\"single\">\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '┌─┐\\n│X│\\n│ │\\n│ │\\n└─┘');\n});\n\ntest('row - align text to baseline', t => {\n\tconst output = renderToString(\n\t\t<Box alignItems=\"baseline\" height={3}>\n\t\t\t<Text>\n\t\t\t\tA\n\t\t\t\t<Newline />B\n\t\t\t</Text>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\nBX\\n');\n});\n"
  },
  {
    "path": "test/flex-align-self.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text, Newline} from '../src/index.js';\nimport {renderToString} from './helpers/render-to-string.js';\n\ntest('row - align text to center', t => {\n\tconst output = renderToString(\n\t\t<Box height={3}>\n\t\t\t<Box alignSelf=\"center\">\n\t\t\t\t<Text>Test</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\nTest\\n');\n});\n\ntest('row - align multiple text nodes to center', t => {\n\tconst output = renderToString(\n\t\t<Box height={3}>\n\t\t\t<Box alignSelf=\"center\">\n\t\t\t\t<Text>A</Text>\n\t\t\t\t<Text>B</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\nAB\\n');\n});\n\ntest('row - align text to bottom', t => {\n\tconst output = renderToString(\n\t\t<Box height={3}>\n\t\t\t<Box alignSelf=\"flex-end\">\n\t\t\t\t<Text>Test</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\nTest');\n});\n\ntest('row - align multiple text nodes to bottom', t => {\n\tconst output = renderToString(\n\t\t<Box height={3}>\n\t\t\t<Box alignSelf=\"flex-end\">\n\t\t\t\t<Text>A</Text>\n\t\t\t\t<Text>B</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\nAB');\n});\n\ntest('column - align text to center', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" width={10}>\n\t\t\t<Box alignSelf=\"center\">\n\t\t\t\t<Text>Test</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '   Test');\n});\n\ntest('column - align text to right', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" width={10}>\n\t\t\t<Box alignSelf=\"flex-end\">\n\t\t\t\t<Text>Test</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '      Test');\n});\n\ntest('column - align self stretch', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" width={7}>\n\t\t\t<Box alignSelf=\"stretch\" borderStyle=\"single\">\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '┌─────┐\\n│X    │\\n└─────┘');\n});\n\ntest('row - align self stretch', t => {\n\tconst output = renderToString(\n\t\t<Box height={5}>\n\t\t\t<Box alignSelf=\"stretch\" borderStyle=\"single\">\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '┌─┐\\n│X│\\n│ │\\n│ │\\n└─┘');\n});\n\ntest('row - align self baseline', t => {\n\tconst output = renderToString(\n\t\t<Box alignItems=\"flex-end\" height={3}>\n\t\t\t<Text>\n\t\t\t\tA\n\t\t\t\t<Newline />B\n\t\t\t</Text>\n\t\t\t<Box alignSelf=\"baseline\">\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'AX\\nB\\n');\n});\n"
  },
  {
    "path": "test/flex-direction.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text} from '../src/index.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\n\ntest('direction row', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"row\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'AB');\n});\n\ntest('direction row reverse', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"row-reverse\" width={4}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '  BA');\n});\n\ntest('direction column', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\nB');\n});\n\ntest('direction column reverse', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column-reverse\" height={4}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\nB\\nA');\n});\n\ntest('don’t squash text nodes when column direction is applied', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\nB');\n});\n\n// Concurrent mode tests\ntest('direction row - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box flexDirection=\"row\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'AB');\n});\n\ntest('direction column - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\nB');\n});\n"
  },
  {
    "path": "test/flex-justify-content.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport chalk from 'chalk';\nimport {Box, Text} from '../src/index.js';\nimport {renderToString} from './helpers/render-to-string.js';\n\ntest('row - align text to center', t => {\n\tconst output = renderToString(\n\t\t<Box justifyContent=\"center\" width={10}>\n\t\t\t<Text>Test</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '   Test');\n});\n\ntest('row - align multiple text nodes to center', t => {\n\tconst output = renderToString(\n\t\t<Box justifyContent=\"center\" width={10}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '    AB');\n});\n\ntest('row - align text to right', t => {\n\tconst output = renderToString(\n\t\t<Box justifyContent=\"flex-end\" width={10}>\n\t\t\t<Text>Test</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '      Test');\n});\n\ntest('row - align multiple text nodes to right', t => {\n\tconst output = renderToString(\n\t\t<Box justifyContent=\"flex-end\" width={10}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '        AB');\n});\n\ntest('row - align two text nodes on the edges', t => {\n\tconst output = renderToString(\n\t\t<Box justifyContent=\"space-between\" width={4}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A  B');\n});\n\ntest('row - space evenly two text nodes', t => {\n\tconst output = renderToString(\n\t\t<Box justifyContent=\"space-evenly\" width={10}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '  A   B');\n});\n\n// Yoga has a bug, where first child in a container with space-around doesn't have\n// the correct X coordinate and measure function is used on that child node\ntest.failing('row - align two text nodes with equal space around them', t => {\n\tconst output = renderToString(\n\t\t<Box justifyContent=\"space-around\" width={5}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, ' A B');\n});\n\ntest('row - align colored text node when text is squashed', t => {\n\tconst output = renderToString(\n\t\t<Box justifyContent=\"flex-end\" width={5}>\n\t\t\t<Text color=\"green\">X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, `    ${chalk.green('X')}`);\n});\n\ntest('column - align text to center', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" justifyContent=\"center\" height={3}>\n\t\t\t<Text>Test</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\nTest\\n');\n});\n\ntest('column - align text to bottom', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" justifyContent=\"flex-end\" height={3}>\n\t\t\t<Text>Test</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\nTest');\n});\n\ntest('column - align two text nodes on the edges', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" justifyContent=\"space-between\" height={4}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\n\\n\\nB');\n});\n\n// Yoga has a bug, where first child in a container with space-around doesn't have\n// the correct X coordinate and measure function is used on that child node\ntest.failing(\n\t'column - align two text nodes with equal space around them',\n\tt => {\n\t\tconst output = renderToString(\n\t\t\t<Box flexDirection=\"column\" justifyContent=\"space-around\" height={5}>\n\t\t\t\t<Text>A</Text>\n\t\t\t\t<Text>B</Text>\n\t\t\t</Box>,\n\t\t);\n\n\t\tt.is(output, '\\nA\\n\\nB\\n');\n\t},\n);\n"
  },
  {
    "path": "test/flex-wrap.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text} from '../src/index.js';\nimport {renderToString} from './helpers/render-to-string.js';\n\ntest('row - no wrap', t => {\n\tconst output = renderToString(\n\t\t<Box width={2}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>BC</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'BC\\n');\n});\n\ntest('column - no wrap', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" height={2}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'B\\nC');\n});\n\ntest('row - wrap content', t => {\n\tconst output = renderToString(\n\t\t<Box width={2} flexWrap=\"wrap\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>BC</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\nBC');\n});\n\ntest('column - wrap content', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" height={2} flexWrap=\"wrap\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'AC\\nB');\n});\n\ntest('column - wrap content reverse', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" height={2} width={3} flexWrap=\"wrap-reverse\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, ' CA\\n  B');\n});\n\ntest('row - wrap content reverse', t => {\n\tconst output = renderToString(\n\t\t<Box height={3} width={2} flexWrap=\"wrap-reverse\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\nC\\nAB');\n});\n"
  },
  {
    "path": "test/flex.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text} from '../src/index.js';\nimport {renderToString} from './helpers/render-to-string.js';\n\ntest('grow equally', t => {\n\tconst output = renderToString(\n\t\t<Box width={6}>\n\t\t\t<Box flexGrow={1}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Box flexGrow={1}>\n\t\t\t\t<Text>B</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A  B');\n});\n\ntest('grow one element', t => {\n\tconst output = renderToString(\n\t\t<Box width={6}>\n\t\t\t<Box flexGrow={1}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A    B');\n});\n\ntest('do not shrink', t => {\n\tconst output = renderToString(\n\t\t<Box width={16}>\n\t\t\t<Box flexShrink={0} width={6}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Box flexShrink={0} width={6}>\n\t\t\t\t<Text>B</Text>\n\t\t\t</Box>\n\t\t\t<Box width={6}>\n\t\t\t\t<Text>C</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A     B     C');\n});\n\ntest('shrink equally', t => {\n\tconst output = renderToString(\n\t\t<Box width={10}>\n\t\t\t<Box flexShrink={1} width={6}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Box flexShrink={1} width={6}>\n\t\t\t\t<Text>B</Text>\n\t\t\t</Box>\n\t\t\t<Text>C</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A    B   C');\n});\n\ntest('set flex basis with flexDirection=\"row\" container', t => {\n\tconst output = renderToString(\n\t\t<Box width={6}>\n\t\t\t<Box flexBasis={3}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A  B');\n});\n\ntest('set flex basis in percent with flexDirection=\"row\" container', t => {\n\tconst output = renderToString(\n\t\t<Box width={6}>\n\t\t\t<Box flexBasis=\"50%\">\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A  B');\n});\n\ntest('set flex basis with flexDirection=\"column\" container', t => {\n\tconst output = renderToString(\n\t\t<Box height={6} flexDirection=\"column\">\n\t\t\t<Box flexBasis={3}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\n\\n\\nB\\n\\n');\n});\n\ntest('set flex basis in percent with flexDirection=\"column\" container', t => {\n\tconst output = renderToString(\n\t\t<Box height={6} flexDirection=\"column\">\n\t\t\t<Box flexBasis=\"50%\">\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\n\\n\\nB\\n\\n');\n});\n"
  },
  {
    "path": "test/focus.tsx",
    "content": "import EventEmitter from 'node:events';\nimport React, {useEffect} from 'react';\nimport delay from 'delay';\nimport test from 'ava';\nimport {spy, stub} from 'sinon';\nimport {render, Box, Text, useFocus, useFocusManager} from '../src/index.js';\nimport createStdout from './helpers/create-stdout.js';\n\nconst createStdin = () => {\n\tconst stdin = new EventEmitter() as unknown as NodeJS.WriteStream;\n\tstdin.isTTY = true;\n\tstdin.setRawMode = spy();\n\tstdin.setEncoding = () => {};\n\tstdin.read = stub();\n\tstdin.unref = () => {};\n\tstdin.ref = () => {};\n\n\treturn stdin;\n};\n\nconst emitReadable = (stdin: NodeJS.WriteStream, chunk: string) => {\n\t/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment */\n\tconst read = stdin.read as ReturnType<typeof stub>;\n\tread.onCall(0).returns(chunk);\n\tread.onCall(1).returns(null);\n\tstdin.emit('readable');\n\tread.reset();\n\t/* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment */\n};\n\ntype TestProps = {\n\treadonly showFirst?: boolean;\n\treadonly disableFirst?: boolean;\n\treadonly disableSecond?: boolean;\n\treadonly disableThird?: boolean;\n\treadonly autoFocus?: boolean;\n\treadonly disabled?: boolean;\n\treadonly focusNext?: boolean;\n\treadonly focusPrevious?: boolean;\n\treadonly unmountChildren?: boolean;\n};\n\nfunction Test({\n\tshowFirst = true,\n\tdisableFirst = false,\n\tdisableSecond = false,\n\tdisableThird = false,\n\tautoFocus = false,\n\tdisabled = false,\n\tfocusNext = false,\n\tfocusPrevious = false,\n\tunmountChildren = false,\n}: TestProps) {\n\tconst focusManager = useFocusManager();\n\n\tuseEffect(() => {\n\t\tif (disabled) {\n\t\t\tfocusManager.disableFocus();\n\t\t} else {\n\t\t\tfocusManager.enableFocus();\n\t\t}\n\t}, [disabled]);\n\n\tuseEffect(() => {\n\t\tif (focusNext) {\n\t\t\tfocusManager.focusNext();\n\t\t}\n\t}, [focusNext]);\n\n\tuseEffect(() => {\n\t\tif (focusPrevious) {\n\t\t\tfocusManager.focusPrevious();\n\t\t}\n\t}, [focusPrevious]);\n\n\tif (unmountChildren) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t<Box flexDirection=\"column\">\n\t\t\t{showFirst ? (\n\t\t\t\t<Item label=\"First\" autoFocus={autoFocus} disabled={disableFirst} />\n\t\t\t) : null}\n\t\t\t<Item label=\"Second\" autoFocus={autoFocus} disabled={disableSecond} />\n\t\t\t<Item label=\"Third\" autoFocus={autoFocus} disabled={disableThird} />\n\t\t</Box>\n\t);\n}\n\ntype ItemProps = {\n\treadonly label: string;\n\treadonly autoFocus: boolean;\n\treadonly disabled?: boolean;\n};\n\nfunction Item({label, autoFocus, disabled = false}: ItemProps) {\n\tconst {isFocused} = useFocus({\n\t\tautoFocus,\n\t\tisActive: !disabled,\n\t});\n\n\treturn (\n\t\t<Text>\n\t\t\t{label} {isFocused ? '✔' : null}\n\t\t</Text>\n\t);\n}\n\ntest('do not focus on register when auto focus is off', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second', 'Third'].join('\\n'),\n\t);\n});\n\ntest('focus the first component to register', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test autoFocus />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First ✔', 'Second', 'Third'].join('\\n'),\n\t);\n});\n\ntest('unfocus active component on Esc', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\temitReadable(stdin, '\\u001B');\n\tawait delay(50);\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second', 'Third'].join('\\n'),\n\t);\n});\n\ntest('switch focus to first component on Tab', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First ✔', 'Second', 'Third'].join('\\n'),\n\t);\n});\n\ntest('switch focus to the next component on Tab', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\temitReadable(stdin, '\\t');\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second ✔', 'Third'].join('\\n'),\n\t);\n});\n\ntest('switch focus to the first component if currently focused component is the last one on Tab', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test autoFocus />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\temitReadable(stdin, '\\t');\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second', 'Third ✔'].join('\\n'),\n\t);\n\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First ✔', 'Second', 'Third'].join('\\n'),\n\t);\n});\n\ntest('skip disabled component on Tab', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test autoFocus disableSecond />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second', 'Third ✔'].join('\\n'),\n\t);\n});\n\ntest('switch focus to the previous component on Shift+Tab', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test autoFocus />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second ✔', 'Third'].join('\\n'),\n\t);\n\n\temitReadable(stdin, '\\u001B[Z');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First ✔', 'Second', 'Third'].join('\\n'),\n\t);\n});\n\ntest('switch focus to the last component if currently focused component is the first one on Shift+Tab', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test autoFocus />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\temitReadable(stdin, '\\u001B[Z');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second', 'Third ✔'].join('\\n'),\n\t);\n});\n\ntest('skip disabled component on Shift+Tab', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test autoFocus disableSecond />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\temitReadable(stdin, '\\u001B[Z');\n\temitReadable(stdin, '\\u001B[Z');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First ✔', 'Second', 'Third'].join('\\n'),\n\t);\n});\n\ntest('reset focus when focused component unregisters', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tconst {rerender} = render(<Test autoFocus />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\trerender(<Test autoFocus showFirst={false} />);\n\tawait delay(50);\n\n\tt.is((stdout.write as any).lastCall.args[0], ['Second', 'Third'].join('\\n'));\n});\n\ntest('focus first component after focused component unregisters', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tconst {rerender} = render(<Test autoFocus />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\trerender(<Test autoFocus showFirst={false} />);\n\tawait delay(50);\n\n\tt.is((stdout.write as any).lastCall.args[0], ['Second', 'Third'].join('\\n'));\n\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['Second ✔', 'Third'].join('\\n'),\n\t);\n});\n\ntest('toggle focus management', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tconst {rerender} = render(<Test autoFocus />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\trerender(<Test autoFocus disabled />);\n\tawait delay(50);\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First ✔', 'Second', 'Third'].join('\\n'),\n\t);\n\n\trerender(<Test autoFocus />);\n\tawait delay(50);\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second ✔', 'Third'].join('\\n'),\n\t);\n});\n\ntest('manually focus next component', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tconst {rerender} = render(<Test autoFocus />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\trerender(<Test autoFocus focusNext />);\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second ✔', 'Third'].join('\\n'),\n\t);\n});\n\ntest('manually focus previous component', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tconst {rerender} = render(<Test autoFocus />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\trerender(<Test autoFocus focusPrevious />);\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second', 'Third ✔'].join('\\n'),\n\t);\n});\n\ntest('does not crash when focusing next on unmounted children', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tconst {rerender} = render(<Test autoFocus />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\trerender(<Test focusNext unmountChildren />);\n\tawait delay(50);\n\n\tt.is((stdout.write as any).lastCall.args[0], '');\n});\n\ntest('does not crash when focusing previous on unmounted children', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tconst {rerender} = render(<Test autoFocus />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\trerender(<Test focusPrevious unmountChildren />);\n\tawait delay(50);\n\n\tt.is((stdout.write as any).lastCall.args[0], '');\n});\n\ntest('focuses first non-disabled component', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test autoFocus disableFirst disableSecond />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second', 'Third ✔'].join('\\n'),\n\t);\n});\n\ntest('skips disabled elements when wrapping around', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test autoFocus disableFirst />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second ✔', 'Third'].join('\\n'),\n\t);\n});\n\ntest('skips disabled elements when wrapping around from the front', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\trender(<Test autoFocus disableThird />, {\n\t\tstdout,\n\t\tstdin,\n\t\tdebug: true,\n\t});\n\n\tawait delay(50);\n\temitReadable(stdin, '\\u001B[Z');\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second ✔', 'Third'].join('\\n'),\n\t);\n});\n\n// Concurrent mode tests\n// Note: Focus tests with stdin interaction are complex to migrate.\n// These tests verify basic concurrent rendering with focus components.\ntest('focus component renders in concurrent mode', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tconst {act} = await import('react');\n\n\tawait act(async () => {\n\t\trender(<Test />, {\n\t\t\tstdout,\n\t\t\tstdin,\n\t\t\tdebug: true,\n\t\t\tconcurrent: true,\n\t\t});\n\t});\n\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First', 'Second', 'Third'].join('\\n'),\n\t);\n});\n\ntest('focus component with autoFocus renders in concurrent mode', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tconst {act} = await import('react');\n\n\tawait act(async () => {\n\t\trender(<Test autoFocus />, {\n\t\t\tstdout,\n\t\t\tstdin,\n\t\t\tdebug: true,\n\t\t\tconcurrent: true,\n\t\t});\n\t});\n\n\tawait delay(50);\n\n\tt.is(\n\t\t(stdout.write as any).lastCall.args[0],\n\t\t['First ✔', 'Second', 'Third'].join('\\n'),\n\t);\n});\n\nfunction ItemWithId({\n\tlabel,\n\tid,\n\tautoFocus = false,\n}: {\n\treadonly label: string;\n\treadonly id: string;\n\treadonly autoFocus?: boolean;\n}) {\n\tconst {isFocused} = useFocus({id, autoFocus});\n\treturn (\n\t\t<Text>\n\t\t\t{label} {isFocused ? '✔' : null}\n\t\t</Text>\n\t);\n}\n\nfunction ActiveIdReader({\n\tonActiveId,\n}: {\n\treadonly onActiveId: (id: string | undefined) => void;\n}) {\n\tconst {activeId} = useFocusManager();\n\tonActiveId(activeId);\n\treturn null;\n}\n\ntest('activeId from useFocusManager reflects currently focused component', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tlet capturedActiveId: string | undefined;\n\n\trender(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<ActiveIdReader\n\t\t\t\tonActiveId={id => {\n\t\t\t\t\tcapturedActiveId = id;\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<ItemWithId label=\"First\" id=\"first\" />\n\t\t\t<ItemWithId label=\"Second\" id=\"second\" />\n\t\t</Box>,\n\t\t{stdout, stdin, debug: true},\n\t);\n\n\tawait delay(50);\n\tt.is(capturedActiveId, undefined);\n\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\tt.is(capturedActiveId, 'first');\n\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\tt.is(capturedActiveId, 'second');\n});\n\ntest('activeId resets to undefined on Esc', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tlet capturedActiveId: string | undefined;\n\n\trender(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<ActiveIdReader\n\t\t\t\tonActiveId={id => {\n\t\t\t\t\tcapturedActiveId = id;\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<ItemWithId label=\"First\" id=\"first\" />\n\t\t</Box>,\n\t\t{stdout, stdin, debug: true},\n\t);\n\n\tawait delay(50);\n\temitReadable(stdin, '\\t');\n\tawait delay(50);\n\tt.is(capturedActiveId, 'first');\n\n\temitReadable(stdin, '\\u001B');\n\tawait delay(50);\n\tt.is(capturedActiveId, undefined);\n});\n\ntest('activeId is set immediately when component uses autoFocus', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tlet capturedActiveId: string | undefined;\n\n\trender(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<ActiveIdReader\n\t\t\t\tonActiveId={id => {\n\t\t\t\t\tcapturedActiveId = id;\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<ItemWithId autoFocus label=\"First\" id=\"first\" />\n\t\t\t<ItemWithId label=\"Second\" id=\"second\" />\n\t\t</Box>,\n\t\t{stdout, stdin, debug: true},\n\t);\n\n\tawait delay(50);\n\tt.is(capturedActiveId, 'first');\n});\n\ntest('activeId updates when focus is changed programmatically', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tlet capturedActiveId: string | undefined;\n\tlet capturedFocus: ((id: string) => void) | undefined;\n\n\tfunction FocusCapture() {\n\t\tconst {focus} = useFocusManager();\n\t\tcapturedFocus = focus;\n\t\treturn null;\n\t}\n\n\trender(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<ActiveIdReader\n\t\t\t\tonActiveId={id => {\n\t\t\t\t\tcapturedActiveId = id;\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<FocusCapture />\n\t\t\t<ItemWithId label=\"First\" id=\"first\" />\n\t\t\t<ItemWithId label=\"Second\" id=\"second\" />\n\t\t</Box>,\n\t\t{stdout, stdin, debug: true},\n\t);\n\n\tawait delay(50);\n\tt.is(capturedActiveId, undefined);\n\n\tcapturedFocus!('second');\n\tawait delay(50);\n\tt.is(capturedActiveId, 'second');\n\n\tcapturedFocus!('first');\n\tawait delay(50);\n\tt.is(capturedActiveId, 'first');\n});\n\ntest('activeId resets to undefined when focused component unmounts', async t => {\n\tconst stdout = createStdout();\n\tconst stdin = createStdin();\n\tlet capturedActiveId: string | undefined;\n\n\tconst {rerender} = render(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<ActiveIdReader\n\t\t\t\tonActiveId={id => {\n\t\t\t\t\tcapturedActiveId = id;\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<ItemWithId autoFocus label=\"First\" id=\"first\" />\n\t\t\t<ItemWithId label=\"Second\" id=\"second\" />\n\t\t</Box>,\n\t\t{stdout, stdin, debug: true},\n\t);\n\n\tawait delay(50);\n\tt.is(capturedActiveId, 'first');\n\n\trerender(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<ActiveIdReader\n\t\t\t\tonActiveId={id => {\n\t\t\t\t\tcapturedActiveId = id;\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<ItemWithId label=\"Second\" id=\"second\" />\n\t\t</Box>,\n\t);\n\n\tawait delay(50);\n\tt.is(capturedActiveId, undefined);\n});\n"
  },
  {
    "path": "test/gap.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text} from '../src/index.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\n\ntest('gap', t => {\n\tconst output = renderToString(\n\t\t<Box gap={1} width={3} flexWrap=\"wrap\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A B\\n\\nC');\n});\n\ntest('column gap', t => {\n\tconst output = renderToString(\n\t\t<Box gap={1}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A B');\n});\n\ntest('row gap', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" gap={1}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\n\\nB');\n});\n\n// Concurrent mode tests\ntest('gap - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box gap={1} width={3} flexWrap=\"wrap\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A B\\n\\nC');\n});\n\ntest('column gap - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box gap={1}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A B');\n});\n\ntest('row gap - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box flexDirection=\"column\" gap={1}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\n\\nB');\n});\n"
  },
  {
    "path": "test/helpers/create-stdin.ts",
    "content": "import EventEmitter from 'node:events';\nimport {stub} from 'sinon';\n\nexport const createStdin = (): NodeJS.WriteStream => {\n\tconst stdin = new EventEmitter() as unknown as NodeJS.WriteStream;\n\tstdin.isTTY = true;\n\tstdin.setRawMode = stub();\n\tstdin.setEncoding = () => {};\n\tstdin.read = stub();\n\tstdin.unref = () => {};\n\tstdin.ref = () => {};\n\n\treturn stdin;\n};\n\nexport const emitReadable = (\n\tstdin: NodeJS.WriteStream,\n\tchunk: string,\n): void => {\n\t/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment */\n\tconst read = stdin.read as ReturnType<typeof stub>;\n\tread.onCall(0).returns(chunk);\n\tread.onCall(1).returns(null);\n\tstdin.emit('readable');\n\tread.reset();\n\t/* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment */\n};\n"
  },
  {
    "path": "test/helpers/create-stdout.ts",
    "content": "import EventEmitter from 'node:events';\nimport {spy} from 'sinon';\n\n// Fake process.stdout\nexport type FakeStdout = {\n\tget: () => string;\n\tgetWrites: () => string[];\n} & NodeJS.WriteStream;\n\nconst createStdout = (columns?: number, isTTY?: boolean): FakeStdout => {\n\tconst stdout = new EventEmitter() as unknown as FakeStdout;\n\tstdout.columns = columns ?? 100;\n\tstdout.isTTY = isTTY ?? true;\n\n\tconst write = spy();\n\tstdout.write = write;\n\n\tstdout.get = () => write.lastCall.args[0] as string;\n\n\tstdout.getWrites = () => (write.args as string[][]).map(args => args[0]!);\n\n\treturn stdout;\n};\n\nexport default createStdout;\n"
  },
  {
    "path": "test/helpers/force-colors.ts",
    "content": "import chalk, {supportsColor} from 'chalk';\n\n// Force chalk to output colors even in non-TTY environments for testing\nexport const enableTestColors = () => {\n\t// Force chalk to output colors\n\tchalk.level = 3; // Full color support (16m colors)\n};\n\nexport const disableTestColors = () => {\n\t// Restore chalk's automatic detection\n\tchalk.level = supportsColor ? supportsColor.level : 0;\n};\n"
  },
  {
    "path": "test/helpers/render-to-string.ts",
    "content": "import {act} from 'react';\nimport {render} from '../../src/index.js';\nimport createStdout from './create-stdout.js';\n\ntype RenderToStringOptions = {\n\tcolumns?: number;\n\tisScreenReaderEnabled?: boolean;\n};\n\n/**\nSynchronous render to string (legacy mode).\n*/\nexport const renderToString: (\n\tnode: React.JSX.Element,\n\toptions?: RenderToStringOptions,\n) => string = (node, options) => {\n\tconst stdout = createStdout(options?.columns ?? 100);\n\n\trender(node, {\n\t\tstdout,\n\t\tdebug: true,\n\t\tisScreenReaderEnabled: options?.isScreenReaderEnabled,\n\t});\n\n\tconst output = stdout.get();\n\treturn output;\n};\n\n/**\nAsync render to string with concurrent mode support.\n\nUses `act()` to properly flush updates.\n*/\nexport const renderToStringAsync: (\n\tnode: React.JSX.Element,\n\toptions?: RenderToStringOptions,\n) => Promise<string> = async (node, options) => {\n\tconst stdout = createStdout(options?.columns ?? 100);\n\n\tawait act(async () => {\n\t\trender(node, {\n\t\t\tstdout,\n\t\t\tdebug: true,\n\t\t\tisScreenReaderEnabled: options?.isScreenReaderEnabled,\n\t\t\tconcurrent: true,\n\t\t});\n\t});\n\n\tconst output = stdout.get();\n\treturn output;\n};\n"
  },
  {
    "path": "test/helpers/run.ts",
    "content": "import process from 'node:process';\nimport {createRequire} from 'node:module';\nimport path from 'node:path';\nimport url from 'node:url';\n\nconst require = createRequire(import.meta.url);\n\n// eslint-disable-next-line @typescript-eslint/consistent-type-imports\nconst {spawn} = require('node-pty') as typeof import('node-pty');\n\nconst __dirname = url.fileURLToPath(new URL('.', import.meta.url));\n\ntype Run = (\n\tfixture: string,\n\tprops?: {env?: Record<string, string>; columns?: number},\n) => Promise<string>;\n\nexport const run: Run = async (fixture, props) => {\n\tconst env: Record<string, string> = {\n\t\t...(process.env as Record<string, string>),\n\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\tCI: 'false',\n\t\t...props?.env,\n\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\tNODE_NO_WARNINGS: '1',\n\t};\n\n\treturn new Promise<string>((resolve, reject) => {\n\t\tconst term = spawn(\n\t\t\t'node',\n\t\t\t['--import=tsx', path.join(__dirname, `/../fixtures/${fixture}.tsx`)],\n\t\t\t{\n\t\t\t\tname: 'xterm-color',\n\t\t\t\tcols: typeof props?.columns === 'number' ? props.columns : 100,\n\t\t\t\tcwd: __dirname,\n\t\t\t\tenv,\n\t\t\t},\n\t\t);\n\n\t\tlet output = '';\n\n\t\tterm.onData(data => {\n\t\t\toutput += data;\n\t\t});\n\n\t\tterm.onExit(({exitCode}) => {\n\t\t\tif (exitCode === 0) {\n\t\t\t\tresolve(output);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\treject(new Error(`Process exited with a non-zero code: ${exitCode}`));\n\t\t});\n\t});\n};\n"
  },
  {
    "path": "test/helpers/term.ts",
    "content": "import process from 'node:process';\nimport {createRequire} from 'node:module';\nimport path from 'node:path';\nimport url from 'node:url';\n\nconst require = createRequire(import.meta.url);\n\n// eslint-disable-next-line @typescript-eslint/consistent-type-imports\nconst {spawn} = require('node-pty') as typeof import('node-pty');\n\nconst fixturesDir = url.fileURLToPath(new URL('../fixtures', import.meta.url));\n\nconst term = (fixture: string, args: string[] = []) => {\n\tlet resolve: (value?: any) => void;\n\tlet reject: (error?: Error) => void;\n\n\t// eslint-disable-next-line promise/param-names\n\tconst exitPromise = new Promise((resolve2, reject2) => {\n\t\tresolve = resolve2;\n\t\treject = reject2;\n\t});\n\n\tlet readyResolve: () => void;\n\t// eslint-disable-next-line promise/param-names\n\tconst readyPromise = new Promise<void>(r => {\n\t\treadyResolve = r;\n\t});\n\n\tconst env: Record<string, string> = {\n\t\t...(process.env as Record<string, string>),\n\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\tNODE_NO_WARNINGS: '1',\n\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\tCI: 'false',\n\t};\n\n\tconst ps = spawn(\n\t\t'node',\n\t\t['--import=tsx', path.join(fixturesDir, `${fixture}.tsx`), ...args],\n\t\t{\n\t\t\tname: 'xterm-color',\n\t\t\tcols: 100,\n\t\t\tcwd: fixturesDir,\n\t\t\tenv,\n\t\t},\n\t);\n\n\tconst result = {\n\t\twrite(input: string) {\n\t\t\t// Wait for the fixture to signal it's ready to accept input\n\n\t\t\tvoid readyPromise.then(() => {\n\t\t\t\tps.write(input);\n\t\t\t});\n\t\t},\n\t\toutput: '',\n\t\twaitForExit: async () => exitPromise,\n\t};\n\n\tps.onData(data => {\n\t\tresult.output += data;\n\n\t\tif (result.output.includes('__READY__')) {\n\t\t\treadyResolve();\n\t\t}\n\t});\n\n\tps.onExit(({exitCode}) => {\n\t\tif (exitCode === 0) {\n\t\t\tresolve();\n\t\t\treturn;\n\t\t}\n\n\t\treject(new Error(`Process exited with non-zero exit code: ${exitCode}`));\n\t});\n\n\treturn result;\n};\n\nexport default term;\n"
  },
  {
    "path": "test/helpers/test-renderer.ts",
    "content": "import {act} from 'react';\nimport {render, type Instance} from '../../src/index.js';\nimport createStdout from './create-stdout.js';\n\ntype TestRenderOptions = {\n\tcolumns?: number;\n\tisScreenReaderEnabled?: boolean;\n};\n\nexport type TestInstance = Instance & {\n\tstdout: ReturnType<typeof createStdout>;\n\tgetOutput: () => string;\n\trerenderAsync: (node: React.ReactNode) => Promise<void>;\n};\n\n/**\nRender helper that supports concurrent mode with `act()` wrapping.\n\nUses `act()` to properly flush updates in concurrent mode.\n*/\nexport async function renderAsync(\n\tnode: React.ReactNode,\n\toptions: TestRenderOptions = {},\n): Promise<TestInstance> {\n\tconst stdout = createStdout(options.columns ?? 100);\n\n\tlet instance!: Instance;\n\n\tawait act(async () => {\n\t\tinstance = render(node, {\n\t\t\tstdout,\n\t\t\tdebug: true,\n\t\t\tconcurrent: true,\n\t\t\tisScreenReaderEnabled: options.isScreenReaderEnabled,\n\t\t});\n\t});\n\n\treturn {\n\t\t...instance,\n\t\tstdout,\n\t\tgetOutput: () => stdout.get(),\n\t\tasync rerenderAsync(newNode: React.ReactNode) {\n\t\t\tawait act(async () => {\n\t\t\t\tinstance.rerender(newNode);\n\t\t\t});\n\t\t},\n\t};\n}\n\n/**\nSynchronous render for legacy mode tests (backward compatible).\n*/\nexport function renderSync(\n\tnode: React.ReactNode,\n\toptions: TestRenderOptions = {},\n): TestInstance {\n\tconst stdout = createStdout(options.columns ?? 100);\n\n\tconst instance = render(node, {\n\t\tstdout,\n\t\tdebug: true,\n\t\tconcurrent: false,\n\t\tisScreenReaderEnabled: options.isScreenReaderEnabled,\n\t});\n\n\treturn {\n\t\t...instance,\n\t\tstdout,\n\t\tgetOutput: () => stdout.get(),\n\t\tasync rerenderAsync(newNode: React.ReactNode) {\n\t\t\tinstance.rerender(newNode);\n\t\t},\n\t};\n}\n\n/**\nWrapper to make existing sync code work with concurrent mode.\n\nUse this to gradually migrate tests.\n*/\nexport async function withAct<T>(fn: () => T | Promise<T>): Promise<T> {\n\tlet result!: T;\n\tawait act(async () => {\n\t\tresult = await fn();\n\t});\n\treturn result;\n}\n\n/**\nWait for pending suspense boundaries to resolve.\n*/\nexport async function waitForSuspense(ms = 0): Promise<void> {\n\tawait act(async () => {\n\t\tawait new Promise<void>(resolve => {\n\t\t\tsetTimeout(resolve, ms);\n\t\t});\n\t});\n}\n"
  },
  {
    "path": "test/hooks-use-input-kitty.tsx",
    "content": "import test from 'ava';\nimport term from './helpers/term.js';\n\ntest.serial('useInput - handle kitty protocol super modifier', async t => {\n\tconst ps = term('use-input-kitty', ['super']);\n\t// 's' with super modifier (modifier 9 = super(8) + 1)\n\tps.write('\\u001B[115;9u');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle kitty protocol hyper modifier', async t => {\n\tconst ps = term('use-input-kitty', ['hyper']);\n\t// 'h' with hyper modifier (modifier 17 = hyper(16) + 1)\n\tps.write('\\u001B[104;17u');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle kitty protocol capsLock', async t => {\n\tconst ps = term('use-input-kitty', ['capsLock']);\n\t// 'a' with capsLock (modifier 65 = capsLock(64) + 1)\n\tps.write('\\u001B[97;65u');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle kitty protocol numLock', async t => {\n\tconst ps = term('use-input-kitty', ['numLock']);\n\t// 'a' with numLock (modifier 129 = numLock(128) + 1)\n\tps.write('\\u001B[97;129u');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle kitty protocol super+ctrl', async t => {\n\tconst ps = term('use-input-kitty', ['superCtrl']);\n\t// 's' with super+ctrl (modifier 13 = super(8) + ctrl(4) + 1)\n\tps.write('\\u001B[115;13u');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle kitty protocol press event', async t => {\n\tconst ps = term('use-input-kitty', ['press']);\n\t// 'a' press event (eventType 1 = press)\n\tps.write('\\u001B[97;1:1u');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle kitty protocol repeat event', async t => {\n\tconst ps = term('use-input-kitty', ['repeat']);\n\t// 'a' repeat event (eventType 2 = repeat)\n\tps.write('\\u001B[97;1:2u');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle kitty protocol release event', async t => {\n\tconst ps = term('use-input-kitty', ['release']);\n\t// 'a' release event (eventType 3 = release)\n\tps.write('\\u001B[97;1:3u');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle kitty protocol escape key', async t => {\n\tconst ps = term('use-input-kitty', ['escapeKitty']);\n\t// Escape key\n\tps.write('\\u001B[27u');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial(\n\t'useInput - non-printable kitty key (capslock) produces empty input',\n\tasync t => {\n\t\tconst ps = term('use-input-kitty', ['nonPrintable']);\n\t\t// Capslock (codepoint 57358)\n\t\tps.write('\\u001B[57358u');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n\ntest.serial(\n\t'useInput - non-printable kitty key (f13) produces empty input',\n\tasync t => {\n\t\tconst ps = term('use-input-kitty', ['nonPrintable']);\n\t\t// F13 (codepoint 57376)\n\t\tps.write('\\u001B[57376u');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n\ntest.serial(\n\t'useInput - non-printable kitty key (printscreen) produces empty input',\n\tasync t => {\n\t\tconst ps = term('use-input-kitty', ['nonPrintable']);\n\t\t// PrintScreen (codepoint 57361)\n\t\tps.write('\\u001B[57361u');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n\ntest.serial(\n\t'useInput - kitty protocol space key produces space input',\n\tasync t => {\n\t\tconst ps = term('use-input-kitty', ['space']);\n\t\t// Space key (codepoint 32)\n\t\tps.write('\\u001B[32u');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n\ntest.serial(\n\t'useInput - kitty protocol return key produces carriage return input',\n\tasync t => {\n\t\tconst ps = term('use-input-kitty', ['returnKey']);\n\t\t// Return key (codepoint 13)\n\t\tps.write('\\u001B[13u');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n\ntest.serial(\n\t'useInput - kitty protocol ctrl+letter via codepoint 1-26 produces input',\n\tasync t => {\n\t\tconst ps = term('use-input-kitty', ['ctrlLetter']);\n\t\t// Ctrl+a via codepoint 1 form (modifier 5 = ctrl(4) + 1)\n\t\tps.write('\\u001B[1;5u');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n"
  },
  {
    "path": "test/hooks-use-input-navigation.tsx",
    "content": "import test from 'ava';\nimport term from './helpers/term.js';\n\ntest.serial('useInput - handle up arrow', async t => {\n\tconst ps = term('use-input', ['upArrow']);\n\tps.write('\\u001B[A');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle down arrow', async t => {\n\tconst ps = term('use-input', ['downArrow']);\n\tps.write('\\u001B[B');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle left arrow', async t => {\n\tconst ps = term('use-input', ['leftArrow']);\n\tps.write('\\u001B[D');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle right arrow', async t => {\n\tconst ps = term('use-input', ['rightArrow']);\n\tps.write('\\u001B[C');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial(\n\t'useInput - handles rapid arrows and enter in one chunk',\n\tasync t => {\n\t\tconst ps = term('use-input', ['rapidArrowsEnter']);\n\t\tps.write('\\u001B[B\\u001B[B\\u001B[B\\r');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n\ntest.serial('useInput - handle meta + up arrow', async t => {\n\tconst ps = term('use-input', ['upArrowMeta']);\n\tps.write('\\u001B\\u001B[A');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle meta + down arrow', async t => {\n\tconst ps = term('use-input', ['downArrowMeta']);\n\tps.write('\\u001B\\u001B[B');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle meta + left arrow', async t => {\n\tconst ps = term('use-input', ['leftArrowMeta']);\n\tps.write('\\u001B\\u001B[D');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle meta + right arrow', async t => {\n\tconst ps = term('use-input', ['rightArrowMeta']);\n\tps.write('\\u001B\\u001B[C');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle ctrl + up arrow', async t => {\n\tconst ps = term('use-input', ['upArrowCtrl']);\n\tps.write('\\u001B[1;5A');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle ctrl + down arrow', async t => {\n\tconst ps = term('use-input', ['downArrowCtrl']);\n\tps.write('\\u001B[1;5B');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle ctrl + left arrow', async t => {\n\tconst ps = term('use-input', ['leftArrowCtrl']);\n\tps.write('\\u001B[1;5D');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle ctrl + right arrow', async t => {\n\tconst ps = term('use-input', ['rightArrowCtrl']);\n\tps.write('\\u001B[1;5C');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle page down', async t => {\n\tconst ps = term('use-input', ['pageDown']);\n\tps.write('\\u001B[6~');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle page up', async t => {\n\tconst ps = term('use-input', ['pageUp']);\n\tps.write('\\u001B[5~');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle home', async t => {\n\tconst ps = term('use-input', ['home']);\n\tps.write('\\u001B[H');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle end', async t => {\n\tconst ps = term('use-input', ['end']);\n\tps.write('\\u001B[F');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n"
  },
  {
    "path": "test/hooks-use-input.tsx",
    "content": "import test from 'ava';\nimport term from './helpers/term.js';\n\ntest.serial(\n\t'useInput - discrete priority keeps states in sync with useTransition during rapid input',\n\tasync t => {\n\t\tconst ps = term('use-input-discrete-priority');\n\t\t// Simulate rapid delete key repeat at ~30ms intervals.\n\t\t// State starts pre-populated with \"abcde\". Send 5 rapid deletes\n\t\t// to clear it, then wait for transitions to settle and check state.\n\t\tconst delay = async (ms: number) =>\n\t\t\tnew Promise(resolve => {\n\t\t\t\tsetTimeout(resolve, ms);\n\t\t\t});\n\t\tconst pressDeleteKey = () => {\n\t\t\tps.write('\\u001B[3~');\n\t\t};\n\n\t\t// Use escape sequence for delete key (raw \\x7F gets processed by pty)\n\t\tfor (const delayMilliseconds of [0, 30, 60, 90, 120]) {\n\t\t\tsetTimeout(() => {\n\t\t\t\tpressDeleteKey();\n\t\t\t}, delayMilliseconds);\n\t\t}\n\n\t\tawait delay(200);\n\n\t\t// Wait for all transitions to settle, then press Enter to report state\n\t\tawait delay(2000);\n\t\tps.write('\\r');\n\t\tawait ps.waitForExit();\n\t\tconst finalMatch = /FINAL .+/.exec(ps.output);\n\t\tt.log('Output:', finalMatch?.[0] ?? ps.output.slice(-300));\n\t\tt.true(ps.output.includes('FINAL query:\"\" deferred:\"\"'));\n\t},\n);\n\ntest.serial('useInput - handle lowercase character', async t => {\n\tconst ps = term('use-input', ['lowercase']);\n\tps.write('q');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle uppercase character', async t => {\n\tconst ps = term('use-input', ['uppercase']);\n\tps.write('Q');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial(\n\t'useInput - \\\\r should not count as an uppercase character',\n\tasync t => {\n\t\tconst ps = term('use-input', ['uppercase']);\n\t\tps.write('\\r');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n\ntest.serial('useInput - pasted carriage return', async t => {\n\tconst ps = term('use-input', ['pastedCarriageReturn']);\n\tps.write('\\rtest');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - pasted tab', async t => {\n\tconst ps = term('use-input', ['pastedTab']);\n\tps.write('\\ttest');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial(\n\t'useInput - receives bracketed paste when no usePaste handler is active',\n\tasync t => {\n\t\tconst ps = term('use-input', ['bracketedPaste']);\n\t\tps.write('\\u001B[200~hello\\u001B[201~');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n\ntest.serial('useInput - handle escape', async t => {\n\tconst ps = term('use-input', ['escape']);\n\tps.write('\\u001B');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle ctrl', async t => {\n\tconst ps = term('use-input', ['ctrl']);\n\tps.write('\\u0006');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle meta', async t => {\n\tconst ps = term('use-input', ['meta']);\n\tps.write('\\u001Bm');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - flushes ESC[ prefix as literal input', async t => {\n\tconst ps = term('use-input', ['escapeBracketPrefix']);\n\tps.write('\\u001B[');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle meta + O with pending flush', async t => {\n\tconst ps = term('use-input', ['metaUpperO']);\n\tps.write('\\u001BO');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle tab', async t => {\n\tconst ps = term('use-input', ['tab']);\n\tps.write('\\t');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle shift + tab', async t => {\n\tconst ps = term('use-input', ['shiftTab']);\n\tps.write('\\u001B[Z');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle backspace', async t => {\n\tconst ps = term('use-input', ['backspace']);\n\tps.write('\\u0008');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle delete', async t => {\n\tconst ps = term('use-input', ['delete']);\n\tps.write('\\u007F');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle remove (delete)', async t => {\n\tconst ps = term('use-input', ['remove']);\n\tps.write('\\u001B[3~');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n\ntest.serial('useInput - handle option + return (macOS)', async t => {\n\tconst ps = term('use-input', ['returnMeta']);\n\tps.write('\\u001B\\r');\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes('exited'));\n});\n"
  },
  {
    "path": "test/hooks-use-paste.tsx",
    "content": "import test from 'ava';\nimport term from './helpers/term.js';\n\ntest.serial(\n\t'usePaste - receives bracketed paste as single text blob',\n\tasync t => {\n\t\tconst ps = term('use-paste', ['basic']);\n\t\tps.write('\\u001B[200~hello world\\u001B[201~');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t\tt.true(\n\t\t\tps.output.includes('\\u001B[?2004h'),\n\t\t\t'bracketed paste mode was enabled',\n\t\t);\n\t\tt.true(\n\t\t\tps.output.includes('\\u001B[?2004l'),\n\t\t\t'bracketed paste mode was disabled on exit',\n\t\t);\n\t},\n);\n\ntest.serial(\n\t'usePaste - paste content with escape sequences is delivered verbatim',\n\tasync t => {\n\t\tconst ps = term('use-paste', ['escapeSequences']);\n\t\tps.write('\\u001B[200~hello\\u001B[Aworld\\u001B[201~');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n\ntest.serial(\n\t'usePaste - useInput does not receive bracketed paste content',\n\tasync t => {\n\t\tconst ps = term('use-paste', ['noUseInput']);\n\t\tps.write('\\u001B[200~hello\\u001B[201~');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n\ntest.serial(\n\t'usePaste - multiple simultaneous hooks both receive the same paste event',\n\tasync t => {\n\t\tconst ps = term('use-paste', ['multipleHooks']);\n\t\tps.write('\\u001B[200~hello\\u001B[201~');\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n"
  },
  {
    "path": "test/hooks.tsx",
    "content": "import test, {type ExecutionContext} from 'ava';\nimport stripAnsi from 'strip-ansi';\nimport term from './helpers/term.js';\n\ntest.serial('useInput - ignore input if not active', async t => {\n\tconst ps = term('use-input-multiple');\n\tps.write('x');\n\tawait ps.waitForExit();\n\tt.false(ps.output.includes('xx'));\n\tt.true(ps.output.includes('x'));\n\tt.true(ps.output.includes('exited'));\n});\n\n// For some reason this test is flaky, so we have to resort to using `t.try` to run it multiple times\ntest.serial(\n\t'useInput - handle Ctrl+C when `exitOnCtrlC` is `false`',\n\tasync t => {\n\t\tconst run = async (tt: ExecutionContext) => {\n\t\t\tconst ps = term('use-input-ctrl-c');\n\t\t\tps.write('\\u0003');\n\t\t\tawait ps.waitForExit();\n\t\t\ttt.true(ps.output.includes('exited'));\n\t\t};\n\n\t\tconst firstTry = await t.try(run);\n\n\t\tif (firstTry.passed) {\n\t\t\tfirstTry.commit();\n\t\t\treturn;\n\t\t}\n\n\t\tfirstTry.discard();\n\n\t\tconst secondTry = await t.try(run);\n\n\t\tif (secondTry.passed) {\n\t\t\tsecondTry.commit();\n\t\t\treturn;\n\t\t}\n\n\t\tsecondTry.discard();\n\n\t\tconst thirdTry = await t.try(run);\n\t\tthirdTry.commit();\n\t},\n);\n\ntest.serial(\n\t'useInput - no MaxListenersExceededWarning with many useInput hooks',\n\tasync t => {\n\t\tconst ps = term('use-input-many');\n\t\tawait ps.waitForExit();\n\t\tt.false(ps.output.includes('MaxListenersExceededWarning'));\n\t\tt.true(ps.output.includes('exited'));\n\t},\n);\n\ntest.serial(\n\t'useInput - handle Ctrl+C via kitty codepoint-3 form when `exitOnCtrlC` is `false`',\n\tasync t => {\n\t\tconst run = async (tt: ExecutionContext) => {\n\t\t\tconst ps = term('use-input-ctrl-c');\n\t\t\t// Ctrl+C via kitty codepoint 3 form (modifier 5 = ctrl(4) + 1)\n\t\t\tps.write('\\u001B[3;5u');\n\t\t\tawait ps.waitForExit();\n\t\t\ttt.true(ps.output.includes('exited'));\n\t\t};\n\n\t\tconst firstTry = await t.try(run);\n\n\t\tif (firstTry.passed) {\n\t\t\tfirstTry.commit();\n\t\t\treturn;\n\t\t}\n\n\t\tfirstTry.discard();\n\n\t\tconst secondTry = await t.try(run);\n\n\t\tif (secondTry.passed) {\n\t\t\tsecondTry.commit();\n\t\t\treturn;\n\t\t}\n\n\t\tsecondTry.discard();\n\n\t\tconst thirdTry = await t.try(run);\n\t\tthirdTry.commit();\n\t},\n);\n\ntest.serial('useStdout - write to stdout', async t => {\n\tconst ps = term('use-stdout');\n\tawait ps.waitForExit();\n\n\tconst lines = stripAnsi(ps.output).split('\\r\\n');\n\n\tt.deepEqual(lines.slice(1, -1), [\n\t\t'Hello from Ink to stdout',\n\t\t'Hello World',\n\t\t'exited',\n\t]);\n});\n\n// `node-pty` doesn't support streaming stderr output, so I need to figure out\n// how to test useStderr() hook. child_process.spawn() can't be used, because\n// Ink fails with \"raw mode unsupported\" error.\ntest.todo('useStderr - write to stderr');\n"
  },
  {
    "path": "test/input-parser.ts",
    "content": "import test from 'ava';\nimport {createInputParser, type InputEvent} from '../src/input-parser.js';\n\nconst parseChunks = (chunks: string[]): InputEvent[] => {\n\tconst parser = createInputParser();\n\tconst events: InputEvent[] = [];\n\n\tfor (const chunk of chunks) {\n\t\tevents.push(...parser.push(chunk));\n\t}\n\n\treturn events;\n};\n\ntest('passes through plain text chunks', t => {\n\tt.deepEqual(parseChunks(['hello', ' ', 'world']), ['hello', ' ', 'world']);\n});\n\ntest('keeps plain text and control sequences separate', t => {\n\tt.deepEqual(parseChunks(['a\\u001B[Ab']), ['a', '\\u001B[A', 'b']);\n});\n\ntest('parses multiple standard CSI keys in one chunk', t => {\n\tt.deepEqual(parseChunks(['\\u001B[A\\u001B[B\\u001B[C\\u001B[D']), [\n\t\t'\\u001B[A',\n\t\t'\\u001B[B',\n\t\t'\\u001B[C',\n\t\t'\\u001B[D',\n\t]);\n});\n\ntest('parses CSI sequences with parameters', t => {\n\tt.deepEqual(parseChunks(['\\u001B[1;5A\\u001B[5~\\u001B[6~']), [\n\t\t'\\u001B[1;5A',\n\t\t'\\u001B[5~',\n\t\t'\\u001B[6~',\n\t]);\n});\n\ntest('parses kitty protocol sequence as one key event', t => {\n\tt.deepEqual(parseChunks(['\\u001B[97;5u']), ['\\u001B[97;5u']);\n});\n\ntest('parses SS3 sequences as one key event', t => {\n\tt.deepEqual(parseChunks(['\\u001BOA\\u001BOB\\u001BOC\\u001BOD']), [\n\t\t'\\u001BOA',\n\t\t'\\u001BOB',\n\t\t'\\u001BOC',\n\t\t'\\u001BOD',\n\t]);\n});\n\ntest('does not consume a following escape as SS3 final byte', t => {\n\tt.deepEqual(parseChunks(['\\u001BO\\u001B[A']), ['\\u001BO', '\\u001B[A']);\n});\n\ntest('parses meta+CSI sequence with double escape', t => {\n\tt.deepEqual(parseChunks(['\\u001B\\u001B[A']), ['\\u001B\\u001B[A']);\n});\n\ntest('parses escaped printable code points', t => {\n\tt.deepEqual(parseChunks(['\\u001Bx\\u001B1']), ['\\u001Bx', '\\u001B1']);\n});\n\ntest('parses escaped supplementary code points', t => {\n\tt.deepEqual(parseChunks(['\\u001B😀']), ['\\u001B😀']);\n});\n\ntest('preserves legacy ESC[[... sequences in a mixed chunk', t => {\n\tt.deepEqual(parseChunks(['\\u001B[[A\\u001B[[5~']), [\n\t\t'\\u001B[[A',\n\t\t'\\u001B[[5~',\n\t]);\n});\n\ntest('preserves legacy ESC[[... sequences across chunks', t => {\n\tt.deepEqual(parseChunks(['\\u001B[[', 'A\\u001B[[5~']), [\n\t\t'\\u001B[[A',\n\t\t'\\u001B[[5~',\n\t]);\n});\n\ntest('parses legacy and standard CSI sequences mixed together', t => {\n\tt.deepEqual(parseChunks(['\\u001B[[A\\u001B[B\\u001B[[6~\\u001B[1;5D']), [\n\t\t'\\u001B[[A',\n\t\t'\\u001B[B',\n\t\t'\\u001B[[6~',\n\t\t'\\u001B[1;5D',\n\t]);\n});\n\ntest('holds incomplete CSI sequence until final byte arrives', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B['), []);\n\tt.true(parser.hasPendingEscape());\n\tt.deepEqual(parser.push('1;5'), []);\n\tt.deepEqual(parser.push('A'), ['\\u001B[1;5A']);\n});\n\ntest('holds incomplete legacy ESC[[... sequence until final byte arrives', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B[['), []);\n\tt.deepEqual(parser.push('5'), []);\n\tt.deepEqual(parser.push('~'), ['\\u001B[[5~']);\n});\n\ntest('holds incomplete SS3 sequence until final byte arrives', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001BO'), []);\n\tt.deepEqual(parser.push('A'), ['\\u001BOA']);\n});\n\ntest('holds incomplete double-escape CSI sequence until final byte arrives', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B\\u001B['), []);\n\tt.deepEqual(parser.push('A'), ['\\u001B\\u001B[A']);\n});\n\ntest('keeps pending plain escape and can flush it', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B'), []);\n\tt.true(parser.hasPendingEscape());\n\tt.is(parser.flushPendingEscape(), '\\u001B');\n\tt.false(parser.hasPendingEscape());\n});\n\ntest('flushes pending CSI prefix as literal input', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B['), []);\n\tt.true(parser.hasPendingEscape());\n\tt.is(parser.flushPendingEscape(), '\\u001B[');\n\tt.false(parser.hasPendingEscape());\n\tt.deepEqual(parser.push('A'), ['A']);\n});\n\ntest('reset clears pending input state', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B['), []);\n\tparser.reset();\n\tt.deepEqual(parser.push('A'), ['A']);\n});\n\ntest('treats invalid CSI continuation as escaped code point plus plain text', t => {\n\tt.deepEqual(parseChunks(['\\u001B[\\n']), ['\\u001B[', '\\n']);\n});\n\ntest('parses mixed text and many key events in one read', t => {\n\tt.deepEqual(parseChunks(['start\\u001B[A mid \\u001BOH end\\u001B[[5~']), [\n\t\t'start',\n\t\t'\\u001B[A',\n\t\t' mid ',\n\t\t'\\u001BOH',\n\t\t' end',\n\t\t'\\u001B[[5~',\n\t]);\n});\n\ntest('flushes pending SS3 prefix as literal input', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001BO'), []);\n\tt.true(parser.hasPendingEscape());\n\tt.is(parser.flushPendingEscape(), '\\u001BO');\n\tt.deepEqual(parser.push('x'), ['x']);\n});\n\ntest('flushes pending legacy CSI prefix as literal input', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B[['), []);\n\tt.true(parser.hasPendingEscape());\n\tt.is(parser.flushPendingEscape(), '\\u001B[[');\n\tt.deepEqual(parser.push('x'), ['x']);\n});\n\ntest('parses meta+SS3 sequence with double escape', t => {\n\tt.deepEqual(parseChunks(['\\u001B\\u001BOA']), ['\\u001B\\u001BOA']);\n});\n\ntest('holds incomplete double-escape SS3 sequence until final byte arrives', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B\\u001BO'), []);\n\tt.true(parser.hasPendingEscape());\n\tt.deepEqual(parser.push('A'), ['\\u001B\\u001BOA']);\n});\n\ntest('emits double escape as single event for non-control character', t => {\n\tt.deepEqual(parseChunks(['\\u001B\\u001Bx']), ['\\u001B\\u001B', 'x']);\n});\n\ntest('empty chunk produces no events', t => {\n\tt.deepEqual(parseChunks(['']), []);\n});\n\ntest('empty chunk does not disturb pending state', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B['), []);\n\tt.deepEqual(parser.push(''), []);\n\tt.true(parser.hasPendingEscape());\n\tt.deepEqual(parser.push('A'), ['\\u001B[A']);\n});\n\ntest('plain text followed by incomplete escape holds escape as pending', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('hello\\u001B'), ['hello']);\n\tt.true(parser.hasPendingEscape());\n\tt.is(parser.flushPendingEscape(), '\\u001B');\n});\n\nconst deleteAndBackspaceCases = [\n\t{\n\t\ttitle: 'splits batched delete characters into individual events',\n\t\tchunks: ['\\u007F\\u007F\\u007F'],\n\t\tevents: ['\\u007F', '\\u007F', '\\u007F'],\n\t},\n\t{\n\t\ttitle: 'splits batched backspace characters into individual events',\n\t\tchunks: ['\\u0008\\u0008\\u0008'],\n\t\tevents: ['\\u0008', '\\u0008', '\\u0008'],\n\t},\n\t{\n\t\ttitle: 'splits mixed delete and backspace characters',\n\t\tchunks: ['\\u007F\\u0008\\u007F'],\n\t\tevents: ['\\u007F', '\\u0008', '\\u007F'],\n\t},\n\t{\n\t\ttitle: 'splits mixed printable text and delete characters',\n\t\tchunks: ['abc\\u007F\\u007F\\u007F'],\n\t\tevents: ['abc', '\\u007F', '\\u007F', '\\u007F'],\n\t},\n\t{\n\t\ttitle: 'single delete character is preserved as individual event',\n\t\tchunks: ['\\u007F'],\n\t\tevents: ['\\u007F'],\n\t},\n\t{\n\t\ttitle: 'single backspace character is preserved as individual event',\n\t\tchunks: ['\\u0008'],\n\t\tevents: ['\\u0008'],\n\t},\n\t{\n\t\ttitle: 'splits trailing delete from text',\n\t\tchunks: ['abc\\u007F'],\n\t\tevents: ['abc', '\\u007F'],\n\t},\n\t{\n\t\ttitle: 'splits delete characters before escape sequences',\n\t\tchunks: ['\\u007F\\u007F\\u001B[A'],\n\t\tevents: ['\\u007F', '\\u007F', '\\u001B[A'],\n\t},\n\t{\n\t\ttitle: 'splits delete characters after escape sequences',\n\t\tchunks: ['\\u001B[A\\u007F\\u007F'],\n\t\tevents: ['\\u001B[A', '\\u007F', '\\u007F'],\n\t},\n\t{\n\t\ttitle: 'splits delete characters between escape sequences',\n\t\tchunks: ['\\u001B[A\\u007F\\u001B[B'],\n\t\tevents: ['\\u001B[A', '\\u007F', '\\u001B[B'],\n\t},\n\t{\n\t\ttitle: 'splits backspace characters around escape sequences',\n\t\tchunks: ['\\u0008\\u001B[A\\u0008'],\n\t\tevents: ['\\u0008', '\\u001B[A', '\\u0008'],\n\t},\n\t{\n\t\ttitle: 'splits interleaved text and delete characters',\n\t\tchunks: ['ab\\u007Fcd'],\n\t\tevents: ['ab', '\\u007F', 'cd'],\n\t},\n\t{\n\t\ttitle: 'does not split pasted carriage return from text',\n\t\tchunks: ['\\rtest'],\n\t\tevents: ['\\rtest'],\n\t},\n\t{\n\t\ttitle: 'does not split pasted tab from text',\n\t\tchunks: ['\\ttest'],\n\t\tevents: ['\\ttest'],\n\t},\n] as const;\n\nfor (const testCase of deleteAndBackspaceCases) {\n\ttest(testCase.title, t => {\n\t\tt.deepEqual(parseChunks(testCase.chunks), testCase.events);\n\t});\n}\n\ntest('assembles CSI sequence from single-byte chunks', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B'), []);\n\tt.deepEqual(parser.push('['), []);\n\tt.deepEqual(parser.push('1'), []);\n\tt.deepEqual(parser.push(';'), []);\n\tt.deepEqual(parser.push('5'), []);\n\tt.deepEqual(parser.push('A'), ['\\u001B[1;5A']);\n});\n\ntest('emits paste event for bracketed paste sequence', t => {\n\tt.deepEqual(parseChunks(['\\u001B[200~hello world\\u001B[201~']), [\n\t\t{paste: 'hello world'},\n\t]);\n});\n\ntest('emits paste event for multiline bracketed paste', t => {\n\tt.deepEqual(parseChunks(['\\u001B[200~line1\\nline2\\u001B[201~']), [\n\t\t{paste: 'line1\\nline2'},\n\t]);\n});\n\ntest('paste content with escape sequences is delivered verbatim', t => {\n\tt.deepEqual(parseChunks(['\\u001B[200~hello\\u001B[Aworld\\u001B[201~']), [\n\t\t{paste: 'hello\\u001B[Aworld'},\n\t]);\n});\n\ntest('emits normal events before and after bracketed paste', t => {\n\tt.deepEqual(parseChunks(['before\\u001B[200~pasted\\u001B[201~after']), [\n\t\t'before',\n\t\t{paste: 'pasted'},\n\t\t'after',\n\t]);\n});\n\ntest('emits multiple paste events in one chunk', t => {\n\tt.deepEqual(\n\t\tparseChunks(['\\u001B[200~first\\u001B[201~mid\\u001B[200~second\\u001B[201~']),\n\t\t[{paste: 'first'}, 'mid', {paste: 'second'}],\n\t);\n});\n\ntest('holds incomplete bracketed paste as pending', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B[200~hello'), []);\n\tt.false(parser.hasPendingEscape());\n\tt.deepEqual(parser.push(' world\\u001B[201~'), [{paste: 'hello world'}]);\n});\n\ntest('assembles bracketed paste from chunk-by-chunk delivery', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B[200~'), []);\n\tt.deepEqual(parser.push('hello'), []);\n\tt.deepEqual(parser.push('\\u001B[201~'), [{paste: 'hello'}]);\n});\n\ntest('emits empty paste for adjacent paste markers', t => {\n\tt.deepEqual(parseChunks(['\\u001B[200~\\u001B[201~']), [{paste: ''}]);\n});\n\ntest('handles pasteStart split before the tilde (\\\\u001B[200 without ~)', t => {\n\tconst parser = createInputParser();\n\n\t// Chunk ends exactly at the 5th byte of the 6-byte pasteStart sequence.\n\t// Keep waiting for the final `~` to avoid splitting bracketed paste input.\n\tt.deepEqual(parser.push('\\u001B[200'), []);\n\tt.false(parser.hasPendingEscape());\n\tt.deepEqual(parser.push('~hello\\u001B[201~'), [{paste: 'hello'}]);\n});\n\ntest('hasPendingEscape returns true for length-3 pasteStart prefix (\\\\u001B[2)', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B[2'), []);\n\tt.true(parser.hasPendingEscape());\n});\n\ntest('hasPendingEscape returns true for length-4 pasteStart prefix (\\\\u001B[20)', t => {\n\tconst parser = createInputParser();\n\n\tt.deepEqual(parser.push('\\u001B[20'), []);\n\tt.true(parser.hasPendingEscape());\n});\n\ntest('paste event delivers delete and backspace chars verbatim without splitting', t => {\n\tt.deepEqual(parseChunks(['\\u001B[200~\\u007F\\u0008\\u007F\\u001B[201~']), [\n\t\t{paste: '\\u007F\\u0008\\u007F'},\n\t]);\n});\n"
  },
  {
    "path": "test/kitty-keyboard.tsx",
    "content": "import process from 'node:process';\nimport EventEmitter from 'node:events';\nimport {Buffer} from 'node:buffer';\nimport React from 'react';\nimport test from 'ava';\nimport {stub, spy} from 'sinon';\nimport parseKeypress from '../src/parse-keypress.js';\nimport {render, Text} from '../src/index.js';\n\n// Helper to create kitty protocol CSI u sequences\nconst kittyKey = (\n\tcodepoint: number,\n\tmodifiers?: number,\n\teventType?: number,\n\ttextCodepoints?: number[],\n): string => {\n\tlet seq = `\\u001B[${codepoint}`;\n\tif (\n\t\tmodifiers !== undefined ||\n\t\teventType !== undefined ||\n\t\ttextCodepoints !== undefined\n\t) {\n\t\tseq += `;${modifiers ?? 1}`;\n\t}\n\n\tif (eventType !== undefined || textCodepoints !== undefined) {\n\t\tseq += `:${eventType ?? 1}`;\n\t}\n\n\tif (textCodepoints !== undefined) {\n\t\tseq += `;${textCodepoints.join(':')}`;\n\t}\n\n\tseq += 'u';\n\treturn seq;\n};\n\ntest('kitty protocol - simple character', t => {\n\t// 'a' key\n\tconst result = parseKeypress(kittyKey(97));\n\tt.is(result.name, 'a');\n\tt.false(result.ctrl);\n\tt.false(result.shift);\n\tt.false(result.meta);\n\tt.is(result.eventType, 'press');\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - uppercase character (shift)', t => {\n\t// 'A' with shift (modifier 2 = shift + 1)\n\tconst result = parseKeypress(kittyKey(65, 2));\n\tt.is(result.name, 'a');\n\tt.true(result.shift);\n\tt.false(result.ctrl);\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - ctrl modifier', t => {\n\t// 'a' with ctrl (modifier 5 = ctrl(4) + 1)\n\tconst result = parseKeypress(kittyKey(97, 5));\n\tt.is(result.name, 'a');\n\tt.true(result.ctrl);\n\tt.false(result.shift);\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - alt/option modifier', t => {\n\t// 'a' with alt (modifier 3 = alt(2) + 1)\n\tconst result = parseKeypress(kittyKey(97, 3));\n\tt.is(result.name, 'a');\n\tt.true(result.option);\n\tt.false(result.ctrl);\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - super modifier', t => {\n\t// 'a' with super (modifier 9 = super(8) + 1)\n\tconst result = parseKeypress(kittyKey(97, 9));\n\tt.is(result.name, 'a');\n\tt.true(result.super);\n\tt.false(result.ctrl);\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - hyper modifier', t => {\n\t// 'a' with hyper (modifier 17 = hyper(16) + 1)\n\tconst result = parseKeypress(kittyKey(97, 17));\n\tt.is(result.name, 'a');\n\tt.true(result.hyper);\n\tt.false(result.super);\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - meta modifier', t => {\n\t// 'a' with meta (modifier 33 = meta(32) + 1)\n\tconst result = parseKeypress(kittyKey(97, 33));\n\tt.is(result.name, 'a');\n\tt.true(result.meta);\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - caps lock', t => {\n\t// 'a' with capsLock (modifier 65 = capsLock(64) + 1)\n\tconst result = parseKeypress(kittyKey(97, 65));\n\tt.is(result.name, 'a');\n\tt.true(result.capsLock);\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - num lock', t => {\n\t// 'a' with numLock (modifier 129 = numLock(128) + 1)\n\tconst result = parseKeypress(kittyKey(97, 129));\n\tt.is(result.name, 'a');\n\tt.true(result.numLock);\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - combined modifiers (ctrl+shift)', t => {\n\t// 'a' with ctrl+shift (modifier 6 = ctrl(4) + shift(1) + 1)\n\tconst result = parseKeypress(kittyKey(97, 6));\n\tt.is(result.name, 'a');\n\tt.true(result.ctrl);\n\tt.true(result.shift);\n\tt.false(result.meta);\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - combined modifiers (super+ctrl)', t => {\n\t// 's' with super+ctrl (modifier 13 = super(8) + ctrl(4) + 1)\n\tconst result = parseKeypress(kittyKey(115, 13));\n\tt.is(result.name, 's');\n\tt.true(result.super);\n\tt.true(result.ctrl);\n\tt.false(result.shift);\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - escape key', t => {\n\t// Escape key\n\tconst result = parseKeypress(kittyKey(27));\n\tt.is(result.name, 'escape');\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - return/enter key', t => {\n\t// Return/enter key\n\tconst result = parseKeypress(kittyKey(13));\n\tt.is(result.name, 'return');\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - tab key', t => {\n\t// Tab key\n\tconst result = parseKeypress(kittyKey(9));\n\tt.is(result.name, 'tab');\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - backspace key', t => {\n\t// Backspace key\n\tconst result = parseKeypress(kittyKey(8));\n\tt.is(result.name, 'backspace');\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - delete key', t => {\n\t// Delete key\n\tconst result = parseKeypress(kittyKey(127));\n\tt.is(result.name, 'delete');\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - space key', t => {\n\t// Space key\n\tconst result = parseKeypress(kittyKey(32));\n\tt.is(result.name, 'space');\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - event type press', t => {\n\t// 'a' press event\n\tconst result = parseKeypress(kittyKey(97, 1, 1));\n\tt.is(result.name, 'a');\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - event type repeat', t => {\n\t// 'a' repeat event\n\tconst result = parseKeypress(kittyKey(97, 1, 2));\n\tt.is(result.name, 'a');\n\tt.is(result.eventType, 'repeat');\n});\n\ntest('kitty protocol - event type release', t => {\n\t// 'a' release event\n\tconst result = parseKeypress(kittyKey(97, 1, 3));\n\tt.is(result.name, 'a');\n\tt.is(result.eventType, 'release');\n});\n\ntest('kitty protocol - number keys', t => {\n\t// '1' key\n\tconst result = parseKeypress(kittyKey(49));\n\tt.is(result.name, '1');\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - special character', t => {\n\t// '@' key\n\tconst result = parseKeypress(kittyKey(64));\n\tt.is(result.name, '@');\n\tt.is(result.eventType, 'press');\n});\n\ntest('kitty protocol - ctrl+letter produces codepoint 1-26', t => {\n\t// When using ctrl+a, kitty sends codepoint 1 (not 97)\n\t// Ctrl+a (codepoint 1, modifier 5 = ctrl + 1)\n\tconst result = parseKeypress(kittyKey(1, 5));\n\tt.is(result.name, 'a');\n\tt.true(result.ctrl);\n});\n\ntest('kitty protocol - preserves sequence and raw', t => {\n\tconst seq = kittyKey(97, 5);\n\tconst result = parseKeypress(seq);\n\tt.is(result.sequence, seq);\n\tt.is(result.raw, seq);\n});\n\ntest('kitty protocol - text-as-codepoints field', t => {\n\t// 'a' key with text-as-codepoints containing 'A' (shifted)\n\tconst result = parseKeypress(kittyKey(97, 2, 1, [65]));\n\tt.is(result.name, 'a');\n\tt.is(result.text, 'A');\n\tt.true(result.shift);\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - text-as-codepoints with multiple codepoints', t => {\n\t// Key with text containing multiple codepoints (e.g., composed character)\n\tconst result = parseKeypress(kittyKey(97, 1, 1, [72, 101]));\n\tt.is(result.text, 'He');\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - supplementary unicode codepoint', t => {\n\t// Emoji: 😀 (U+1F600 = 128512)\n\tconst result = parseKeypress(kittyKey(128_512));\n\tt.is(result.name, '😀');\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - text-as-codepoints with supplementary unicode', t => {\n\t// Text field with emoji codepoint\n\tconst result = parseKeypress(kittyKey(97, 1, 1, [128_512]));\n\tt.is(result.text, '😀');\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - text defaults to character from codepoint', t => {\n\tconst result = parseKeypress(kittyKey(97));\n\tt.is(result.text, 'a');\n\tt.true(result.isKittyProtocol);\n});\n\n// --- Kitty-enhanced special key tests ---\n\ntest('kitty protocol - arrow keys with event type', t => {\n\t// Up arrow press: CSI 1;1:1 A\n\tconst up = parseKeypress('\\u001B[1;1:1A');\n\tt.is(up.name, 'up');\n\tt.is(up.eventType, 'press');\n\tt.true(up.isKittyProtocol);\n\n\t// Down arrow release: CSI 1;1:3 B\n\tconst down = parseKeypress('\\u001B[1;1:3B');\n\tt.is(down.name, 'down');\n\tt.is(down.eventType, 'release');\n\tt.true(down.isKittyProtocol);\n\n\t// Right arrow repeat: CSI 1;1:2 C\n\tconst right = parseKeypress('\\u001B[1;1:2C');\n\tt.is(right.name, 'right');\n\tt.is(right.eventType, 'repeat');\n\tt.true(right.isKittyProtocol);\n\n\t// Left arrow: CSI 1;1:1 D\n\tconst left = parseKeypress('\\u001B[1;1:1D');\n\tt.is(left.name, 'left');\n\tt.is(left.eventType, 'press');\n\tt.true(left.isKittyProtocol);\n});\n\ntest('kitty protocol - arrow keys with modifiers', t => {\n\t// Ctrl+up: CSI 1;5:1 A (modifiers=5 means ctrl(4)+1)\n\tconst result = parseKeypress('\\u001B[1;5:1A');\n\tt.is(result.name, 'up');\n\tt.true(result.ctrl);\n\tt.is(result.eventType, 'press');\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - home and end keys', t => {\n\tconst home = parseKeypress('\\u001B[1;1:1H');\n\tt.is(home.name, 'home');\n\tt.is(home.eventType, 'press');\n\tt.true(home.isKittyProtocol);\n\n\tconst end = parseKeypress('\\u001B[1;1:1F');\n\tt.is(end.name, 'end');\n\tt.is(end.eventType, 'press');\n\tt.true(end.isKittyProtocol);\n});\n\ntest('kitty protocol - tilde-terminated special keys', t => {\n\t// Delete: CSI 3;1:1 ~\n\tconst del = parseKeypress('\\u001B[3;1:1~');\n\tt.is(del.name, 'delete');\n\tt.is(del.eventType, 'press');\n\tt.true(del.isKittyProtocol);\n\n\t// Insert: CSI 2;1:1 ~\n\tconst ins = parseKeypress('\\u001B[2;1:1~');\n\tt.is(ins.name, 'insert');\n\tt.true(ins.isKittyProtocol);\n\n\t// Page up: CSI 5;1:1 ~\n\tconst pgup = parseKeypress('\\u001B[5;1:1~');\n\tt.is(pgup.name, 'pageup');\n\tt.true(pgup.isKittyProtocol);\n\n\t// F5: CSI 15;1:1 ~\n\tconst f5 = parseKeypress('\\u001B[15;1:1~');\n\tt.is(f5.name, 'f5');\n\tt.true(f5.isKittyProtocol);\n});\n\ntest('kitty protocol - tilde keys with modifiers', t => {\n\t// Shift+Delete: CSI 3;2:1 ~ (modifiers=2 means shift(1)+1)\n\tconst result = parseKeypress('\\u001B[3;2:1~');\n\tt.is(result.name, 'delete');\n\tt.true(result.shift);\n\tt.is(result.eventType, 'press');\n\tt.true(result.isKittyProtocol);\n});\n\n// --- Malformed input handling ---\n\ntest('kitty protocol - invalid codepoint above U+10FFFF returns safe empty keypress', t => {\n\t// Codepoint 1114112 = 0x110000, one above max Unicode\n\tconst result = parseKeypress('\\u001B[1114112u');\n\tt.is(result.name, '');\n\tt.false(result.ctrl);\n\tt.true(result.isKittyProtocol);\n\tt.false(result.isPrintable);\n});\n\ntest('kitty protocol - surrogate codepoint returns safe empty keypress', t => {\n\t// Codepoint 0xD800 is a surrogate\n\tconst result = parseKeypress('\\u001B[55296u');\n\tt.is(result.name, '');\n\tt.false(result.ctrl);\n\tt.true(result.isKittyProtocol);\n\tt.false(result.isPrintable);\n});\n\ntest('kitty protocol - invalid text codepoint replaced with fallback', t => {\n\t// Valid primary codepoint, but text field has an invalid codepoint\n\tconst result = parseKeypress(kittyKey(97, 1, 1, [1_114_112]));\n\tt.is(result.name, 'a');\n\tt.is(result.text, '?');\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - malformed modifier 0 does not set all flags', t => {\n\t// Malformed sequence with modifier 0 (should clamp to 0, not become -1)\n\tconst result = parseKeypress('\\u001B[97;0u');\n\tt.is(result.name, 'a');\n\tt.false(result.ctrl);\n\tt.false(result.shift);\n\tt.false(result.option);\n\tt.false(result.super ?? false);\n\tt.true(result.isKittyProtocol);\n});\n\n// --- Legacy fallback ---\n\ntest('non-kitty sequences fall back to legacy parsing', t => {\n\t// Regular escape sequence (not kitty protocol)\n\t// Up arrow key\n\tconst result = parseKeypress('\\u001B[A');\n\tt.is(result.name, 'up');\n\tt.is(result.isKittyProtocol, undefined);\n});\n\ntest('non-kitty sequences - ctrl+c', t => {\n\t// Ctrl+c\n\tconst result = parseKeypress('\\u0003');\n\tt.is(result.name, 'c');\n\tt.true(result.ctrl);\n\tt.is(result.isKittyProtocol, undefined);\n});\n\n// --- isPrintable field tests ---\n\ntest('kitty protocol - isPrintable is true for regular characters', t => {\n\t// 'a' key\n\tconst result = parseKeypress(kittyKey(97));\n\tt.true(result.isPrintable);\n});\n\ntest('kitty protocol - isPrintable is true for digits', t => {\n\t// '1' key\n\tconst result = parseKeypress(kittyKey(49));\n\tt.true(result.isPrintable);\n});\n\ntest('kitty protocol - isPrintable is true for symbols', t => {\n\t// '@' key\n\tconst result = parseKeypress(kittyKey(64));\n\tt.true(result.isPrintable);\n});\n\ntest('kitty protocol - isPrintable is true for emoji', t => {\n\tconst result = parseKeypress(kittyKey(128_512));\n\tt.true(result.isPrintable);\n});\n\ntest('kitty protocol - isPrintable is false for escape', t => {\n\tconst result = parseKeypress(kittyKey(27));\n\tt.false(result.isPrintable);\n});\n\ntest('kitty protocol - isPrintable is true for return', t => {\n\tconst result = parseKeypress(kittyKey(13));\n\tt.true(result.isPrintable);\n});\n\ntest('kitty protocol - isPrintable is false for tab', t => {\n\tconst result = parseKeypress(kittyKey(9));\n\tt.false(result.isPrintable);\n});\n\ntest('kitty protocol - isPrintable is true for space', t => {\n\tconst result = parseKeypress(kittyKey(32));\n\tt.true(result.isPrintable);\n});\n\ntest('kitty protocol - isPrintable is false for backspace', t => {\n\tconst result = parseKeypress(kittyKey(8));\n\tt.false(result.isPrintable);\n});\n\ntest('kitty protocol - isPrintable is false for ctrl+letter', t => {\n\t// Ctrl+a (codepoint 1)\n\tconst result = parseKeypress(kittyKey(1, 5));\n\tt.false(result.isPrintable);\n});\n\ntest('kitty protocol - isPrintable is false for special keys (arrows)', t => {\n\t// Up arrow via kitty enhanced special key format\n\tconst result = parseKeypress('\\u001B[1;1:1A');\n\tt.false(result.isPrintable);\n});\n\n// --- Non-printable key suppression tests (feedback #3 repros) ---\n\ntest('kitty protocol - capslock (57358) is non-printable', t => {\n\t// \\x1b[57358u -> capslock should have isPrintable=false\n\tconst result = parseKeypress('\\u001B[57358u');\n\tt.is(result.name, 'capslock');\n\tt.false(result.isPrintable);\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - printscreen (57361) is non-printable', t => {\n\t// \\x1b[57361u -> printscreen should have isPrintable=false\n\tconst result = parseKeypress('\\u001B[57361u');\n\tt.is(result.name, 'printscreen');\n\tt.false(result.isPrintable);\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - f13 (57376) is non-printable', t => {\n\t// \\x1b[57376u -> f13 should have isPrintable=false\n\tconst result = parseKeypress('\\u001B[57376u');\n\tt.is(result.name, 'f13');\n\tt.false(result.isPrintable);\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - media key (57428 mediaplay) is non-printable', t => {\n\tconst result = parseKeypress('\\u001B[57428u');\n\tt.is(result.name, 'mediaplay');\n\tt.false(result.isPrintable);\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - modifier-only key (57441 leftshift) is non-printable', t => {\n\tconst result = parseKeypress('\\u001B[57441u');\n\tt.is(result.name, 'leftshift');\n\tt.false(result.isPrintable);\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - modifier-only key (57442 leftcontrol) is non-printable', t => {\n\tconst result = parseKeypress('\\u001B[57442u');\n\tt.is(result.name, 'leftcontrol');\n\tt.false(result.isPrintable);\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - kp keys (57399 kp0) are non-printable', t => {\n\tconst result = parseKeypress('\\u001B[57399u');\n\tt.is(result.name, 'kp0');\n\tt.false(result.isPrintable);\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - scrolllock (57359) is non-printable', t => {\n\tconst result = parseKeypress('\\u001B[57359u');\n\tt.is(result.name, 'scrolllock');\n\tt.false(result.isPrintable);\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - numlock (57360) is non-printable', t => {\n\tconst result = parseKeypress('\\u001B[57360u');\n\tt.is(result.name, 'numlock');\n\tt.false(result.isPrintable);\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - pause (57362) is non-printable', t => {\n\tconst result = parseKeypress('\\u001B[57362u');\n\tt.is(result.name, 'pause');\n\tt.false(result.isPrintable);\n\tt.true(result.isKittyProtocol);\n});\n\ntest('kitty protocol - volume keys are non-printable', t => {\n\t// Lower volume (57438)\n\tconst lower = parseKeypress('\\u001B[57438u');\n\tt.is(lower.name, 'lowervolume');\n\tt.false(lower.isPrintable);\n\n\t// Raise volume (57439)\n\tconst raise = parseKeypress('\\u001B[57439u');\n\tt.is(raise.name, 'raisevolume');\n\tt.false(raise.isPrintable);\n\n\t// Mute volume (57440)\n\tconst mute = parseKeypress('\\u001B[57440u');\n\tt.is(mute.name, 'mutevolume');\n\tt.false(mute.isPrintable);\n});\n\n// --- Init/cleanup control sequence tests ---\n\nconst createFakeStdout = () => {\n\tconst stdout = new EventEmitter() as unknown as NodeJS.WriteStream;\n\tstdout.columns = 100;\n\tstdout.isTTY = true;\n\tconst write = spy();\n\tstdout.write = write;\n\treturn {stdout, write};\n};\n\nconst createFakeStdin = () => {\n\tconst stdin = new EventEmitter() as unknown as NodeJS.ReadStream;\n\tstdin.isTTY = true;\n\tstdin.setRawMode = stub();\n\tstdin.setEncoding = () => {};\n\tstdin.read = stub();\n\treturn stdin;\n};\n\nconst getWrittenStrings = (write: ReturnType<typeof spy>): string[] =>\n\t(write.args as string[][]).map(args => args[0]!);\n\ntest.serial(\n\t'kitty protocol - writes enable sequence on init when mode is enabled',\n\tt => {\n\t\tconst {stdout, write} = createFakeStdout();\n\t\tconst stdin = createFakeStdin();\n\n\t\tconst {unmount} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t\tstdin,\n\t\t\tkittyKeyboard: {mode: 'enabled'},\n\t\t});\n\n\t\t// CSI > 1 u (push keyboard mode with disambiguateEscapeCodes flag)\n\t\tt.true(getWrittenStrings(write).includes('\\u001B[>1u'));\n\n\t\tunmount();\n\t},\n);\n\ntest.serial('kitty protocol - writes disable sequence on unmount', t => {\n\tconst {stdout, write} = createFakeStdout();\n\tconst stdin = createFakeStdin();\n\n\tconst {unmount} = render(<Text>Hello</Text>, {\n\t\tstdout,\n\t\tstdin,\n\t\tkittyKeyboard: {mode: 'enabled'},\n\t});\n\n\tunmount();\n\n\t// CSI < u (pop keyboard mode)\n\tt.true(getWrittenStrings(write).includes('\\u001B[<u'));\n});\n\ntest.serial('kitty protocol - not enabled when stdin is not a TTY', t => {\n\tconst {stdout, write} = createFakeStdout();\n\tconst stdin = createFakeStdin();\n\tstdin.isTTY = false;\n\n\tconst {unmount} = render(<Text>Hello</Text>, {\n\t\tstdout,\n\t\tstdin,\n\t\tkittyKeyboard: {mode: 'enabled'},\n\t});\n\n\tt.false(getWrittenStrings(write).includes('\\u001B[>1u'));\n\n\tunmount();\n});\n\ntest.serial('kitty protocol - not enabled when stdout is not a TTY', t => {\n\tconst {stdout, write} = createFakeStdout();\n\tstdout.isTTY = false;\n\tconst stdin = createFakeStdin();\n\n\tconst {unmount} = render(<Text>Hello</Text>, {\n\t\tstdout,\n\t\tstdin,\n\t\tkittyKeyboard: {mode: 'enabled'},\n\t});\n\n\tt.false(getWrittenStrings(write).includes('\\u001B[>1u'));\n\n\tunmount();\n});\n\n// --- Auto-detection race condition tests ---\n\ntest.serial(\n\t'kitty protocol - auto detection does not enable protocol after unmount',\n\tt => {\n\t\tconst {stdout, write} = createFakeStdout();\n\t\tconst stdin = createFakeStdin();\n\n\t\tconst origKittyId = process.env['KITTY_WINDOW_ID'];\n\t\tprocess.env['KITTY_WINDOW_ID'] = '1';\n\t\tt.teardown(() => {\n\t\t\tif (origKittyId === undefined) {\n\t\t\t\tdelete process.env['KITTY_WINDOW_ID'];\n\t\t\t} else {\n\t\t\t\tprocess.env['KITTY_WINDOW_ID'] = origKittyId;\n\t\t\t}\n\t\t});\n\n\t\tconst {unmount} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t\tstdin,\n\t\t\tkittyKeyboard: {mode: 'auto'},\n\t\t});\n\n\t\t// Unmount before the terminal responds\n\t\tunmount();\n\n\t\t// Simulate a late terminal response arriving after unmount\n\t\tstdin.emit('data', '\\u001B[?1u');\n\n\t\t// The enable sequence should NOT have been written after unmount\n\t\tconst strings = getWrittenStrings(write);\n\t\tconst enableCount = strings.filter(s => s === '\\u001B[>1u').length;\n\t\tt.is(enableCount, 0);\n\t},\n);\n\ntest.serial(\n\t'kitty protocol - auto detection handles synchronous query response',\n\tt => {\n\t\tconst {stdout} = createFakeStdout();\n\t\tconst stdin = createFakeStdin();\n\t\tconst writtenStrings: string[] = [];\n\n\t\t// Override stdout.write to synchronously emit the response on stdin\n\t\t// when the query sequence is written, simulating a fast terminal\n\t\tstdout.write = ((data: string) => {\n\t\t\twrittenStrings.push(data);\n\t\t\tif (data === '\\u001B[?u') {\n\t\t\t\tstdin.emit('data', '\\u001B[?1u');\n\t\t\t}\n\n\t\t\treturn true;\n\t\t}) as typeof stdout.write;\n\n\t\tconst origKittyId = process.env['KITTY_WINDOW_ID'];\n\t\tprocess.env['KITTY_WINDOW_ID'] = '1';\n\t\tt.teardown(() => {\n\t\t\tif (origKittyId === undefined) {\n\t\t\t\tdelete process.env['KITTY_WINDOW_ID'];\n\t\t\t} else {\n\t\t\t\tprocess.env['KITTY_WINDOW_ID'] = origKittyId;\n\t\t\t}\n\t\t});\n\n\t\tconst {unmount} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t\tstdin,\n\t\t\tkittyKeyboard: {mode: 'auto'},\n\t\t});\n\n\t\t// The enable sequence should have been written\n\t\tt.true(writtenStrings.includes('\\u001B[>1u'));\n\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'kitty protocol - auto detection handles Uint8Array query response',\n\tt => {\n\t\tconst {stdout, write} = createFakeStdout();\n\t\tconst stdin = createFakeStdin();\n\n\t\tconst origKittyId = process.env['KITTY_WINDOW_ID'];\n\t\tprocess.env['KITTY_WINDOW_ID'] = '1';\n\t\tt.teardown(() => {\n\t\t\tif (origKittyId === undefined) {\n\t\t\t\tdelete process.env['KITTY_WINDOW_ID'];\n\t\t\t} else {\n\t\t\t\tprocess.env['KITTY_WINDOW_ID'] = origKittyId;\n\t\t\t}\n\t\t});\n\n\t\tconst {unmount} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t\tstdin,\n\t\t\tkittyKeyboard: {mode: 'auto'},\n\t\t});\n\n\t\t// Respond with Uint8Array instead of string\n\t\tconst response = Buffer.from('\\u001B[?1u');\n\t\tstdin.emit('data', new Uint8Array(response));\n\n\t\t// The enable sequence should have been written\n\t\tconst strings = getWrittenStrings(write);\n\t\tt.true(strings.includes('\\u001B[>1u'));\n\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'kitty protocol - auto detection preserves split UTF-8 input bytes',\n\tasync t => {\n\t\tconst {stdout} = createFakeStdout();\n\t\tconst stdin = createFakeStdin();\n\t\tconst unshifted: Uint8Array[] = [];\n\n\t\tconst concatUint8Arrays = (chunks: Uint8Array[]): number[] => {\n\t\t\tconst merged: number[] = [];\n\t\t\tfor (const chunk of chunks) {\n\t\t\t\tfor (const byte of chunk) {\n\t\t\t\t\tmerged.push(byte);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn merged;\n\t\t};\n\n\t\tstdin.unshift = ((chunk: Uint8Array) => {\n\t\t\tunshifted.push(Uint8Array.from(chunk));\n\t\t\treturn true;\n\t\t}) as typeof stdin.unshift;\n\n\t\tconst origKittyId = process.env['KITTY_WINDOW_ID'];\n\t\tprocess.env['KITTY_WINDOW_ID'] = '1';\n\t\tt.teardown(() => {\n\t\t\tif (origKittyId === undefined) {\n\t\t\t\tdelete process.env['KITTY_WINDOW_ID'];\n\t\t\t} else {\n\t\t\t\tprocess.env['KITTY_WINDOW_ID'] = origKittyId;\n\t\t\t}\n\t\t});\n\n\t\tconst {unmount} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t\tstdin,\n\t\t\tkittyKeyboard: {mode: 'auto'},\n\t\t});\n\n\t\t// Emit one UTF-8 emoji split across chunks during detection.\n\t\tstdin.emit('data', new Uint8Array([0xf0, 0x9f]));\n\t\tstdin.emit('data', new Uint8Array([0x92, 0xa9]));\n\n\t\tawait new Promise(resolve => {\n\t\t\tsetTimeout(resolve, 250);\n\t\t});\n\n\t\tt.deepEqual(concatUint8Arrays(unshifted), [0xf0, 0x9f, 0x92, 0xa9]);\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'kitty protocol - auto detection timeout does not leak partial query response',\n\tasync t => {\n\t\tconst {stdout} = createFakeStdout();\n\t\tconst stdin = createFakeStdin();\n\t\tconst unshifted: Uint8Array[] = [];\n\n\t\tstdin.unshift = ((chunk: Uint8Array) => {\n\t\t\tunshifted.push(Uint8Array.from(chunk));\n\t\t\treturn true;\n\t\t}) as typeof stdin.unshift;\n\n\t\tconst origKittyId = process.env['KITTY_WINDOW_ID'];\n\t\tprocess.env['KITTY_WINDOW_ID'] = '1';\n\t\tt.teardown(() => {\n\t\t\tif (origKittyId === undefined) {\n\t\t\t\tdelete process.env['KITTY_WINDOW_ID'];\n\t\t\t} else {\n\t\t\t\tprocess.env['KITTY_WINDOW_ID'] = origKittyId;\n\t\t\t}\n\t\t});\n\n\t\tconst {unmount} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t\tstdin,\n\t\t\tkittyKeyboard: {mode: 'auto'},\n\t\t});\n\n\t\t// Simulate partial terminal response that times out before completion.\n\t\tstdin.emit('data', '\\u001B[?1');\n\n\t\tawait new Promise(resolve => {\n\t\t\tsetTimeout(resolve, 250);\n\t\t});\n\n\t\tt.is(unshifted.length, 0);\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'kitty protocol - auto detection timeout preserves query prefix without digits',\n\tasync t => {\n\t\tconst {stdout, write} = createFakeStdout();\n\t\tconst stdin = createFakeStdin();\n\t\tconst unshifted: Uint8Array[] = [];\n\n\t\tstdin.unshift = ((chunk: Uint8Array) => {\n\t\t\tunshifted.push(Uint8Array.from(chunk));\n\t\t\treturn true;\n\t\t}) as typeof stdin.unshift;\n\n\t\tconst origKittyId = process.env['KITTY_WINDOW_ID'];\n\t\tprocess.env['KITTY_WINDOW_ID'] = '1';\n\t\tt.teardown(() => {\n\t\t\tif (origKittyId === undefined) {\n\t\t\t\tdelete process.env['KITTY_WINDOW_ID'];\n\t\t\t} else {\n\t\t\t\tprocess.env['KITTY_WINDOW_ID'] = origKittyId;\n\t\t\t}\n\t\t});\n\n\t\tconst {unmount} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t\tstdin,\n\t\t\tkittyKeyboard: {mode: 'auto'},\n\t\t});\n\n\t\tstdin.emit('data', '\\u001B[?');\n\n\t\tawait new Promise(resolve => {\n\t\t\tsetTimeout(resolve, 250);\n\t\t});\n\n\t\tconst strings = getWrittenStrings(write);\n\t\tconst enableCount = strings.filter(s => s === '\\u001B[>1u').length;\n\t\tt.is(enableCount, 0);\n\t\tt.deepEqual(\n\t\t\tunshifted.map(chunk => [...chunk]),\n\t\t\t[[0x1b, 0x5b, 0x3f]],\n\t\t);\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'kitty protocol - auto detection ignores query response without digits',\n\tasync t => {\n\t\tconst {stdout, write} = createFakeStdout();\n\t\tconst stdin = createFakeStdin();\n\t\tconst unshifted: Uint8Array[] = [];\n\n\t\tstdin.unshift = ((chunk: Uint8Array) => {\n\t\t\tunshifted.push(Uint8Array.from(chunk));\n\t\t\treturn true;\n\t\t}) as typeof stdin.unshift;\n\n\t\tconst origKittyId = process.env['KITTY_WINDOW_ID'];\n\t\tprocess.env['KITTY_WINDOW_ID'] = '1';\n\t\tt.teardown(() => {\n\t\t\tif (origKittyId === undefined) {\n\t\t\t\tdelete process.env['KITTY_WINDOW_ID'];\n\t\t\t} else {\n\t\t\t\tprocess.env['KITTY_WINDOW_ID'] = origKittyId;\n\t\t\t}\n\t\t});\n\n\t\tconst {unmount} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t\tstdin,\n\t\t\tkittyKeyboard: {mode: 'auto'},\n\t\t});\n\n\t\tstdin.emit('data', '\\u001B[?u');\n\n\t\tawait new Promise(resolve => {\n\t\t\tsetTimeout(resolve, 250);\n\t\t});\n\n\t\tconst strings = getWrittenStrings(write);\n\t\tconst enableCount = strings.filter(s => s === '\\u001B[>1u').length;\n\t\tt.is(enableCount, 0);\n\t\tt.deepEqual(\n\t\t\tunshifted.map(chunk => [...chunk]),\n\t\t\t[[0x1b, 0x5b, 0x3f, 0x75]],\n\t\t);\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'kitty protocol - auto detection preserves invalid query-like escape sequence',\n\tasync t => {\n\t\tconst {stdout, write} = createFakeStdout();\n\t\tconst stdin = createFakeStdin();\n\t\tconst unshifted: Uint8Array[] = [];\n\n\t\tstdin.unshift = ((chunk: Uint8Array) => {\n\t\t\tunshifted.push(Uint8Array.from(chunk));\n\t\t\treturn true;\n\t\t}) as typeof stdin.unshift;\n\n\t\tconst origKittyId = process.env['KITTY_WINDOW_ID'];\n\t\tprocess.env['KITTY_WINDOW_ID'] = '1';\n\t\tt.teardown(() => {\n\t\t\tif (origKittyId === undefined) {\n\t\t\t\tdelete process.env['KITTY_WINDOW_ID'];\n\t\t\t} else {\n\t\t\t\tprocess.env['KITTY_WINDOW_ID'] = origKittyId;\n\t\t\t}\n\t\t});\n\n\t\tconst {unmount} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t\tstdin,\n\t\t\tkittyKeyboard: {mode: 'auto'},\n\t\t});\n\n\t\tstdin.emit('data', '\\u001B[?1x');\n\n\t\tawait new Promise(resolve => {\n\t\t\tsetTimeout(resolve, 250);\n\t\t});\n\n\t\tconst strings = getWrittenStrings(write);\n\t\tconst enableCount = strings.filter(s => s === '\\u001B[>1u').length;\n\t\tt.is(enableCount, 0);\n\t\tt.deepEqual(\n\t\t\tunshifted.map(chunk => [...chunk]),\n\t\t\t[[0x1b, 0x5b, 0x3f, 0x31, 0x78]],\n\t\t);\n\t\tunmount();\n\t},\n);\n\n// --- Space and return text input tests ---\n\ntest('kitty protocol - space key has text field set to space character', t => {\n\tconst result = parseKeypress(kittyKey(32));\n\tt.is(result.text, ' ');\n});\n\ntest('kitty protocol - return key has text field set to carriage return', t => {\n\tconst result = parseKeypress(kittyKey(13));\n\tt.is(result.text, '\\r');\n});\n"
  },
  {
    "path": "test/log-update.tsx",
    "content": "import test from 'ava';\nimport ansiEscapes from 'ansi-escapes';\nimport logUpdate from '../src/log-update.js';\nimport createStdout from './helpers/create-stdout.js';\n\ntest('standard rendering - renders and updates output', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {showCursor: true});\n\n\trender('Hello\\n');\n\tt.is((stdout.write as any).callCount, 1);\n\tt.is((stdout.write as any).firstCall.args[0], 'Hello\\n');\n\n\trender('World\\n');\n\tt.is((stdout.write as any).callCount, 2);\n\tt.true(\n\t\t((stdout.write as any).secondCall.args[0] as string).includes('World'),\n\t);\n});\n\ntest('standard rendering - skips identical output', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {showCursor: true});\n\n\trender('Hello\\n');\n\trender('Hello\\n');\n\n\tt.is((stdout.write as any).callCount, 1);\n});\n\ntest('incremental rendering - renders and updates output', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('Hello\\n');\n\tt.is((stdout.write as any).callCount, 1);\n\tt.is((stdout.write as any).firstCall.args[0], 'Hello\\n');\n\n\trender('World\\n');\n\tt.is((stdout.write as any).callCount, 2);\n\tt.true(\n\t\t((stdout.write as any).secondCall.args[0] as string).includes('World'),\n\t);\n});\n\ntest('incremental rendering - skips identical output', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('Hello\\n');\n\trender('Hello\\n');\n\n\tt.is((stdout.write as any).callCount, 1);\n});\n\ntest('incremental rendering - surgical updates', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\trender('Line 1\\nUpdated\\nLine 3\\n');\n\n\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\tt.true(secondCall.includes(ansiEscapes.cursorNextLine)); // Skips unchanged lines\n\tt.true(secondCall.includes('Updated')); // Only updates changed line\n\tt.false(secondCall.includes('Line 1')); // Doesn't rewrite unchanged\n\tt.false(secondCall.includes('Line 3')); // Doesn't rewrite unchanged\n});\n\ntest('incremental rendering - clears extra lines when output shrinks', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\trender('Line 1\\n');\n\n\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\tt.true(secondCall.includes(ansiEscapes.eraseLines(2))); // Erases 2 extra lines\n});\n\ntest('incremental rendering - when output grows', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('Line 1\\n');\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\n\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\tt.true(secondCall.includes(ansiEscapes.cursorNextLine)); // Skips unchanged first line\n\tt.true(secondCall.includes('Line 2')); // Adds new line\n\tt.true(secondCall.includes('Line 3')); // Adds new line\n\tt.false(secondCall.includes('Line 1')); // Doesn't rewrite unchanged\n});\n\ntest('incremental rendering - single write call with multiple surgical updates', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender(\n\t\t'Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\\nLine 10\\n',\n\t);\n\trender(\n\t\t'Line 1\\nUpdated 2\\nLine 3\\nUpdated 4\\nLine 5\\nUpdated 6\\nLine 7\\nUpdated 8\\nLine 9\\nUpdated 10\\n',\n\t);\n\n\tt.is((stdout.write as any).callCount, 2); // Only 2 writes total (initial + update)\n});\n\ntest('incremental rendering - shrinking output keeps screen tight', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\trender('Line 1\\nLine 2\\n');\n\trender('Line 1\\n');\n\n\tconst thirdCall = stdout.get();\n\n\tt.is(\n\t\tthirdCall,\n\t\tansiEscapes.eraseLines(2) + // Erase Line 2 and ending cursorNextLine\n\t\t\tansiEscapes.cursorUp(1) + // Move to beginning of Line 1\n\t\t\tansiEscapes.cursorNextLine, // Move to next line after Line 1\n\t);\n});\n\ntest('incremental rendering - clear() fully resets incremental state', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\trender.clear();\n\trender('Line 1\\n');\n\n\tconst afterClear = stdout.get();\n\n\tt.is(afterClear, ansiEscapes.eraseLines(0) + 'Line 1\\n'); // Should do a fresh write\n});\n\ntest('incremental rendering - done() resets before next render', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\trender.done();\n\trender('Line 1\\n');\n\n\tconst afterDone = stdout.get();\n\n\tt.is(afterDone, ansiEscapes.eraseLines(0) + 'Line 1\\n'); // Should do a fresh write\n});\n\ntest('incremental rendering - multiple consecutive clear() calls (should be harmless no-ops)', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\trender.clear();\n\trender.clear();\n\trender.clear();\n\n\tt.is((stdout.write as any).callCount, 4); // Initial render + 3 clears (each writes eraseLines)\n\n\t// Verify state is properly reset after multiple clears\n\trender('New content\\n');\n\tconst afterClears = stdout.get();\n\tt.is(afterClears, ansiEscapes.eraseLines(0) + 'New content\\n'); // Should do a fresh write\n});\n\ntest('incremental rendering - sync() followed by update (assert incremental path is used)', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender.sync('Line 1\\nLine 2\\nLine 3\\n');\n\tt.is((stdout.write as any).callCount, 0); // The sync() call shouldn't write to stdout\n\n\trender('Line 1\\nUpdated\\nLine 3\\n');\n\tt.is((stdout.write as any).callCount, 1);\n\n\tconst firstCall = (stdout.write as any).firstCall.args[0] as string;\n\tt.true(firstCall.includes(ansiEscapes.cursorNextLine)); // Skips unchanged lines\n\tt.true(firstCall.includes('Updated')); // Only updates changed line\n\tt.false(firstCall.includes('Line 1')); // Doesn't rewrite unchanged\n\tt.false(firstCall.includes('Line 3')); // Doesn't rewrite unchanged\n});\n\n// Cursor positioning tests\n\nconst showCursorEscape = '\\u001B[?25h';\nconst hideCursorEscape = '\\u001B[?25l';\n\nconst renderingModes = [\n\t{name: 'standard rendering', incremental: false},\n\t{name: 'incremental rendering', incremental: true},\n] as const;\n\nconst createRenderForMode = (incremental: boolean) => {\n\tconst stdout = createStdout();\n\tconst render = incremental\n\t\t? logUpdate.create(stdout, {showCursor: true, incremental: true})\n\t\t: logUpdate.create(stdout, {showCursor: true});\n\treturn {stdout, render};\n};\n\ntest('standard rendering - positions cursor after output when cursorPosition is set', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {showCursor: true});\n\n\trender.setCursorPosition({x: 5, y: 1});\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\n\tconst written = (stdout.write as any).firstCall.args[0] as string;\n\t// Output is \"Line 1\\nLine 2\\nLine 3\\n\" (3 visible lines)\n\t// Cursor after write is at line 3 (0-indexed), col 0\n\t// To reach y=1: cursorUp(3 - 1) = cursorUp(2)\n\t// Then cursorTo(5) and show cursor\n\tt.true(written.includes('Line 3'));\n\tt.true(\n\t\twritten.endsWith(\n\t\t\tansiEscapes.cursorUp(2) + ansiEscapes.cursorTo(5) + showCursorEscape,\n\t\t),\n\t);\n});\n\ntest('standard rendering - hides cursor before erase when cursor was previously shown', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {showCursor: true});\n\n\trender.setCursorPosition({x: 0, y: 0});\n\trender('Hello\\n');\n\trender.setCursorPosition({x: 0, y: 0});\n\trender('World\\n');\n\n\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\t// Should start with hide cursor before erasing\n\tt.true(secondCall.startsWith(hideCursorEscape));\n\t// Should end with show cursor at position\n\tt.true(\n\t\tsecondCall.endsWith(\n\t\t\tansiEscapes.cursorUp(1) + ansiEscapes.cursorTo(0) + showCursorEscape,\n\t\t),\n\t);\n});\n\ntest('standard rendering - no cursor positioning when cursorPosition is undefined', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {showCursor: true});\n\n\trender('Hello\\n');\n\n\tconst written = (stdout.write as any).firstCall.args[0] as string;\n\tt.false(written.includes(showCursorEscape));\n});\n\ntest('standard rendering - cursor position at second-to-last line emits cursorUp(1)', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {showCursor: true});\n\n\trender.setCursorPosition({x: 3, y: 2});\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\n\tconst written = (stdout.write as any).firstCall.args[0] as string;\n\t// Output has 3 visible lines. After write, cursor is at line 3 (past last visible).\n\t// To reach y=2: cursorUp(3 - 2) = cursorUp(1)\n\tt.true(\n\t\twritten.endsWith(\n\t\t\tansiEscapes.cursorUp(1) + ansiEscapes.cursorTo(3) + showCursorEscape,\n\t\t),\n\t);\n});\n\nfor (const {name, incremental} of renderingModes) {\n\ttest(`${name} - clear() returns cursor to bottom before erasing`, t => {\n\t\tconst {stdout, render} = createRenderForMode(incremental);\n\n\t\trender.setCursorPosition({x: 5, y: 0});\n\t\trender('Line 1\\nLine 2\\nLine 3\\n');\n\n\t\trender.clear();\n\n\t\tconst clearCall = (stdout.write as any).secondCall.args[0] as string;\n\t\t// Cursor was at y=0, output had 4 lines (3 visible + trailing newline).\n\t\t// clear() should: hide cursor, move down to bottom (from y=0 to line 3), then erase\n\t\tt.true(clearCall.includes(hideCursorEscape));\n\t\tt.true(clearCall.includes(ansiEscapes.cursorDown(3)));\n\t\tt.true(clearCall.includes(ansiEscapes.eraseLines(4)));\n\t});\n}\n\ntest('standard rendering - clearing cursor position stops cursor positioning', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {showCursor: true});\n\n\trender.setCursorPosition({x: 0, y: 0});\n\trender('Hello\\n');\n\n\trender.setCursorPosition(undefined);\n\trender('World\\n');\n\n\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\tt.false(secondCall.includes(showCursorEscape));\n});\n\ntest('incremental rendering - positions cursor after surgical updates', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender.setCursorPosition({x: 5, y: 1});\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\n\tconst written = (stdout.write as any).firstCall.args[0] as string;\n\t// After incremental write, cursor is at line 3 (past last visible)\n\t// To reach y=1: cursorUp(3 - 1) = cursorUp(2)\n\tt.true(\n\t\twritten.endsWith(\n\t\t\tansiEscapes.cursorUp(2) + ansiEscapes.cursorTo(5) + showCursorEscape,\n\t\t),\n\t);\n});\n\ntest('incremental rendering - positions cursor after update', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender.setCursorPosition({x: 2, y: 0});\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\trender.setCursorPosition({x: 2, y: 0});\n\trender('Line 1\\nUpdated\\nLine 3\\n');\n\n\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\t// After incremental update, cursor is at line 3\n\t// To reach y=0: cursorUp(3)\n\tt.true(\n\t\tsecondCall.endsWith(\n\t\t\tansiEscapes.cursorUp(3) + ansiEscapes.cursorTo(2) + showCursorEscape,\n\t\t),\n\t);\n});\n\nfor (const {name, incremental} of renderingModes) {\n\ttest(`${name} - repositions cursor when only cursor position changes (same output)`, t => {\n\t\tconst {stdout, render} = createRenderForMode(incremental);\n\n\t\trender.setCursorPosition({x: 2, y: 0});\n\t\trender('Hello\\n');\n\t\tt.is((stdout.write as any).callCount, 1);\n\n\t\t// Same output, but cursor moved (simulates space input where output is padded identically)\n\t\trender.setCursorPosition({x: 3, y: 0});\n\t\trender('Hello\\n');\n\n\t\tt.is((stdout.write as any).callCount, 2);\n\t\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\t\t// Should reposition cursor: hide + return to bottom + move to new position + show\n\t\tt.true(secondCall.includes(showCursorEscape));\n\t\tt.true(secondCall.endsWith(ansiEscapes.cursorTo(3) + showCursorEscape));\n\t});\n}\n\ntest('standard rendering - returns to bottom before erase when cursor was positioned', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {showCursor: true});\n\n\trender.setCursorPosition({x: 0, y: 0});\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\n\trender.setCursorPosition({x: 5, y: 0});\n\trender('Line A\\nLine B\\nLine C\\n');\n\n\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\t// Should: hide cursor, move down to bottom (from y=0 to line 3), then erase + rewrite\n\tt.true(secondCall.startsWith(hideCursorEscape));\n\tt.true(secondCall.includes(ansiEscapes.cursorDown(3)));\n\tt.true(secondCall.includes('Line A'));\n});\n\nfor (const {name, incremental} of renderingModes) {\n\ttest(`${name} - sync() resets cursor state`, t => {\n\t\tconst {stdout, render} = createRenderForMode(incremental);\n\n\t\trender.setCursorPosition({x: 5, y: 0});\n\t\trender('Line 1\\nLine 2\\nLine 3\\n');\n\n\t\t// Sync() simulates clearTerminal path: screen is fully reset\n\t\trender.sync('Fresh output\\n');\n\n\t\t// Next render should NOT include hideCursor + cursorDown (return-to-bottom prefix)\n\t\t// because sync() should have reset previousCursorPosition and cursorWasShown\n\t\trender('Updated output\\n');\n\n\t\tconst afterSync = stdout.get();\n\t\tt.false(afterSync.includes(hideCursorEscape));\n\t\tt.false(afterSync.includes(ansiEscapes.cursorDown(3)));\n\t});\n}\n\nfor (const {name, incremental} of renderingModes) {\n\ttest(`${name} - sync() writes cursor suffix when cursor is dirty`, t => {\n\t\tconst {stdout, render} = createRenderForMode(incremental);\n\n\t\trender.setCursorPosition({x: 5, y: 1});\n\t\trender.sync('Line 1\\nLine 2\\nLine 3\\n');\n\n\t\t// Sync() should write cursor suffix to position cursor\n\t\t// 3 visible lines, cursor at y=1 → cursorUp(3-1) = cursorUp(2)\n\t\tt.is((stdout.write as any).callCount, 1);\n\t\tconst written = (stdout.write as any).firstCall.args[0] as string;\n\t\tt.is(\n\t\t\twritten,\n\t\t\tansiEscapes.cursorUp(2) + ansiEscapes.cursorTo(5) + showCursorEscape,\n\t\t);\n\t});\n}\n\nfor (const {name, incremental} of renderingModes) {\n\ttest(`${name} - sync() with cursor sets cursorWasShown for next render`, t => {\n\t\tconst {stdout, render} = createRenderForMode(incremental);\n\n\t\trender.setCursorPosition({x: 5, y: 1});\n\t\trender.sync('Line 1\\nLine 2\\nLine 3\\n');\n\n\t\t// Next render should hide cursor before erasing (cursorWasShown = true from sync)\n\t\trender('Updated\\n');\n\n\t\tconst renderCall = stdout.get();\n\t\tt.true(renderCall.startsWith(hideCursorEscape));\n\t});\n}\n\nfor (const {name, incremental} of renderingModes) {\n\ttest(`${name} - sync() hides cursor when previous render showed cursor`, t => {\n\t\tconst {stdout, render} = createRenderForMode(incremental);\n\n\t\trender.setCursorPosition({x: 5, y: 1});\n\t\trender('Line 1\\nLine 2\\nLine 3\\n');\n\t\tt.is((stdout.write as any).callCount, 1);\n\n\t\trender.sync('Fresh output\\n');\n\n\t\tt.is((stdout.write as any).callCount, 2);\n\t\tt.is((stdout.write as any).secondCall.args[0] as string, hideCursorEscape);\n\t});\n}\n\ntest('standard rendering - sync() without cursor does not write to stream', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {showCursor: true});\n\n\trender.sync('Line 1\\nLine 2\\nLine 3\\n');\n\n\tt.is((stdout.write as any).callCount, 0);\n});\n\n// No-trailing-newline tests (fullscreen mode)\n\ntest('incremental rendering - no trailing newline: trailing to no-trailing transition', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('A\\nB\\n');\n\trender('A\\nB');\n\n\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\t// Both lines are unchanged, so only cursor movement should occur.\n\t// The key is that the cursor does NOT overshoot past line B.\n\tt.true(secondCall.includes(ansiEscapes.cursorNextLine)); // Skip unchanged A\n\tt.false(secondCall.endsWith('\\n')); // No trailing newline in output\n});\n\ntest('incremental rendering - no trailing newline: no-trailing to no-trailing update', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('A\\nB');\n\trender('A\\nC');\n\n\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\tt.true(secondCall.includes(ansiEscapes.cursorNextLine)); // Skip unchanged A\n\tt.true(secondCall.includes('C')); // Updates B to C\n\tt.false(secondCall.endsWith('\\n')); // No trailing newline\n});\n\ntest('incremental rendering - no trailing newline: shrink', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('A\\nB');\n\trender('A');\n\n\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\t// Should erase 1 extra line (B), not over-erase A\n\t// previousVisible=2, visibleCount=1, no trailing newline -> eraseLines(2-1+0) = eraseLines(1)\n\tt.true(secondCall.includes(ansiEscapes.eraseLines(1)));\n\tt.false(secondCall.endsWith('\\n')); // No trailing newline\n});\n\ntest('incremental rendering - no trailing newline: grow', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('A');\n\trender('A\\nB\\nC');\n\n\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\tt.true(secondCall.includes('B')); // New line B\n\tt.true(secondCall.includes('C')); // New line C\n\tt.false(secondCall.endsWith('\\n')); // No trailing newline\n});\n\ntest('incremental rendering - no trailing newline: unchanged lines do not overshoot cursor', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('A\\nB');\n\trender('A\\nB'); // Identical - should be skipped entirely\n\n\tt.is((stdout.write as any).callCount, 1); // No second write (identical)\n\n\t// Now change only the first line\n\trender('X\\nB');\n\n\tconst thirdCall = (stdout.write as any).secondCall.args[0] as string;\n\t// Should write X with newline to advance to B's line, then skip B.\n\t// The buffer ends with the \\n that moves to B's line, but no extra\n\t// cursorNextLine past B -- the cursor stays on the last visible line.\n\tt.true(thirdCall.includes('X'));\n\t// Verify no cursorNextLine appears after B's position (B is unchanged\n\t// and last, so no cursor movement is emitted for it)\n\tconst lastCursorNextLine = thirdCall.lastIndexOf(ansiEscapes.cursorNextLine);\n\tt.is(lastCursorNextLine, -1); // No cursorNextLine at all since A is changed (written) not skipped\n});\n\ntest('incremental rendering - render to empty string (full clear vs early exit)', t => {\n\tconst stdout = createStdout();\n\tconst render = logUpdate.create(stdout, {\n\t\tshowCursor: true,\n\t\tincremental: true,\n\t});\n\n\trender('Line 1\\nLine 2\\nLine 3\\n');\n\trender('\\n');\n\n\tt.is((stdout.write as any).callCount, 2);\n\tconst secondCall = (stdout.write as any).secondCall.args[0] as string;\n\tt.is(secondCall, ansiEscapes.eraseLines(4) + '\\n'); // Erases all 4 lines + writes single newline\n\n\t// Rendering empty string again should be skipped (identical output)\n\trender('\\n');\n\tt.is((stdout.write as any).callCount, 2); // No additional write\n});\n"
  },
  {
    "path": "test/margin.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text} from '../src/index.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\n\ntest('margin', t => {\n\tconst output = renderToString(\n\t\t<Box margin={2}>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n  X\\n\\n');\n});\n\ntest('margin X', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Box marginX={2}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t\t<Text>Y</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '  X  Y');\n});\n\ntest('margin Y', t => {\n\tconst output = renderToString(\n\t\t<Box marginY={2}>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\nX\\n\\n');\n});\n\ntest('margin top', t => {\n\tconst output = renderToString(\n\t\t<Box marginTop={2}>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\nX');\n});\n\ntest('margin bottom', t => {\n\tconst output = renderToString(\n\t\t<Box marginBottom={2}>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'X\\n\\n');\n});\n\ntest('margin left', t => {\n\tconst output = renderToString(\n\t\t<Box marginLeft={2}>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '  X');\n});\n\ntest('margin right', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Box marginRight={2}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t\t<Text>Y</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'X  Y');\n});\n\ntest('nested margin', t => {\n\tconst output = renderToString(\n\t\t<Box margin={2}>\n\t\t\t<Box margin={2}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n\\n\\n    X\\n\\n\\n\\n');\n});\n\ntest('margin with multiline string', t => {\n\tconst output = renderToString(\n\t\t<Box margin={2}>\n\t\t\t<Text>{'A\\nB'}</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n  A\\n  B\\n\\n');\n});\n\ntest('apply margin to text with newlines', t => {\n\tconst output = renderToString(\n\t\t<Box margin={1}>\n\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t</Box>,\n\t);\n\tt.is(output, '\\n Hello\\n World\\n');\n});\n\ntest('apply margin to wrapped text', t => {\n\tconst output = renderToString(\n\t\t<Box margin={1} width={6}>\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n Hello\\n World\\n');\n});\n\n// Concurrent mode tests\ntest('margin - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box margin={2}>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n  X\\n\\n');\n});\n\ntest('nested margin - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box margin={2}>\n\t\t\t<Box margin={2}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n\\n\\n    X\\n\\n\\n\\n');\n});\n"
  },
  {
    "path": "test/measure-element.tsx",
    "content": "import React, {useState, useRef, useEffect, useLayoutEffect} from 'react';\nimport test from 'ava';\nimport delay from 'delay';\nimport stripAnsi from 'strip-ansi';\nimport {\n\tBox,\n\tText,\n\trender,\n\tmeasureElement,\n\ttype DOMElement,\n} from '../src/index.js';\nimport createStdout from './helpers/create-stdout.js';\n\ntest('measure element', async t => {\n\tconst stdout = createStdout();\n\n\tfunction Test() {\n\t\tconst [width, setWidth] = useState(0);\n\t\tconst ref = useRef<DOMElement>(null);\n\n\t\tuseEffect(() => {\n\t\t\tif (!ref.current) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetWidth(measureElement(ref.current).width);\n\t\t}, []);\n\n\t\treturn (\n\t\t\t<Box ref={ref}>\n\t\t\t\t<Text>Width: {width}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\trender(<Test />, {stdout, debug: true});\n\tt.is((stdout.write as any).firstCall.args[0], 'Width: 0');\n\tawait delay(100);\n\tt.is((stdout.write as any).lastCall.args[0], 'Width: 100');\n});\n\ntest('measure element after state update', async t => {\n\tconst stdout = createStdout();\n\tlet setTestItems!: (items: string[]) => void;\n\n\tfunction Test() {\n\t\tconst [items, setItems] = useState<string[]>([]);\n\t\tconst [height, setHeight] = useState(0);\n\t\tconst ref = useRef<DOMElement>(null);\n\n\t\tsetTestItems = setItems;\n\n\t\tuseEffect(() => {\n\t\t\tif (!ref.current) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetHeight(measureElement(ref.current).height);\n\t\t}, [items.length]);\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Box ref={ref} flexDirection=\"column\">\n\t\t\t\t\t{items.map(item => (\n\t\t\t\t\t\t<Text key={item}>{item}</Text>\n\t\t\t\t\t))}\n\t\t\t\t</Box>\n\t\t\t\t<Text>Height: {height}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\trender(<Test />, {stdout, debug: true});\n\tawait delay(50);\n\n\tsetTestItems(['line 1', 'line 2', 'line 3']);\n\tawait delay(50);\n\n\tt.is(\n\t\tstripAnsi((stdout.write as any).lastCall.firstArg as string).trim(),\n\t\t'line 1\\nline 2\\nline 3\\nHeight: 3',\n\t);\n});\n\ntest('measure element after multiple state updates', async t => {\n\tconst stdout = createStdout();\n\tlet setTestItems!: (items: string[]) => void;\n\n\tfunction Test() {\n\t\tconst [items, setItems] = useState<string[]>([]);\n\t\tconst [height, setHeight] = useState(0);\n\t\tconst ref = useRef<DOMElement>(null);\n\n\t\tsetTestItems = setItems;\n\n\t\tuseEffect(() => {\n\t\t\tif (!ref.current) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetHeight(measureElement(ref.current).height);\n\t\t}, [items.length]);\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Box ref={ref} flexDirection=\"column\">\n\t\t\t\t\t{items.map(item => (\n\t\t\t\t\t\t<Text key={item}>{item}</Text>\n\t\t\t\t\t))}\n\t\t\t\t</Box>\n\t\t\t\t<Text>Height: {height}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\trender(<Test />, {stdout, debug: true});\n\tawait delay(50);\n\n\tsetTestItems(['line 1', 'line 2', 'line 3']);\n\tawait delay(50);\n\n\tsetTestItems(['line 1']);\n\tawait delay(50);\n\n\tt.is(\n\t\tstripAnsi((stdout.write as any).lastCall.firstArg as string).trim(),\n\t\t'line 1\\nHeight: 1',\n\t);\n});\n\ntest('measure element in useLayoutEffect after state update', async t => {\n\tconst stdout = createStdout();\n\tlet setTestItems!: (items: string[]) => void;\n\n\tfunction Test() {\n\t\tconst [items, setItems] = useState<string[]>([]);\n\t\tconst [height, setHeight] = useState(0);\n\t\tconst ref = useRef<DOMElement>(null);\n\n\t\tsetTestItems = setItems;\n\n\t\tuseLayoutEffect(() => {\n\t\t\tif (!ref.current) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetHeight(measureElement(ref.current).height);\n\t\t}, [items.length]);\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Box ref={ref} flexDirection=\"column\">\n\t\t\t\t\t{items.map(item => (\n\t\t\t\t\t\t<Text key={item}>{item}</Text>\n\t\t\t\t\t))}\n\t\t\t\t</Box>\n\t\t\t\t<Text>Height: {height}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\trender(<Test />, {stdout, debug: true});\n\tawait delay(50);\n\n\tsetTestItems(['line 1', 'line 2', 'line 3']);\n\tawait delay(50);\n\n\tt.is(\n\t\tstripAnsi((stdout.write as any).lastCall.firstArg as string).trim(),\n\t\t'line 1\\nline 2\\nline 3\\nHeight: 3',\n\t);\n});\n\ntest.serial('calculate layout while rendering is throttled', async t => {\n\tconst stdout = createStdout();\n\n\tfunction Test() {\n\t\tconst [width, setWidth] = useState(0);\n\t\tconst ref = useRef<DOMElement>(null);\n\n\t\tuseEffect(() => {\n\t\t\tif (!ref.current) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetWidth(measureElement(ref.current).width);\n\t\t}, []);\n\n\t\treturn (\n\t\t\t<Box ref={ref}>\n\t\t\t\t<Text>Width: {width}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(null, {stdout, patchConsole: false});\n\trerender(<Test />);\n\tawait delay(50);\n\n\t// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call\n\tconst writes: string[] = (stdout.write as any)\n\t\t.getCalls()\n\t\t.map((c: any) => c.args[0] as string)\n\t\t.filter(\n\t\t\t(w: string) =>\n\t\t\t\t!w.startsWith('\\u001B[?25') && !w.startsWith('\\u001B[?2026'),\n\t\t);\n\tconst lastContentWrite = writes.at(-1)!;\n\n\tt.is(stripAnsi(lastContentWrite).trim(), 'Width: 100');\n});\n"
  },
  {
    "path": "test/measure-text.tsx",
    "content": "import test from 'ava';\nimport measureText from '../src/measure-text.js';\n\ntest('measure single word', t => {\n\tt.deepEqual(measureText('constructor'), {width: 11, height: 1});\n});\n\ntest('measure empty string', t => {\n\tt.deepEqual(measureText(''), {width: 0, height: 0});\n});\n\ntest('measure multiline text', t => {\n\tconst result = measureText('hello\\nworld');\n\tt.is(result.width, 5);\n\tt.is(result.height, 2);\n});\n\ntest('measure multiline text with varying line lengths', t => {\n\tconst result = measureText('a\\nfoo\\nhi');\n\tt.is(result.width, 3);\n\tt.is(result.height, 3);\n});\n\ntest('measure text with trailing newline', t => {\n\tconst result = measureText('hello\\n');\n\tt.is(result.width, 5);\n\tt.is(result.height, 2);\n});\n\ntest('measure text with only newlines', t => {\n\tconst result = measureText('\\n\\n');\n\tt.is(result.width, 0);\n\tt.is(result.height, 3);\n});\n\ntest('returns cached result on repeated calls', t => {\n\tconst first = measureText('cached-test');\n\tt.is(first.width, 11);\n\tt.is(first.height, 1);\n\tconst second = measureText('cached-test');\n\tt.is(first, second);\n});\n\ntest('measure text with ANSI escape sequences', t => {\n\tconst result = measureText('\\u001B[31mred\\u001B[0m');\n\tt.is(result.width, 3);\n\tt.is(result.height, 1);\n});\n\ntest('measure text with 256-color ANSI', t => {\n\tconst result = measureText('\\u001B[38;5;196mred\\u001B[0m');\n\tt.is(result.width, 3);\n\tt.is(result.height, 1);\n});\n\ntest('measure text with wide characters', t => {\n\tconst result = measureText('你好');\n\tt.is(result.width, 4);\n\tt.is(result.height, 1);\n});\n\ntest('measure text with emoji', t => {\n\tconst result = measureText('🍔');\n\tt.is(result.width, 2);\n\tt.is(result.height, 1);\n});\n\ntest('measure multiline with wide characters', t => {\n\tconst result = measureText('🍔🍟\\nabc');\n\tt.is(result.width, 4);\n\tt.is(result.height, 2);\n});\n"
  },
  {
    "path": "test/overflow.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport boxen, {type Options} from 'boxen';\nimport sliceAnsi from 'slice-ansi';\nimport {Box, Text} from '../src/index.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\n\nconst box = (text: string, options?: Options): string => {\n\treturn boxen(text, {\n\t\t...options,\n\t\tborderStyle: 'round',\n\t});\n};\n\nconst clipX = (text: string, columns: number): string => {\n\treturn text\n\t\t.split('\\n')\n\t\t.map(line => sliceAnsi(line, 0, columns).trim())\n\t\t.join('\\n');\n};\n\ntest('overflowX - single text node in a box inside overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} overflowX=\"hidden\">\n\t\t\t<Box width={16} flexShrink={0}>\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello');\n});\n\ntest('overflowX - single text node inside overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} overflowX=\"hidden\" borderStyle=\"round\">\n\t\t\t<Box width={16} flexShrink={0}>\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, box('Hell'));\n});\n\ntest('overflowX - single text node in a box with border inside overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} overflowX=\"hidden\">\n\t\t\t<Box width={16} flexShrink={0} borderStyle=\"round\">\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, clipX(box('Hello'), 6));\n});\n\ntest('overflowX - multiple text nodes in a box inside overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} overflowX=\"hidden\">\n\t\t\t<Box width={12} flexShrink={0}>\n\t\t\t\t<Text>Hello </Text>\n\t\t\t\t<Text>World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello');\n});\n\ntest('overflowX - multiple text nodes in a box inside overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box width={8} overflowX=\"hidden\" borderStyle=\"round\">\n\t\t\t<Box width={12} flexShrink={0}>\n\t\t\t\t<Text>Hello </Text>\n\t\t\t\t<Text>World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, box('Hello '));\n});\n\ntest('overflowX - multiple text nodes in a box with border inside overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={8} overflowX=\"hidden\">\n\t\t\t<Box width={12} flexShrink={0} borderStyle=\"round\">\n\t\t\t\t<Text>Hello </Text>\n\t\t\t\t<Text>World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, clipX(box('HelloWo\\n'), 8));\n});\n\ntest('overflowX - multiple boxes inside overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} overflowX=\"hidden\">\n\t\t\t<Box width={6} flexShrink={0}>\n\t\t\t\t<Text>Hello </Text>\n\t\t\t</Box>\n\t\t\t<Box width={6} flexShrink={0}>\n\t\t\t\t<Text>World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello');\n});\n\ntest('overflowX - multiple boxes inside overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box width={8} overflowX=\"hidden\" borderStyle=\"round\">\n\t\t\t<Box width={6} flexShrink={0}>\n\t\t\t\t<Text>Hello </Text>\n\t\t\t</Box>\n\t\t\t<Box width={6} flexShrink={0}>\n\t\t\t\t<Text>World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, box('Hello '));\n});\n\ntest('overflowX - box before left edge of overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} overflowX=\"hidden\">\n\t\t\t<Box marginLeft={-12} width={6} flexShrink={0}>\n\t\t\t\t<Text>Hello</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '');\n});\n\ntest('overflowX - box before left edge of overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} overflowX=\"hidden\" borderStyle=\"round\">\n\t\t\t<Box marginLeft={-12} width={6} flexShrink={0}>\n\t\t\t\t<Text>Hello</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, box(' '.repeat(4)));\n});\n\ntest('overflowX - box intersecting with left edge of overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} overflowX=\"hidden\">\n\t\t\t<Box marginLeft={-3} width={12} flexShrink={0}>\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'lo Wor');\n});\n\ntest('overflowX - box intersecting with left edge of overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box width={8} overflowX=\"hidden\" borderStyle=\"round\">\n\t\t\t<Box marginLeft={-3} width={12} flexShrink={0}>\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, box('lo Wor'));\n});\n\ntest('overflowX - box after right edge of overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} overflowX=\"hidden\">\n\t\t\t<Box marginLeft={6} width={6} flexShrink={0}>\n\t\t\t\t<Text>Hello</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '');\n});\n\ntest('overflowX - box intersecting with right edge of overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} overflowX=\"hidden\">\n\t\t\t<Box marginLeft={3} width={6} flexShrink={0}>\n\t\t\t\t<Text>Hello</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '   Hel');\n});\n\ntest('overflowY - single text node inside overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box height={1} overflowY=\"hidden\">\n\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello');\n});\n\ntest('overflowY - single text node inside overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box width={20} height={3} overflowY=\"hidden\" borderStyle=\"round\">\n\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, box('Hello'.padEnd(18, ' ')));\n});\n\ntest('overflowY - multiple boxes inside overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box height={2} overflowY=\"hidden\" flexDirection=\"column\">\n\t\t\t<Box flexShrink={0}>\n\t\t\t\t<Text>Line #1</Text>\n\t\t\t</Box>\n\t\t\t<Box flexShrink={0}>\n\t\t\t\t<Text>Line #2</Text>\n\t\t\t</Box>\n\t\t\t<Box flexShrink={0}>\n\t\t\t\t<Text>Line #3</Text>\n\t\t\t</Box>\n\t\t\t<Box flexShrink={0}>\n\t\t\t\t<Text>Line #4</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Line #1\\nLine #2');\n});\n\ntest('overflowY - multiple boxes inside overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box\n\t\t\twidth={9}\n\t\t\theight={4}\n\t\t\toverflowY=\"hidden\"\n\t\t\tflexDirection=\"column\"\n\t\t\tborderStyle=\"round\"\n\t\t>\n\t\t\t<Box flexShrink={0}>\n\t\t\t\t<Text>Line #1</Text>\n\t\t\t</Box>\n\t\t\t<Box flexShrink={0}>\n\t\t\t\t<Text>Line #2</Text>\n\t\t\t</Box>\n\t\t\t<Box flexShrink={0}>\n\t\t\t\t<Text>Line #3</Text>\n\t\t\t</Box>\n\t\t\t<Box flexShrink={0}>\n\t\t\t\t<Text>Line #4</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, box('Line #1\\nLine #2'));\n});\n\ntest('overflowY - box above top edge of overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box height={1} overflowY=\"hidden\">\n\t\t\t<Box marginTop={-2} height={2} flexShrink={0}>\n\t\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '');\n});\n\ntest('overflowY - box above top edge of overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box width={7} height={3} overflowY=\"hidden\" borderStyle=\"round\">\n\t\t\t<Box marginTop={-3} height={2} flexShrink={0}>\n\t\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, box(' '.repeat(5)));\n});\n\ntest('overflowY - box intersecting with top edge of overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box height={1} overflowY=\"hidden\">\n\t\t\t<Box marginTop={-1} height={2} flexShrink={0}>\n\t\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'World');\n});\n\ntest('overflowY - box intersecting with top edge of overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box width={7} height={3} overflowY=\"hidden\" borderStyle=\"round\">\n\t\t\t<Box marginTop={-1} height={2} flexShrink={0}>\n\t\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, box('World'));\n});\n\ntest('overflowY - box below bottom edge of overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box height={1} overflowY=\"hidden\">\n\t\t\t<Box marginTop={1} height={2} flexShrink={0}>\n\t\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '');\n});\n\ntest('overflowY - box below bottom edge of overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box width={7} height={3} overflowY=\"hidden\" borderStyle=\"round\">\n\t\t\t<Box marginTop={2} height={2} flexShrink={0}>\n\t\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, box(' '.repeat(5)));\n});\n\ntest('overflowY - box intersecting with bottom edge of overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box height={1} overflowY=\"hidden\">\n\t\t\t<Box height={2} flexShrink={0}>\n\t\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello');\n});\n\ntest('overflowY - box intersecting with bottom edge of overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box width={7} height={3} overflowY=\"hidden\" borderStyle=\"round\">\n\t\t\t<Box height={2} flexShrink={0}>\n\t\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, box('Hello'));\n});\n\ntest('overflow - single text node inside overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box paddingBottom={1}>\n\t\t\t<Box width={6} height={1} overflow=\"hidden\">\n\t\t\t\t<Box width={12} height={2} flexShrink={0}>\n\t\t\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello\\n');\n});\n\ntest('overflow - single text node inside overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box paddingBottom={1}>\n\t\t\t<Box width={8} height={3} overflow=\"hidden\" borderStyle=\"round\">\n\t\t\t\t<Box width={12} height={2} flexShrink={0}>\n\t\t\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, `${box('Hello ')}\\n`);\n});\n\ntest('overflow - multiple boxes inside overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box paddingBottom={1}>\n\t\t\t<Box width={4} height={1} overflow=\"hidden\">\n\t\t\t\t<Box width={2} height={2} flexShrink={0}>\n\t\t\t\t\t<Text>TL{'\\n'}BL</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Box width={2} height={2} flexShrink={0}>\n\t\t\t\t\t<Text>TR{'\\n'}BR</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'TLTR\\n');\n});\n\ntest('overflow - multiple boxes inside overflow container with border', t => {\n\tconst output = renderToString(\n\t\t<Box paddingBottom={1}>\n\t\t\t<Box width={6} height={3} overflow=\"hidden\" borderStyle=\"round\">\n\t\t\t\t<Box width={2} height={2} flexShrink={0}>\n\t\t\t\t\t<Text>TL{'\\n'}BL</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Box width={2} height={2} flexShrink={0}>\n\t\t\t\t\t<Text>TR{'\\n'}BR</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, `${box('TLTR')}\\n`);\n});\n\ntest('overflow - box intersecting with top left edge of overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={4} height={4} overflow=\"hidden\">\n\t\t\t<Box marginTop={-2} marginLeft={-2} width={4} height={4} flexShrink={0}>\n\t\t\t\t<Text>\n\t\t\t\t\tAAAA{'\\n'}BBBB{'\\n'}CCCC{'\\n'}DDDD\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'CC\\nDD\\n\\n');\n});\n\ntest('overflow - box intersecting with top right edge of overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={4} height={4} overflow=\"hidden\">\n\t\t\t<Box marginTop={-2} marginLeft={2} width={4} height={4} flexShrink={0}>\n\t\t\t\t<Text>\n\t\t\t\t\tAAAA{'\\n'}BBBB{'\\n'}CCCC{'\\n'}DDDD\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '  CC\\n  DD\\n\\n');\n});\n\ntest('overflow - box intersecting with bottom left edge of overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={4} height={4} overflow=\"hidden\">\n\t\t\t<Box marginTop={2} marginLeft={-2} width={4} height={4} flexShrink={0}>\n\t\t\t\t<Text>\n\t\t\t\t\tAAAA{'\\n'}BBBB{'\\n'}CCCC{'\\n'}DDDD\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\nAA\\nBB');\n});\n\ntest('overflow - box intersecting with bottom right edge of overflow container', t => {\n\tconst output = renderToString(\n\t\t<Box width={4} height={4} overflow=\"hidden\">\n\t\t\t<Box marginTop={2} marginLeft={2} width={4} height={4} flexShrink={0}>\n\t\t\t\t<Text>\n\t\t\t\t\tAAAA{'\\n'}BBBB{'\\n'}CCCC{'\\n'}DDDD\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n  AA\\n  BB');\n});\n\ntest('nested overflow', t => {\n\tconst output = renderToString(\n\t\t<Box paddingBottom={1}>\n\t\t\t<Box width={4} height={4} overflow=\"hidden\" flexDirection=\"column\">\n\t\t\t\t<Box width={2} height={2} overflow=\"hidden\">\n\t\t\t\t\t<Box width={4} height={4} flexShrink={0}>\n\t\t\t\t\t\t<Text>\n\t\t\t\t\t\t\tAAAA{'\\n'}BBBB{'\\n'}CCCC{'\\n'}DDDD\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t</Box>\n\n\t\t\t\t<Box width={4} height={3}>\n\t\t\t\t\t<Text>\n\t\t\t\t\t\tXXXX{'\\n'}YYYY{'\\n'}ZZZZ\n\t\t\t\t\t</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'AA\\nBB\\nXXXX\\nYYYY\\n');\n});\n\n// See https://github.com/vadimdemedes/ink/pull/564#issuecomment-1637022742\ntest('out of bounds writes do not crash', t => {\n\tconst output = renderToString(\n\t\t<Box width={12} height={10} borderStyle=\"round\" />,\n\t\t{columns: 10},\n\t);\n\n\tconst expected = boxen('', {\n\t\twidth: 12,\n\t\theight: 10,\n\t\tborderStyle: 'round',\n\t})\n\t\t.split('\\n')\n\t\t.map((line, index) => {\n\t\t\treturn index === 0 || index === 9\n\t\t\t\t? line\n\t\t\t\t: `${line.slice(0, 10)}${line[11] ?? ''}`;\n\t\t})\n\t\t.join('\\n');\n\n\tt.is(output, expected);\n});\n\n// Concurrent mode tests\ntest('overflowX - single text node in a box inside overflow container - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box width={6} overflowX=\"hidden\">\n\t\t\t<Box width={16} flexShrink={0}>\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\tt.is(output, 'Hello');\n});\n\ntest('overflowY - single text node inside overflow container - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box height={1} overflowY=\"hidden\">\n\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t</Box>,\n\t);\n\tt.is(output, 'Hello');\n});\n\ntest('overflow - single text node inside overflow container - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box paddingBottom={1}>\n\t\t\t<Box width={6} height={1} overflow=\"hidden\">\n\t\t\t\t<Box width={12} height={2} flexShrink={0}>\n\t\t\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\tt.is(output, 'Hello\\n');\n});\n\ntest('nested overflow - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box paddingBottom={1}>\n\t\t\t<Box width={4} height={4} overflow=\"hidden\" flexDirection=\"column\">\n\t\t\t\t<Box width={2} height={2} overflow=\"hidden\">\n\t\t\t\t\t<Box width={4} height={4} flexShrink={0}>\n\t\t\t\t\t\t<Text>\n\t\t\t\t\t\t\tAAAA{'\\n'}BBBB{'\\n'}CCCC{'\\n'}DDDD\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t</Box>\n\n\t\t\t\t<Box width={4} height={3}>\n\t\t\t\t\t<Text>\n\t\t\t\t\t\tXXXX{'\\n'}YYYY{'\\n'}ZZZZ\n\t\t\t\t\t</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\tt.is(output, 'AA\\nBB\\nXXXX\\nYYYY\\n');\n});\n"
  },
  {
    "path": "test/padding.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text} from '../src/index.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\n\ntest('padding', t => {\n\tconst output = renderToString(\n\t\t<Box padding={2}>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n  X\\n\\n');\n});\n\ntest('padding X', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Box paddingX={2}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t\t<Text>Y</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '  X  Y');\n});\n\ntest('padding Y', t => {\n\tconst output = renderToString(\n\t\t<Box paddingY={2}>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\nX\\n\\n');\n});\n\ntest('padding top', t => {\n\tconst output = renderToString(\n\t\t<Box paddingTop={2}>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\nX');\n});\n\ntest('padding bottom', t => {\n\tconst output = renderToString(\n\t\t<Box paddingBottom={2}>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'X\\n\\n');\n});\n\ntest('padding left', t => {\n\tconst output = renderToString(\n\t\t<Box paddingLeft={2}>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '  X');\n});\n\ntest('padding right', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Box paddingRight={2}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t\t<Text>Y</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'X  Y');\n});\n\ntest('nested padding', t => {\n\tconst output = renderToString(\n\t\t<Box padding={2}>\n\t\t\t<Box padding={2}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n\\n\\n    X\\n\\n\\n\\n');\n});\n\ntest('padding with multiline string', t => {\n\tconst output = renderToString(\n\t\t<Box padding={2}>\n\t\t\t<Text>{'A\\nB'}</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n  A\\n  B\\n\\n');\n});\n\ntest('apply padding to text with newlines', t => {\n\tconst output = renderToString(\n\t\t<Box padding={1}>\n\t\t\t<Text>Hello{'\\n'}World</Text>\n\t\t</Box>,\n\t);\n\tt.is(output, '\\n Hello\\n World\\n');\n});\n\ntest('apply padding to wrapped text', t => {\n\tconst output = renderToString(\n\t\t<Box padding={1} width={5}>\n\t\t\t<Text>Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n Hel\\n lo\\n Wor\\n ld\\n');\n});\n\ntest('text wrapping respects paddingX with flexGrow', t => {\n\t// https://github.com/vadimdemedes/ink/issues/584\n\tconst output = renderToString(\n\t\t<Box width={40} borderStyle=\"round\">\n\t\t\t<Box paddingX={2}>\n\t\t\t\t<Box marginLeft={2}>\n\t\t\t\t\t<Text>•</Text>\n\t\t\t\t\t<Box flexGrow={1} marginLeft={1}>\n\t\t\t\t\t\t<Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tconst lines = output.split('\\n');\n\tfor (const line of lines) {\n\t\tt.true(\n\t\t\tline.length <= 40,\n\t\t\t`Line \"${line}\" exceeds container width of 40 (got ${line.length})`,\n\t\t);\n\t}\n});\n\n// Concurrent mode tests\ntest('padding - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box padding={2}>\n\t\t\t<Text>X</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n  X\\n\\n');\n});\n\ntest('nested padding - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box padding={2}>\n\t\t\t<Box padding={2}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n\\n\\n    X\\n\\n\\n\\n');\n});\n"
  },
  {
    "path": "test/position.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text, render} from '../src/index.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\nimport createStdout from './helpers/create-stdout.js';\n\ntest('absolute position with top and left offsets', t => {\n\tconst output = renderToString(\n\t\t<Box width={5} height={3}>\n\t\t\t<Box position=\"absolute\" top={1} left={2}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n  X\\n');\n});\n\ntest('absolute position with bottom and right offsets', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} height={4}>\n\t\t\t<Box position=\"absolute\" bottom={1} right={1}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n    X\\n');\n});\n\ntest('absolute position with percentage offsets', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} height={4}>\n\t\t\t<Box position=\"absolute\" top=\"50%\" left=\"50%\">\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n\\n   X\\n');\n});\n\ntest('absolute position with percentage bottom and right offsets', t => {\n\tconst output = renderToString(\n\t\t<Box width={6} height={4}>\n\t\t\t<Box position=\"absolute\" bottom=\"50%\" right=\"50%\">\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n  X\\n\\n');\n});\n\ntest('relative position offsets visual position while keeping flow', t => {\n\tconst output = renderToString(\n\t\t<Box width={5}>\n\t\t\t<Box position=\"relative\" left={2}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, ' BA');\n});\n\ntest('static position ignores offsets', t => {\n\tconst output = renderToString(\n\t\t<Box width={5}>\n\t\t\t<Box position=\"static\" left={2}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'AB');\n});\n\ntest('static position ignores percentage offsets', t => {\n\tconst output = renderToString(\n\t\t<Box width={5}>\n\t\t\t<Box position=\"static\" left=\"50%\">\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'AB');\n});\n\ntest('clears top offset on rerender', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({top}: {readonly top?: number}) {\n\t\treturn (\n\t\t\t<Box width={5} height={3}>\n\t\t\t\t<Box position=\"absolute\" top={top} left={2}>\n\t\t\t\t\t<Text>X</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test top={1} />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is(stdout.write.lastCall.args[0], '\\n  X\\n');\n\n\trerender(<Test top={undefined} />);\n\tt.is(stdout.write.lastCall.args[0], '  X\\n\\n');\n});\n\ntest('clears percentage top and left offsets on rerender', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({top, left}: {readonly top?: string; readonly left?: string}) {\n\t\treturn (\n\t\t\t<Box width={6} height={4}>\n\t\t\t\t<Box position=\"absolute\" top={top} left={left}>\n\t\t\t\t\t<Text>X</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test top=\"50%\" left=\"50%\" />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is(stdout.write.lastCall.args[0], '\\n\\n   X\\n');\n\n\trerender(<Test top={undefined} left={undefined} />);\n\tt.is(stdout.write.lastCall.args[0], 'X\\n\\n\\n');\n});\n\ntest('clears percentage top and left offsets when props are omitted on rerender', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({showOffsets}: {readonly showOffsets: boolean}) {\n\t\treturn (\n\t\t\t<Box width={6} height={4}>\n\t\t\t\t<Box\n\t\t\t\t\tposition=\"absolute\"\n\t\t\t\t\t{...(showOffsets ? {top: '50%' as const, left: '50%' as const} : {})}\n\t\t\t\t>\n\t\t\t\t\t<Text>X</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test showOffsets />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is(stdout.write.lastCall.args[0], '\\n\\n   X\\n');\n\n\trerender(<Test showOffsets={false} />);\n\tt.is(stdout.write.lastCall.args[0], 'X\\n\\n\\n');\n});\n\ntest('clears bottom and right offsets on rerender', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({\n\t\tbottom,\n\t\tright,\n\t}: {\n\t\treadonly bottom?: number;\n\t\treadonly right?: number;\n\t}) {\n\t\treturn (\n\t\t\t<Box width={6} height={4}>\n\t\t\t\t<Box position=\"absolute\" bottom={bottom} right={right}>\n\t\t\t\t\t<Text>X</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test bottom={1} right={1} />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is(stdout.write.lastCall.args[0], '\\n\\n    X\\n');\n\n\trerender(<Test bottom={undefined} right={undefined} />);\n\tt.is(stdout.write.lastCall.args[0], 'X\\n\\n\\n');\n});\n\ntest('absolute position with top and left offsets - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box width={5} height={3}>\n\t\t\t<Box position=\"absolute\" top={1} left={2}>\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '\\n  X\\n');\n});\n"
  },
  {
    "path": "test/reconciler.tsx",
    "content": "import React, {Suspense} from 'react';\nimport test from 'ava';\nimport chalk from 'chalk';\nimport {Box, Text, render} from '../src/index.js';\nimport createStdout from './helpers/create-stdout.js';\n\ntest('update child', t => {\n\tfunction Test({update}: {readonly update?: boolean}) {\n\t\treturn <Text>{update ? 'B' : 'A'}</Text>;\n\t}\n\n\tconst stdoutActual = createStdout();\n\tconst stdoutExpected = createStdout();\n\n\tconst actual = render(<Test />, {\n\t\tstdout: stdoutActual,\n\t\tdebug: true,\n\t});\n\n\tconst expected = render(<Text>A</Text>, {\n\t\tstdout: stdoutExpected,\n\t\tdebug: true,\n\t});\n\n\tt.is(\n\t\t(stdoutActual.write as any).lastCall.args[0],\n\t\t(stdoutExpected.write as any).lastCall.args[0],\n\t);\n\n\tactual.rerender(<Test update />);\n\texpected.rerender(<Text>B</Text>);\n\n\tt.is(\n\t\t(stdoutActual.write as any).lastCall.args[0],\n\t\t(stdoutExpected.write as any).lastCall.args[0],\n\t);\n});\n\ntest('update text node', t => {\n\tfunction Test({update}: {readonly update?: boolean}) {\n\t\treturn (\n\t\t\t<Box>\n\t\t\t\t<Text>Hello </Text>\n\t\t\t\t<Text>{update ? 'B' : 'A'}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst stdoutActual = createStdout();\n\tconst stdoutExpected = createStdout();\n\n\tconst actual = render(<Test />, {\n\t\tstdout: stdoutActual,\n\t\tdebug: true,\n\t});\n\n\tconst expected = render(<Text>Hello A</Text>, {\n\t\tstdout: stdoutExpected,\n\t\tdebug: true,\n\t});\n\n\tt.is(\n\t\t(stdoutActual.write as any).lastCall.args[0],\n\t\t(stdoutExpected.write as any).lastCall.args[0],\n\t);\n\n\tactual.rerender(<Test update />);\n\texpected.rerender(<Text>Hello B</Text>);\n\n\tt.is(\n\t\t(stdoutActual.write as any).lastCall.args[0],\n\t\t(stdoutExpected.write as any).lastCall.args[0],\n\t);\n});\n\ntest('remove style prop from intrinsic node', t => {\n\tfunction Test({withStyle}: {readonly withStyle: boolean}) {\n\t\treturn (\n\t\t\t<ink-box style={withStyle ? {marginLeft: 1} : undefined}>\n\t\t\t\t<ink-text>X</ink-text>\n\t\t\t</ink-box>\n\t\t);\n\t}\n\n\tconst stdout = createStdout();\n\n\tconst {rerender} = render(<Test withStyle />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is((stdout.write as any).lastCall.args[0], ' X');\n\n\trerender(<Test withStyle={false} />);\n\tt.is((stdout.write as any).lastCall.args[0], 'X');\n});\n\ntest('append child', t => {\n\tfunction Test({append}: {readonly append?: boolean}) {\n\t\tif (append) {\n\t\t\treturn (\n\t\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t\t<Text>A</Text>\n\t\t\t\t\t<Text>B</Text>\n\t\t\t\t</Box>\n\t\t\t);\n\t\t}\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst stdoutActual = createStdout();\n\tconst stdoutExpected = createStdout();\n\n\tconst actual = render(<Test />, {\n\t\tstdout: stdoutActual,\n\t\tdebug: true,\n\t});\n\n\tconst expected = render(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>A</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tstdout: stdoutExpected,\n\t\t\tdebug: true,\n\t\t},\n\t);\n\n\tt.is(\n\t\t(stdoutActual.write as any).lastCall.args[0],\n\t\t(stdoutExpected.write as any).lastCall.args[0],\n\t);\n\n\tactual.rerender(<Test append />);\n\n\texpected.rerender(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\t(stdoutActual.write as any).lastCall.args[0],\n\t\t(stdoutExpected.write as any).lastCall.args[0],\n\t);\n});\n\ntest('insert child between other children', t => {\n\tfunction Test({insert}: {readonly insert?: boolean}) {\n\t\tif (insert) {\n\t\t\treturn (\n\t\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t\t<Text key=\"a\">A</Text>\n\t\t\t\t\t<Text key=\"b\">B</Text>\n\t\t\t\t\t<Text key=\"c\">C</Text>\n\t\t\t\t</Box>\n\t\t\t);\n\t\t}\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Text key=\"a\">A</Text>\n\t\t\t\t<Text key=\"c\">C</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst stdoutActual = createStdout();\n\tconst stdoutExpected = createStdout();\n\n\tconst actual = render(<Test />, {\n\t\tstdout: stdoutActual,\n\t\tdebug: true,\n\t});\n\n\tconst expected = render(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>C</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tstdout: stdoutExpected,\n\t\t\tdebug: true,\n\t\t},\n\t);\n\n\tt.is(\n\t\t(stdoutActual.write as any).lastCall.args[0],\n\t\t(stdoutExpected.write as any).lastCall.args[0],\n\t);\n\n\tactual.rerender(<Test insert />);\n\n\texpected.rerender(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\t(stdoutActual.write as any).lastCall.args[0],\n\t\t(stdoutExpected.write as any).lastCall.args[0],\n\t);\n});\n\ntest('remove child', t => {\n\tfunction Test({remove}: {readonly remove?: boolean}) {\n\t\tif (remove) {\n\t\t\treturn (\n\t\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t\t<Text>A</Text>\n\t\t\t\t</Box>\n\t\t\t);\n\t\t}\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Text>A</Text>\n\t\t\t\t<Text>B</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst stdoutActual = createStdout();\n\tconst stdoutExpected = createStdout();\n\n\tconst actual = render(<Test />, {\n\t\tstdout: stdoutActual,\n\t\tdebug: true,\n\t});\n\n\tconst expected = render(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tstdout: stdoutExpected,\n\t\t\tdebug: true,\n\t\t},\n\t);\n\n\tt.is(\n\t\t(stdoutActual.write as any).lastCall.args[0],\n\t\t(stdoutExpected.write as any).lastCall.args[0],\n\t);\n\n\tactual.rerender(<Test remove />);\n\n\texpected.rerender(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>A</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\t(stdoutActual.write as any).lastCall.args[0],\n\t\t(stdoutExpected.write as any).lastCall.args[0],\n\t);\n});\n\ntest('reorder children', t => {\n\tfunction Test({reorder}: {readonly reorder?: boolean}) {\n\t\tif (reorder) {\n\t\t\treturn (\n\t\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t\t<Text key=\"b\">B</Text>\n\t\t\t\t\t<Text key=\"a\">A</Text>\n\t\t\t\t</Box>\n\t\t\t);\n\t\t}\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Text key=\"a\">A</Text>\n\t\t\t\t<Text key=\"b\">B</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst stdoutActual = createStdout();\n\tconst stdoutExpected = createStdout();\n\n\tconst actual = render(<Test />, {\n\t\tstdout: stdoutActual,\n\t\tdebug: true,\n\t});\n\n\tconst expected = render(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tstdout: stdoutExpected,\n\t\t\tdebug: true,\n\t\t},\n\t);\n\n\tt.is(\n\t\t(stdoutActual.write as any).lastCall.args[0],\n\t\t(stdoutExpected.write as any).lastCall.args[0],\n\t);\n\n\tactual.rerender(<Test reorder />);\n\n\texpected.rerender(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>B</Text>\n\t\t\t<Text>A</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(\n\t\t(stdoutActual.write as any).lastCall.args[0],\n\t\t(stdoutExpected.write as any).lastCall.args[0],\n\t);\n});\n\ntest('replace child node with text', t => {\n\tconst stdout = createStdout();\n\n\tfunction Dynamic({replace}: {readonly replace?: boolean}) {\n\t\treturn <Text>{replace ? 'x' : <Text color=\"green\">test</Text>}</Text>;\n\t}\n\n\tconst {rerender} = render(<Dynamic />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is((stdout.write as any).lastCall.args[0], chalk.green('test'));\n\n\trerender(<Dynamic replace />);\n\tt.is((stdout.write as any).lastCall.args[0], 'x');\n});\n\ntest('support suspense', async t => {\n\tconst stdout = createStdout();\n\n\tlet promise: Promise<void> | undefined;\n\tlet state: 'pending' | 'done' | undefined;\n\tlet value: string | undefined;\n\n\tconst read = () => {\n\t\tif (!promise) {\n\t\t\tpromise = new Promise(resolve => {\n\t\t\t\tsetTimeout(resolve, 100);\n\t\t\t});\n\n\t\t\tstate = 'pending';\n\n\t\t\t(async () => {\n\t\t\t\tawait promise;\n\t\t\t\tstate = 'done';\n\t\t\t\tvalue = 'Hello World';\n\t\t\t})();\n\t\t}\n\n\t\tif (state === 'done') {\n\t\t\treturn value;\n\t\t}\n\n\t\t// eslint-disable-next-line @typescript-eslint/only-throw-error\n\t\tthrow promise;\n\t};\n\n\tfunction Suspendable() {\n\t\treturn <Text>{read()}</Text>;\n\t}\n\n\tfunction Test() {\n\t\treturn (\n\t\t\t<Suspense fallback={<Text>Loading</Text>}>\n\t\t\t\t<Suspendable />\n\t\t\t</Suspense>\n\t\t);\n\t}\n\n\tconst out = render(<Test />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is((stdout.write as any).lastCall.args[0], 'Loading');\n\n\tawait promise;\n\tout.rerender(<Test />);\n\n\tt.is((stdout.write as any).lastCall.args[0], 'Hello World');\n});\n\ntest('support suspense with concurrent mode', async t => {\n\tconst stdout = createStdout();\n\n\tlet resolvePromise: () => void;\n\tconst promise = new Promise<void>(resolve => {\n\t\tresolvePromise = resolve;\n\t});\n\n\t// eslint-disable-next-line prefer-const\n\tlet data: string | undefined;\n\n\tfunction Suspendable() {\n\t\tif (data === undefined) {\n\t\t\t// eslint-disable-next-line @typescript-eslint/only-throw-error\n\t\t\tthrow promise;\n\t\t}\n\n\t\treturn <Text>{data}</Text>;\n\t}\n\n\tfunction Test() {\n\t\treturn (\n\t\t\t<Suspense fallback={<Text>Loading</Text>}>\n\t\t\t\t<Suspendable />\n\t\t\t</Suspense>\n\t\t);\n\t}\n\n\tconst {act} = await import('react');\n\n\tawait act(async () => {\n\t\trender(<Test />, {\n\t\t\tstdout,\n\t\t\tdebug: true,\n\t\t\tconcurrent: true,\n\t\t});\n\t});\n\n\tt.is((stdout.write as any).lastCall.args[0], 'Loading');\n\n\t// Resolve the suspense and wait for React to re-render\n\tdata = 'Hello Concurrent World';\n\tawait act(async () => {\n\t\tresolvePromise();\n\t\tawait promise;\n\t});\n\n\tt.is((stdout.write as any).lastCall.args[0], 'Hello Concurrent World');\n});\n"
  },
  {
    "path": "test/render-to-string.tsx",
    "content": "import test from 'ava';\nimport chalk from 'chalk';\nimport boxen from 'boxen';\nimport React, {useEffect, useLayoutEffect, useState} from 'react';\nimport {\n\tBox,\n\tText,\n\tStatic,\n\tTransform,\n\tNewline,\n\tSpacer,\n\trenderToString,\n} from '../src/index.js';\n\n// ── Basic rendering ─────────────────────────────────────\n\ntest('render simple text', t => {\n\tconst output = renderToString(<Text>Hello World</Text>);\n\tt.is(output, 'Hello World');\n});\n\ntest('render text with variable', t => {\n\tconst output = renderToString(<Text>Count: {42}</Text>);\n\tt.is(output, 'Count: 42');\n});\n\ntest('render nested text components', t => {\n\tfunction World() {\n\t\treturn <Text>World</Text>;\n\t}\n\n\tconst output = renderToString(\n\t\t<Text>\n\t\t\tHello <World />\n\t\t</Text>,\n\t);\n\n\tt.is(output, 'Hello World');\n});\n\ntest('render empty fragment', t => {\n\tconst output = renderToString(<></>); // eslint-disable-line react/jsx-no-useless-fragment\n\tt.is(output, '');\n});\n\ntest('render null children', t => {\n\tconst output = renderToString(<Text>{null}</Text>);\n\tt.is(output, '');\n});\n\n// ── Layout ──────────────────────────────────────────────\n\ntest('render box with padding', t => {\n\tconst output = renderToString(\n\t\t<Box paddingLeft={2}>\n\t\t\t<Text>Padded</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '  Padded');\n});\n\ntest('render box with flex direction row', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t\t<Text>C</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'ABC');\n});\n\ntest('render box with flex direction column', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>Line 1</Text>\n\t\t\t<Text>Line 2</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Line 1\\nLine 2');\n});\n\ntest('render margin', t => {\n\tconst output = renderToString(\n\t\t<Box marginLeft={2}>\n\t\t\t<Text>Margined</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '  Margined');\n});\n\ntest('render gap between items', t => {\n\tconst output = renderToString(\n\t\t<Box gap={1}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A B');\n});\n\ntest('render box with fixed width and height', t => {\n\tconst output = renderToString(\n\t\t<Box width={10} height={3}>\n\t\t\t<Text>Hi</Text>\n\t\t</Box>,\n\t);\n\n\tconst lines = output.split('\\n');\n\tt.is(lines.length, 3);\n});\n\ntest('render spacer pushes content apart', t => {\n\tconst output = renderToString(\n\t\t<Box width={20}>\n\t\t\t<Text>Left</Text>\n\t\t\t<Spacer />\n\t\t\t<Text>Right</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Left           Right');\n});\n\ntest('render newline inserts blank line', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>Above</Text>\n\t\t\t<Newline />\n\t\t\t<Text>Below</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Above\\n\\n\\nBelow');\n});\n\ntest('render box with border', t => {\n\tconst output = renderToString(\n\t\t<Box borderStyle=\"single\" width={20}>\n\t\t\t<Text>Bordered</Text>\n\t\t</Box>,\n\t\t{columns: 20},\n\t);\n\n\tt.is(\n\t\toutput,\n\t\tboxen('Bordered', {\n\t\t\twidth: 20,\n\t\t\tborderStyle: 'single',\n\t\t}),\n\t);\n});\n\n// ── Styling ─────────────────────────────────────────────\n\ntest('render colored text', t => {\n\tconst output = renderToString(<Text color=\"green\">Green</Text>);\n\tt.is(output, chalk.green('Green'));\n});\n\ntest('render bold text', t => {\n\tconst output = renderToString(<Text bold>Bold</Text>);\n\tt.is(output, chalk.bold('Bold'));\n});\n\n// ── Text wrapping and columns ───────────────────────────\n\ntest('render text with wrap', t => {\n\tconst output = renderToString(\n\t\t<Box width={7}>\n\t\t\t<Text wrap=\"wrap\">Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello\\nWorld');\n});\n\ntest('render text with truncate', t => {\n\tconst output = renderToString(\n\t\t<Box width={7}>\n\t\t\t<Text wrap=\"truncate\">Hello World</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'Hello …');\n});\n\ntest('default columns is 80', t => {\n\tconst longText = 'A'.repeat(100);\n\tconst output = renderToString(<Text>{longText}</Text>);\n\n\tconst lines = output.split('\\n');\n\tt.is(lines.length, 2);\n\tt.is(lines[0], 'A'.repeat(80));\n\tt.is(lines[1], 'A'.repeat(20));\n});\n\ntest('custom columns option', t => {\n\tconst longText = 'A'.repeat(50);\n\tconst output = renderToString(<Text>{longText}</Text>, {columns: 30});\n\n\tconst lines = output.split('\\n');\n\tt.is(lines.length, 2);\n\tt.is(lines[0], 'A'.repeat(30));\n\tt.is(lines[1], 'A'.repeat(20));\n});\n\n// ── Components ──────────────────────────────────────────\n\ntest('render Transform component', t => {\n\tconst output = renderToString(\n\t\t<Transform transform={output => output.toUpperCase()}>\n\t\t\t<Text>hello</Text>\n\t\t</Transform>,\n\t);\n\n\tt.is(output, 'HELLO');\n});\n\ntest('render Static component with items', t => {\n\tconst items = ['A', 'B', 'C'];\n\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Static items={items}>{item => <Text key={item}>{item}</Text>}</Static>\n\t\t\t<Text>Dynamic</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\nB\\nC\\nDynamic');\n});\n\ntest('render static-only output has no trailing newline', t => {\n\tconst items = ['A', 'B'];\n\n\tconst output = renderToString(\n\t\t<Static items={items}>{item => <Text key={item}>{item}</Text>}</Static>,\n\t);\n\n\tt.is(output, 'A\\nB');\n});\n\ntest('render static + dynamic output has exactly one newline between parts', t => {\n\tconst items = ['A', 'B'];\n\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Static items={items}>{item => <Text key={item}>{item}</Text>}</Static>\n\t\t\t<Text>Dynamic</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\nB\\nDynamic');\n});\n\n// ── Effect behavior ─────────────────────────────────────\n\ntest('captures initial render output before effect-driven state updates', t => {\n\tfunction App() {\n\t\tconst [text, setText] = useState('Initial');\n\n\t\tuseEffect(() => {\n\t\t\tsetText('Updated');\n\t\t}, []);\n\n\t\treturn <Text>{text}</Text>;\n\t}\n\n\tconst output = renderToString(<App />);\n\tt.is(output, 'Initial');\n});\n\ntest('useLayoutEffect state updates are reflected in output', t => {\n\tfunction App() {\n\t\tconst [text, setText] = useState('Initial');\n\n\t\tuseLayoutEffect(() => {\n\t\t\tsetText('Layout Updated');\n\t\t}, []);\n\n\t\treturn <Text>{text}</Text>;\n\t}\n\n\tconst output = renderToString(<App />);\n\tt.is(output, 'Layout Updated');\n});\n\ntest('runs effect cleanup on teardown', t => {\n\tlet cleanupRan = false;\n\n\tfunction App() {\n\t\tuseEffect(() => {\n\t\t\treturn () => {\n\t\t\t\tcleanupRan = true;\n\t\t\t};\n\t\t}, []);\n\n\t\treturn <Text>Cleanup test</Text>;\n\t}\n\n\tconst output = renderToString(<App />);\n\tt.is(output, 'Cleanup test');\n\tt.true(cleanupRan);\n});\n\n// ── Error handling ──────────────────────────────────────\n\ntest('component that throws propagates the error', t => {\n\tfunction Broken(): React.JSX.Element {\n\t\tthrow new Error('Component error');\n\t}\n\n\tt.throws(() => renderToString(<Broken />), {message: 'Component error'});\n});\n\ntest('text outside Text component throws', t => {\n\tt.throws(() => renderToString(<Box>{'raw text'}</Box>), {\n\t\tmessage: /must be rendered inside <Text>/,\n\t});\n});\n\ntest('subsequent calls work after a component error', t => {\n\tfunction Broken(): React.JSX.Element {\n\t\tthrow new Error('Boom');\n\t}\n\n\tt.throws(() => renderToString(<Broken />));\n\tconst output = renderToString(<Text>Still works</Text>);\n\tt.is(output, 'Still works');\n});\n\n// ── Independence ────────────────────────────────────────\n\ntest('can be called multiple times independently', t => {\n\tconst output1 = renderToString(<Text>First</Text>);\n\tconst output2 = renderToString(<Text>Second</Text>);\n\n\tt.is(output1, 'First');\n\tt.is(output2, 'Second');\n});\n\n// ── Deeply nested tree ──────────────────────────────────\n\ntest('render deeply nested component tree', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Box paddingLeft={1}>\n\t\t\t\t<Box>\n\t\t\t\t\t<Text bold>\n\t\t\t\t\t\t{'Nested '}\n\t\t\t\t\t\t<Text color=\"green\">deep</Text>\n\t\t\t\t\t</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.true(output.includes('Nested'));\n\tt.true(output.includes('deep'));\n});\n"
  },
  {
    "path": "test/render.tsx",
    "content": "import process from 'node:process';\nimport vm from 'node:vm';\nimport {spawn as spawnProcess} from 'node:child_process';\nimport {PassThrough, Writable} from 'node:stream';\nimport {Buffer} from 'node:buffer';\nimport url from 'node:url';\nimport * as path from 'node:path';\nimport {createRequire} from 'node:module';\nimport FakeTimers from '@sinonjs/fake-timers';\nimport {stub} from 'sinon';\nimport test, {type ExecutionContext} from 'ava';\nimport React, {\n\ttype ReactElement,\n\ttype ReactNode,\n\tPureComponent,\n\tuseEffect,\n\tuseState,\n} from 'react';\nimport ansiEscapes from 'ansi-escapes';\nimport stripAnsi from 'strip-ansi';\nimport boxen from 'boxen';\nimport delay from 'delay';\nimport {render, Box, Text, useApp, useCursor, useInput} from '../src/index.js';\nimport {type RenderMetrics} from '../src/ink.js';\nimport {bsu, esu} from '../src/write-synchronized.js';\nimport {createStdin, emitReadable} from './helpers/create-stdin.js';\nimport createStdout from './helpers/create-stdout.js';\n\nconst require = createRequire(import.meta.url);\n\n// eslint-disable-next-line @typescript-eslint/consistent-type-imports\nconst {spawn} = require('node-pty') as typeof import('node-pty');\n\nconst __dirname = url.fileURLToPath(new URL('.', import.meta.url));\n\nconst term = (fixture: string, args: string[] = []) => {\n\tlet resolve: (value?: unknown) => void;\n\tlet reject: (error: Error) => void;\n\n\t// eslint-disable-next-line promise/param-names\n\tconst exitPromise = new Promise((resolve2, reject2) => {\n\t\tresolve = resolve2;\n\t\treject = reject2;\n\t});\n\n\tconst env = {\n\t\t...process.env,\n\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\tNODE_NO_WARNINGS: '1',\n\t};\n\n\tconst ps = spawn(\n\t\t'node',\n\t\t[\n\t\t\t'--import=tsx',\n\t\t\tpath.join(__dirname, `./fixtures/${fixture}.tsx`),\n\t\t\t...args,\n\t\t],\n\t\t{\n\t\t\tname: 'xterm-color',\n\t\t\tcols: 100,\n\t\t\tcwd: __dirname,\n\t\t\tenv,\n\t\t},\n\t);\n\n\tconst result = {\n\t\twrite(input: string) {\n\t\t\tps.write(input);\n\t\t},\n\t\toutput: '',\n\t\twaitForExit: async () => exitPromise,\n\t};\n\n\tps.onData(data => {\n\t\t// Strip Synchronized Update Mode sequences (bsu/esu) so tests\n\t\t// only see the actual content, not the transport wrapper.\n\t\tresult.output += data\n\t\t\t.replaceAll('\\u001B[?2026h', '')\n\t\t\t.replaceAll('\\u001B[?2026l', '');\n\t});\n\n\tps.onExit(({exitCode}) => {\n\t\tif (exitCode === 0) {\n\t\t\tresolve();\n\t\t\treturn;\n\t\t}\n\n\t\treject(new Error(`Process exited with non-zero exit code: ${exitCode}`));\n\t});\n\n\treturn result;\n};\n\nconst countOccurrences = (text: string, searchValue: string): number => {\n\tif (searchValue === '') {\n\t\treturn 0;\n\t}\n\n\treturn text.split(searchValue).length - 1;\n};\n\nconst isWriteBarrierChunk = (chunk: string | Uint8Array): boolean =>\n\t(typeof chunk === 'string' && chunk === '') ||\n\t(chunk instanceof Uint8Array && chunk.length === 0);\n\nconst toRenderedChunk = (chunk: string | Uint8Array): string =>\n\tstripAnsi(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString());\n\nconst isCursorOrSyncEscape = (chunk: string | Uint8Array): boolean => {\n\tconst str = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString();\n\treturn str.startsWith('\\u001B[?25') || str === bsu || str === esu;\n};\n\nconst isRenderContent = (chunk: string | Uint8Array): boolean =>\n\t!isWriteBarrierChunk(chunk) && !isCursorOrSyncEscape(chunk);\n\nconst getContentWrites = (writeSpy: any): string[] =>\n\t(writeSpy.args as string[][])\n\t\t.map((args: string[]) => args[0]!)\n\t\t.filter((w: string) => isRenderContent(w));\n\nconst createDelayedWriteCallbackStdout = ({\n\tshouldDelay,\n\tonDelayElapsed,\n\tdelayMs = 150,\n}: {\n\treadonly shouldDelay: (chunk: string | Uint8Array) => boolean;\n\treadonly onDelayElapsed: () => void;\n\treadonly delayMs?: number;\n}): NodeJS.WriteStream => {\n\tlet didDelayOnce = false;\n\n\tconst stdout = new Writable({\n\t\twrite(\n\t\t\tchunk: string | Uint8Array,\n\t\t\t_encoding: BufferEncoding,\n\t\t\tcallback: (error?: Error) => void,\n\t\t) {\n\t\t\tif (!didDelayOnce && shouldDelay(chunk)) {\n\t\t\t\tdidDelayOnce = true;\n\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tonDelayElapsed();\n\t\t\t\t\tcallback();\n\t\t\t\t}, delayMs);\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcallback();\n\t\t},\n\t}) as unknown as NodeJS.WriteStream;\n\n\tstdout.columns = 100;\n\tstdout.isTTY = true;\n\treturn stdout;\n};\n\ntype Issue450Fixture =\n\t| 'issue-450-full-height-rerender'\n\t| 'issue-450-full-height-rerender-with-marker'\n\t| 'issue-450-height-minus-one-rerender'\n\t| 'issue-450-full-height-with-static-rerender'\n\t| 'issue-450-initial-overflow'\n\t| 'issue-450-initial-fullscreen'\n\t| 'issue-450-grow-to-fullscreen-rerender'\n\t| 'issue-450-shrink-from-fullscreen-rerender'\n\t| 'issue-450-shrink-from-overflow-rerender'\n\t| 'issue-450-static-shrink-from-fullscreen-rerender';\n\nconst runIssue450Fixture = async (\n\tfixture: Issue450Fixture,\n\trows = 6,\n): Promise<string> => {\n\tconst processResult = term(fixture, [String(rows)]);\n\tawait processResult.waitForExit();\n\treturn processResult.output;\n};\n\nconst runNonTtyFixture = async (\n\tfixture: string,\n\targs: string[] = [],\n): Promise<string> => {\n\tlet output = '';\n\tlet errorOutput = '';\n\tconst env = {\n\t\t...process.env,\n\t\t// eslint-disable-next-line @typescript-eslint/naming-convention\n\t\tNODE_NO_WARNINGS: '1',\n\t};\n\t// Force non-CI code path while still using a non-TTY stdout stream.\n\tenv.CI = 'false';\n\n\tconst fixtureProcess = spawnProcess(\n\t\t'node',\n\t\t[\n\t\t\t'--import=tsx',\n\t\t\tpath.join(__dirname, `./fixtures/${fixture}.tsx`),\n\t\t\t...args,\n\t\t],\n\t\t{\n\t\t\tcwd: __dirname,\n\t\t\tenv,\n\t\t\tstdio: ['ignore', 'pipe', 'pipe'],\n\t\t},\n\t);\n\n\tfixtureProcess.stdout.on('data', (data: Uint8Array | string) => {\n\t\toutput += typeof data === 'string' ? data : data.toString();\n\t});\n\n\tfixtureProcess.stderr.on('data', (data: Uint8Array | string) => {\n\t\terrorOutput += typeof data === 'string' ? data : data.toString();\n\t});\n\n\tconst exitCode = await new Promise<number>((resolve, reject) => {\n\t\tfixtureProcess.on('error', reject);\n\t\tfixtureProcess.on('close', code => {\n\t\t\tresolve(code ?? 0);\n\t\t});\n\t});\n\n\tif (exitCode !== 0) {\n\t\tthrow new Error(\n\t\t\t`Non-TTY fixture exited with code ${exitCode}: ${errorOutput}`,\n\t\t);\n\t}\n\n\treturn output;\n};\n\ntype Issue450FixtureResult = {\n\toutput: string;\n\tclearTerminalCount: number;\n\teraseLineCount: number;\n};\n\nconst getIssue450ControlSequenceCounts = (output: string) => ({\n\tclearTerminalCount: countOccurrences(output, ansiEscapes.clearTerminal),\n\teraseLineCount: countOccurrences(output, ansiEscapes.eraseLines(1)),\n});\n\nconst runIssue450FixtureWithCounts = async (\n\tfixture: Issue450Fixture,\n\trows = 6,\n): Promise<Issue450FixtureResult> => {\n\tconst output = await runIssue450Fixture(fixture, rows);\n\tconst {clearTerminalCount, eraseLineCount} =\n\t\tgetIssue450ControlSequenceCounts(output);\n\n\treturn {\n\t\toutput,\n\t\tclearTerminalCount,\n\t\teraseLineCount,\n\t};\n};\n\nconst getOutputBeforeMarker = (\n\tt: ExecutionContext,\n\toutput: string,\n\tmarker: string,\n): string => {\n\tconst markerIndex = output.indexOf(marker);\n\tt.true(markerIndex >= 0, `Fixture marker \"${marker}\" should be present`);\n\treturn markerIndex >= 0 ? output.slice(0, markerIndex) : output;\n};\n\nconst runIssue450FixtureBeforeMarker = async (\n\tt: ExecutionContext,\n\tfixture: Issue450Fixture,\n\tmarker: string,\n\trows = 6,\n): Promise<string> => {\n\tconst output = await runIssue450Fixture(fixture, rows);\n\treturn getOutputBeforeMarker(t, output, marker);\n};\n\nconst assertIssue450DynamicFrameOutput = (\n\tt: ExecutionContext,\n\toutput: string,\n): void => {\n\tt.true(\n\t\toutput.includes('frame 8'),\n\t\t'Fixture should render multiple dynamic frames',\n\t);\n};\n\nclass SynchronousErrorBoundary extends PureComponent<\n\t{\n\t\tonError: (error: Error) => void;\n\t\tchildren?: ReactElement;\n\t},\n\t{error?: Error}\n> {\n\tstatic displayName = 'SynchronousErrorBoundary';\n\n\tstatic override getDerivedStateFromError(error: Error) {\n\t\treturn {error};\n\t}\n\n\toverride state: {error?: Error} = {\n\t\terror: undefined,\n\t};\n\n\toverride componentDidCatch(error: Error) {\n\t\tthis.props.onError(error);\n\t}\n\n\toverride render() {\n\t\tif (this.state.error) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.props.children;\n\t}\n}\n\nfunction SynchronousRenderErrorComponent() {\n\tthrow new Error('Synchronous render error');\n}\n\nfunction ThrowingComponentWithBoundary() {\n\tconst {exit} = useApp();\n\n\treturn (\n\t\t<SynchronousErrorBoundary onError={exit}>\n\t\t\t<SynchronousRenderErrorComponent />\n\t\t</SynchronousErrorBoundary>\n\t);\n}\n\ntest.serial('do not erase screen', async t => {\n\tconst ps = term('erase', ['4']);\n\tawait ps.waitForExit();\n\tt.false(ps.output.includes(ansiEscapes.clearTerminal));\n\n\tfor (const letter of ['A', 'B', 'C']) {\n\t\tt.true(ps.output.includes(letter));\n\t}\n});\n\ntest.serial(\n\t'do not erase screen where <Static> is taller than viewport',\n\tasync t => {\n\t\tconst ps = term('erase-with-static', ['4']);\n\n\t\tawait ps.waitForExit();\n\t\tt.false(ps.output.includes(ansiEscapes.clearTerminal));\n\n\t\tfor (const letter of ['A', 'B', 'C', 'D', 'E', 'F']) {\n\t\t\tt.true(ps.output.includes(letter));\n\t\t}\n\t},\n);\n\ntest.serial('erase screen', async t => {\n\tconst ps = term('erase', ['3']);\n\tawait ps.waitForExit();\n\tt.true(ps.output.includes(ansiEscapes.clearTerminal));\n\n\tfor (const letter of ['A', 'B', 'C']) {\n\t\tt.true(ps.output.includes(letter));\n\t}\n});\n\ntest.serial(\n\t'erase screen where <Static> exists but interactive part is taller than viewport',\n\tasync t => {\n\t\tconst ps = term('erase', ['3']);\n\t\tawait ps.waitForExit();\n\t\tt.true(ps.output.includes(ansiEscapes.clearTerminal));\n\n\t\tfor (const letter of ['A', 'B', 'C']) {\n\t\t\tt.true(ps.output.includes(letter));\n\t\t}\n\t},\n);\n\ntest.serial('erase screen where state changes', async t => {\n\tconst ps = term('erase-with-state-change', ['4']);\n\tawait ps.waitForExit();\n\n\t// The final frame is between the last eraseLines sequence and cursorShow\n\t// Split on cursorShow to isolate the final rendered content before the cursor is shown\n\tconst beforeCursorShow = ps.output.split(ansiEscapes.cursorShow)[0];\n\tif (!beforeCursorShow) {\n\t\tt.fail('beforeCursorShow is undefined');\n\t\treturn;\n\t}\n\n\t// Find the last occurrence of an eraseLines sequence\n\t// eraseLines(1) is the minimal erase pattern used by Ink\n\tconst eraseLinesPattern = ansiEscapes.eraseLines(1);\n\tconst lastEraseIndex = beforeCursorShow.lastIndexOf(eraseLinesPattern);\n\n\tconst lastFrame =\n\t\tlastEraseIndex === -1\n\t\t\t? beforeCursorShow\n\t\t\t: beforeCursorShow.slice(lastEraseIndex + eraseLinesPattern.length);\n\n\tconst lastFrameContent = stripAnsi(lastFrame);\n\n\tfor (const letter of ['A', 'B', 'C']) {\n\t\tt.false(lastFrameContent.includes(letter));\n\t}\n});\n\ntest.serial('erase screen where state changes in small viewport', async t => {\n\tconst ps = term('erase-with-state-change', ['3']);\n\tawait ps.waitForExit();\n\n\tconst frames = ps.output.split(ansiEscapes.clearTerminal);\n\tconst lastFrame = frames.at(-1);\n\n\tfor (const letter of ['A', 'B', 'C']) {\n\t\tt.false(lastFrame?.includes(letter));\n\t}\n});\n\ntest.serial(\n\t'fullscreen mode should not add extra newline at the bottom',\n\tasync t => {\n\t\tconst ps = term('fullscreen-no-extra-newline', ['5']);\n\t\tawait ps.waitForExit();\n\n\t\tt.true(ps.output.includes('Bottom line'));\n\n\t\tconst lastFrame = ps.output.split(ansiEscapes.clearTerminal).at(-1) ?? '';\n\n\t\t// Check that the bottom line is at the end without extra newlines\n\t\t// In a 5-line terminal:\n\t\t// Line 1: Fullscreen: top\n\t\t// Lines 2-4: empty (from flexGrow)\n\t\t// Line 5: Bottom line (should be usable)\n\t\tconst lines = lastFrame.split('\\n');\n\n\t\tt.is(lines.length, 5, 'Should have exactly 5 lines for 5-row terminal');\n\n\t\tt.true(\n\t\t\tlines[4]?.includes('Bottom line') ?? false,\n\t\t\t'Bottom line should be on line 5',\n\t\t);\n\t},\n);\n\ntest.serial(\n\t'#442: full terminal-size box should not add an extra scroll line',\n\tasync t => {\n\t\tconst rows = 5;\n\t\tconst ps = term('issue-442-full-height', [String(rows)]);\n\t\tawait ps.waitForExit();\n\n\t\tconst lastFrame = ps.output.split(ansiEscapes.clearTerminal).at(-1) ?? '';\n\t\tconst lastFrameContent = stripAnsi(lastFrame);\n\t\tconst lines = lastFrameContent.split('\\n');\n\n\t\tt.false(\n\t\t\tlastFrameContent.endsWith('\\n'),\n\t\t\t'Should not end with a trailing newline in fullscreen mode',\n\t\t);\n\t\tt.is(\n\t\t\tlines.length,\n\t\t\trows,\n\t\t\t'Should render exactly terminal row count without an extra line',\n\t\t);\n\t\tt.true(lines.at(-1)?.includes('#442 bottom') ?? false);\n\t},\n);\n\ntest.serial(\n\t'#450: full-height rerenders should not repeatedly clear terminal',\n\tasync t => {\n\t\tconst {output, clearTerminalCount, eraseLineCount} =\n\t\t\tawait runIssue450FixtureWithCounts('issue-450-full-height-rerender');\n\n\t\tassertIssue450DynamicFrameOutput(t, output);\n\t\tt.true(\n\t\t\tclearTerminalCount <= 1,\n\t\t\t`Expected at most one clearTerminal sequence, received ${clearTerminalCount}`,\n\t\t);\n\t\tt.true(\n\t\t\teraseLineCount > 0,\n\t\t\t'Expected incremental erase sequences for fullscreen rerenders',\n\t\t);\n\t},\n);\n\ntest.serial(\n\t'#450: initial overflowing frame should not clear terminal',\n\tasync t => {\n\t\tconst renderedMarker = '__INITIAL_OVERFLOW_FRAME_RENDERED__';\n\t\tconst outputBeforeMarker = await runIssue450FixtureBeforeMarker(\n\t\t\tt,\n\t\t\t'issue-450-initial-overflow',\n\t\t\trenderedMarker,\n\t\t\t3,\n\t\t);\n\n\t\tt.false(\n\t\t\toutputBeforeMarker.includes(ansiEscapes.clearTerminal),\n\t\t\t'Initial overflowing render should not clear terminal',\n\t\t);\n\t},\n);\n\ntest.serial(\n\t'#450: initial full-height frame should not clear terminal',\n\tasync t => {\n\t\tconst renderedMarker = '__INITIAL_FULLSCREEN_FRAME_RENDERED__';\n\t\tconst outputBeforeMarker = await runIssue450FixtureBeforeMarker(\n\t\t\tt,\n\t\t\t'issue-450-initial-fullscreen',\n\t\t\trenderedMarker,\n\t\t\t3,\n\t\t);\n\n\t\tt.false(\n\t\t\toutputBeforeMarker.includes(ansiEscapes.clearTerminal),\n\t\t\t'Initial full-height render should not clear terminal',\n\t\t);\n\t},\n);\n\ntest.serial(\n\t'#450 control: rows - 1 rerenders should avoid clearTerminal',\n\tasync t => {\n\t\tconst {output, clearTerminalCount, eraseLineCount} =\n\t\t\tawait runIssue450FixtureWithCounts('issue-450-height-minus-one-rerender');\n\n\t\tassertIssue450DynamicFrameOutput(t, output);\n\t\tt.is(clearTerminalCount, 0);\n\t\tt.true(\n\t\t\teraseLineCount > 0,\n\t\t\t'Expected incremental erase sequences for non-fullscreen rerenders',\n\t\t);\n\t},\n);\n\ntest.serial(\n\t'#450: full-height rerenders should not clear before unmount',\n\tasync t => {\n\t\tconst renderedMarker = '__FULL_HEIGHT_RERENDER_COMPLETED__';\n\t\tconst outputBeforeMarker = await runIssue450FixtureBeforeMarker(\n\t\t\tt,\n\t\t\t'issue-450-full-height-rerender-with-marker',\n\t\t\trenderedMarker,\n\t\t);\n\t\tconst {clearTerminalCount} =\n\t\t\tgetIssue450ControlSequenceCounts(outputBeforeMarker);\n\n\t\tassertIssue450DynamicFrameOutput(t, outputBeforeMarker);\n\t\tt.is(clearTerminalCount, 0);\n\t},\n);\n\ntest.serial(\n\t'#450: grow from rows - 1 to full-height should not clear before unmount',\n\tasync t => {\n\t\tconst renderedMarker = '__GROW_TO_FULLSCREEN_RERENDER_COMPLETED__';\n\t\tconst outputBeforeMarker = await runIssue450FixtureBeforeMarker(\n\t\t\tt,\n\t\t\t'issue-450-grow-to-fullscreen-rerender',\n\t\t\trenderedMarker,\n\t\t);\n\t\tconst {clearTerminalCount} =\n\t\t\tgetIssue450ControlSequenceCounts(outputBeforeMarker);\n\n\t\tassertIssue450DynamicFrameOutput(t, outputBeforeMarker);\n\t\tt.is(clearTerminalCount, 0);\n\t},\n);\n\ntest.serial(\n\t'#450: shrink from full-height to rows - 1 should clear exactly once',\n\tasync t => {\n\t\tconst {output, clearTerminalCount} = await runIssue450FixtureWithCounts(\n\t\t\t'issue-450-shrink-from-fullscreen-rerender',\n\t\t);\n\n\t\tassertIssue450DynamicFrameOutput(t, output);\n\t\tt.is(clearTerminalCount, 1);\n\t},\n);\n\ntest.serial(\n\t'#450: shrink from overflow to rows - 1 should clear exactly once',\n\tasync t => {\n\t\tconst {output, clearTerminalCount} = await runIssue450FixtureWithCounts(\n\t\t\t'issue-450-shrink-from-overflow-rerender',\n\t\t);\n\n\t\tassertIssue450DynamicFrameOutput(t, output);\n\t\tt.is(clearTerminalCount, 1);\n\t},\n);\n\ntest.serial(\n\t'#450: <Static> with shrink from full-height should clear exactly once',\n\tasync t => {\n\t\tconst {output, clearTerminalCount} = await runIssue450FixtureWithCounts(\n\t\t\t'issue-450-static-shrink-from-fullscreen-rerender',\n\t\t);\n\n\t\tt.true(output.includes('#450 static line'));\n\t\tassertIssue450DynamicFrameOutput(t, output);\n\t\tt.is(clearTerminalCount, 1);\n\t},\n);\n\ntest.serial(\n\t'#450: non-TTY full-height rerenders should never clear terminal',\n\tt => {\n\t\tconst rows = 6;\n\t\tconst stdout = createStdout();\n\t\tstdout.rows = rows;\n\t\tconst writes = captureWrites(stdout);\n\n\t\tfunction NonTtyRerenderTestComponent({\n\t\t\tframeCount,\n\t\t}: {\n\t\t\treadonly frameCount: number;\n\t\t}) {\n\t\t\treturn (\n\t\t\t\t<Box height={rows} flexDirection=\"column\">\n\t\t\t\t\t<Text>#450 top</Text>\n\t\t\t\t\t<Box flexGrow={1}>\n\t\t\t\t\t\t<Text>{`frame ${frameCount}`}</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t\t<Text>#450 bottom</Text>\n\t\t\t\t</Box>\n\t\t\t);\n\t\t}\n\n\t\tconst {rerender, unmount} = render(\n\t\t\t<NonTtyRerenderTestComponent frameCount={0} />,\n\t\t\t{stdout},\n\t\t);\n\n\t\trerender(<NonTtyRerenderTestComponent frameCount={1} />);\n\t\trerender(<NonTtyRerenderTestComponent frameCount={2} />);\n\n\t\tconst {clearTerminalCount} = getIssue450ControlSequenceCounts(\n\t\t\twrites.join(''),\n\t\t);\n\t\tt.is(clearTerminalCount, 0);\n\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'#450: non-TTY overflow transitions should never clear terminal',\n\tt => {\n\t\tconst rows = 3;\n\t\tconst stdout = createStdout();\n\t\tstdout.rows = rows;\n\t\tconst writes = captureWrites(stdout);\n\n\t\tfunction NonTtyOverflowTransitionTestComponent({\n\t\t\tlineCount,\n\t\t}: {\n\t\t\treadonly lineCount: number;\n\t\t}) {\n\t\t\tconst lines = [];\n\t\t\tfor (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {\n\t\t\t\tlines.push(<Text key={lineNumber}>{`line ${lineNumber}`}</Text>);\n\t\t\t}\n\n\t\t\treturn <Box flexDirection=\"column\">{lines}</Box>;\n\t\t}\n\n\t\tconst {rerender, unmount} = render(\n\t\t\t<NonTtyOverflowTransitionTestComponent lineCount={2} />,\n\t\t\t{stdout},\n\t\t);\n\n\t\trerender(<NonTtyOverflowTransitionTestComponent lineCount={4} />);\n\n\t\tconst {clearTerminalCount} = getIssue450ControlSequenceCounts(\n\t\t\twrites.join(''),\n\t\t);\n\t\tt.is(clearTerminalCount, 0);\n\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'#450: viewport shrink into overflow should clear once',\n\tasync t => {\n\t\tconst rows = 6;\n\t\tconst stdout = createTtyStdout();\n\t\tstdout.rows = rows;\n\t\tconst writes = captureWrites(stdout);\n\n\t\tfunction ResizeBoundaryTestComponent() {\n\t\t\treturn (\n\t\t\t\t<Box height={rows} flexDirection=\"column\">\n\t\t\t\t\t<Text>#450 top</Text>\n\t\t\t\t\t<Box flexGrow={1}>\n\t\t\t\t\t\t<Text>#450 middle</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t\t<Text>#450 bottom</Text>\n\t\t\t\t</Box>\n\t\t\t);\n\t\t}\n\n\t\tconst {unmount} = render(<ResizeBoundaryTestComponent />, {stdout});\n\n\t\twrites.length = 0;\n\t\tstdout.rows = rows - 1;\n\t\tstdout.emit('resize');\n\t\tawait delay(0);\n\n\t\tconst {clearTerminalCount} = getIssue450ControlSequenceCounts(\n\t\t\twrites.join(''),\n\t\t);\n\t\tt.is(clearTerminalCount, 1);\n\n\t\tunmount();\n\t},\n);\n\ntest.serial(\n\t'#450: non-TTY grow-to-overflow rerender should not clear terminal',\n\tasync t => {\n\t\tconst output = await runNonTtyFixture(\n\t\t\t'issue-450-grow-to-overflow-rerender',\n\t\t\t['3'],\n\t\t);\n\t\tt.false(output.includes(ansiEscapes.clearTerminal));\n\t},\n);\n\ntest.serial('#725: non-TTY child process output is flushed', async t => {\n\tconst output = await runNonTtyFixture('issue-725-child-process');\n\tconst plainOutput = stripAnsi(output);\n\n\tt.true(plainOutput.includes('ready-stdin-not-tty'));\n\tt.true(plainOutput.includes('exited'));\n});\n\ntest.serial(\n\t'#450: full-height rerenders with <Static> should not repeatedly clear terminal',\n\tasync t => {\n\t\tconst {output, clearTerminalCount, eraseLineCount} =\n\t\t\tawait runIssue450FixtureWithCounts(\n\t\t\t\t'issue-450-full-height-with-static-rerender',\n\t\t\t);\n\n\t\tt.true(\n\t\t\toutput.includes('#450 static line'),\n\t\t\t'Fixture should emit static output',\n\t\t);\n\t\tassertIssue450DynamicFrameOutput(t, output);\n\t\tt.true(\n\t\t\tclearTerminalCount <= 1,\n\t\t\t`Expected at most one clearTerminal sequence, received ${clearTerminalCount}`,\n\t\t);\n\t\tt.true(\n\t\t\teraseLineCount > 0,\n\t\t\t'Expected incremental erase sequences for fullscreen rerenders',\n\t\t);\n\t},\n);\n\ntest.serial('clear output', async t => {\n\tconst ps = term('clear');\n\tawait ps.waitForExit();\n\n\tconst secondFrame = ps.output.split(ansiEscapes.eraseLines(4))[1];\n\n\tfor (const letter of ['A', 'B', 'C']) {\n\t\tt.false(secondFrame?.includes(letter));\n\t}\n});\n\ntest.serial(\n\t'intercept console methods and display result above output',\n\tasync t => {\n\t\tconst ps = term('console');\n\t\tawait ps.waitForExit();\n\n\t\tconst frames = ps.output.split(ansiEscapes.eraseLines(2)).map(line => {\n\t\t\treturn stripAnsi(line);\n\t\t});\n\n\t\tt.deepEqual(frames, [\n\t\t\t'Hello World\\r\\n',\n\t\t\t'First log\\r\\nHello World\\r\\nSecond log\\r\\n',\n\t\t]);\n\t},\n);\n\ntest.serial('rerender on resize', async t => {\n\tconst stdout = createStdout(10);\n\n\tfunction Test() {\n\t\treturn (\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>Test</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {unmount} = render(<Test />, {stdout});\n\n\tconst contentWrites = getContentWrites(stdout.write);\n\tt.is(\n\t\tstripAnsi(contentWrites[0]!),\n\t\tboxen('Test'.padEnd(8), {borderStyle: 'round'}) + '\\n',\n\t);\n\n\tt.is(stdout.listeners('resize').length, 1);\n\n\tstdout.columns = 8;\n\tstdout.emit('resize');\n\tawait delay(100);\n\n\tconst contentWritesAfterResize = getContentWrites(stdout.write);\n\tt.is(\n\t\tstripAnsi(contentWritesAfterResize.at(-1)!),\n\t\tboxen('Test'.padEnd(6), {borderStyle: 'round'}) + '\\n',\n\t);\n\n\tunmount();\n\tt.is(stdout.listeners('resize').length, 0);\n});\n\nfunction ThrottleTestComponent({text}: {readonly text: string}) {\n\treturn <Text>{text}</Text>;\n}\n\nfunction ThrottleCursorTestComponent({text}: {readonly text: string}) {\n\tconst {setCursorPosition} = useCursor();\n\tsetCursorPosition({x: 0, y: 0});\n\treturn <Text>{text}</Text>;\n}\n\ntest.serial('throttle renders to maxFps', t => {\n\tconst clock = FakeTimers.install(); // Controls timers + Date.now()\n\ttry {\n\t\tconst stdout = createStdout();\n\n\t\tconst {unmount, rerender} = render(<ThrottleTestComponent text=\"Hello\" />, {\n\t\t\tstdout,\n\t\t\tmaxFps: 1, // 1 Hz => ~1000 ms window\n\t\t});\n\n\t\t// Initial render (leading call)\n\t\tt.is(getContentWrites(stdout.write).length, 1);\n\t\tt.is(stripAnsi(getContentWrites(stdout.write)[0]!), 'Hello\\n');\n\n\t\t// Trigger another render inside the throttle window\n\t\trerender(<ThrottleTestComponent text=\"World\" />);\n\t\tt.is(getContentWrites(stdout.write).length, 1);\n\n\t\t// Advance 999 ms: still within window, no trailing call yet\n\t\tclock.tick(999);\n\t\tt.is(getContentWrites(stdout.write).length, 1);\n\n\t\t// Cross the boundary: trailing render fires once\n\t\tclock.tick(1);\n\t\tt.is(getContentWrites(stdout.write).length, 2);\n\t\tt.is(stripAnsi(getContentWrites(stdout.write)[1]!), 'World\\n');\n\n\t\tunmount();\n\t} finally {\n\t\tclock.uninstall();\n\t}\n});\n\ntest.serial('outputs renderTime when onRender is passed', async t => {\n\tconst renderTimes: number[] = [];\n\tconst funcObj = {\n\t\tonRender(metrics: RenderMetrics) {\n\t\t\tconst {renderTime} = metrics;\n\t\t\trenderTimes.push(renderTime);\n\t\t},\n\t};\n\n\tconst onRenderStub = stub(funcObj, 'onRender').callThrough();\n\n\tfunction Test({children}: {readonly children?: ReactNode}) {\n\t\tconst [text, setText] = useState('Test');\n\n\t\tuseInput(input => {\n\t\t\tsetText(input);\n\t\t});\n\n\t\treturn (\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>{text}</Text>\n\t\t\t\t{children}\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst stdin = createStdin();\n\tconst {unmount, rerender} = render(<Test />, {\n\t\tonRender: onRenderStub,\n\t\tstdin,\n\t});\n\n\t// Initial render\n\tt.is(onRenderStub.callCount, 1);\n\tt.true(renderTimes[0] >= 0);\n\n\t// Manual rerender\n\tonRenderStub.resetHistory();\n\trerender(\n\t\t<Test>\n\t\t\t<Text>Updated</Text>\n\t\t</Test>,\n\t);\n\tawait delay(100);\n\tt.is(onRenderStub.callCount, 1);\n\tt.true(renderTimes[1] >= 0);\n\n\t// Internal state update via useInput\n\tonRenderStub.resetHistory();\n\temitReadable(stdin, 'a');\n\tawait delay(100);\n\tt.is(onRenderStub.callCount, 1);\n\tt.true(renderTimes[2] >= 0);\n\n\t// Verify all renders were tracked\n\tt.is(renderTimes.length, 3);\n\n\tunmount();\n});\n\ntest.serial('no throttled renders after unmount', t => {\n\tconst clock = FakeTimers.install();\n\ttry {\n\t\tconst stdout = createStdout();\n\n\t\tconst {unmount, rerender} = render(<ThrottleTestComponent text=\"Foo\" />, {\n\t\t\tstdout,\n\t\t});\n\n\t\tt.is(getContentWrites(stdout.write).length, 1);\n\n\t\trerender(<ThrottleTestComponent text=\"Bar\" />);\n\t\trerender(<ThrottleTestComponent text=\"Baz\" />);\n\t\tunmount();\n\n\t\tconst contentCountAfterUnmount = getContentWrites(stdout.write).length;\n\n\t\t// Regression test for https://github.com/vadimdemedes/ink/issues/692\n\t\tclock.tick(1000);\n\t\tt.is(getContentWrites(stdout.write).length, contentCountAfterUnmount);\n\t} finally {\n\t\tclock.uninstall();\n\t}\n});\n\ntest.serial('unmount forces pending throttled render', t => {\n\tconst clock = FakeTimers.install();\n\ttry {\n\t\tconst stdout = createStdout();\n\n\t\tconst {unmount, rerender} = render(<ThrottleTestComponent text=\"Hello\" />, {\n\t\t\tstdout,\n\t\t\tmaxFps: 1, // 1 Hz => ~1000 ms throttle window\n\t\t});\n\n\t\t// Initial render (leading call)\n\t\tt.is(getContentWrites(stdout.write).length, 1);\n\t\tt.is(stripAnsi(getContentWrites(stdout.write)[0]!), 'Hello\\n');\n\n\t\t// Trigger another render inside the throttle window\n\t\trerender(<ThrottleTestComponent text=\"Final\" />);\n\t\t// Not rendered yet due to throttling\n\t\tt.is(getContentWrites(stdout.write).length, 1);\n\n\t\t// Unmount should flush the pending render so the final frame is visible\n\t\tunmount();\n\n\t\t// The final frame should have been rendered\n\t\tconst allContentWrites = getContentWrites(stdout.write).map((w: string) =>\n\t\t\tstripAnsi(w),\n\t\t);\n\t\tt.true(allContentWrites.some((call: string) => call.includes('Final')));\n\t} finally {\n\t\tclock.uninstall();\n\t}\n});\n\ntest.serial(\n\t'should reject waitUntilExit when app exits during synchronous render error handling',\n\tasync t => {\n\t\tconst stdout = createStdout();\n\t\tconst {waitUntilExit} = render(<ThrowingComponentWithBoundary />, {\n\t\t\tstdout,\n\t\t\tpatchConsole: false,\n\t\t});\n\n\t\tawait t.throwsAsync(\n\t\t\tPromise.race([\n\t\t\t\twaitUntilExit(),\n\t\t\t\tdelay(500).then(() => {\n\t\t\t\t\tthrow new Error('waitUntilExit did not settle');\n\t\t\t\t}),\n\t\t\t]),\n\t\t\t{\n\t\t\t\tmessage: 'Synchronous render error',\n\t\t\t},\n\t\t);\n\t},\n);\n\ntest.serial('waitUntilExit resolves after stdout write callback', async t => {\n\tlet writeCallbackFired = false;\n\n\tconst stdout = new Writable({\n\t\twrite(_chunk, _encoding, callback) {\n\t\t\tsetTimeout(() => {\n\t\t\t\twriteCallbackFired = true;\n\t\t\t\tcallback();\n\t\t\t}, 150);\n\t\t},\n\t}) as unknown as NodeJS.WriteStream;\n\n\tstdout.columns = 100;\n\n\tconst {unmount, waitUntilExit} = render(<Text>Hello</Text>, {stdout});\n\tconst exitPromise = waitUntilExit();\n\n\tunmount();\n\tawait exitPromise;\n\n\tt.true(writeCallbackFired);\n});\n\ntest.serial(\n\t'createDelayedWriteCallbackStdout delays only the first matching chunk',\n\tasync t => {\n\t\tlet delayCount = 0;\n\n\t\tconst stdout = createDelayedWriteCallbackStdout({\n\t\t\tshouldDelay(chunk) {\n\t\t\t\treturn !isWriteBarrierChunk(chunk);\n\t\t\t},\n\t\t\tonDelayElapsed() {\n\t\t\t\tdelayCount++;\n\t\t\t},\n\t\t\tdelayMs: 80,\n\t\t});\n\n\t\tconst writeChunk = async (chunk: string | Uint8Array): Promise<void> =>\n\t\t\tnew Promise<void>(resolve => {\n\t\t\t\tstdout.write(chunk, () => {\n\t\t\t\t\tresolve();\n\t\t\t\t});\n\t\t\t});\n\n\t\tawait writeChunk('');\n\t\tt.is(delayCount, 0);\n\n\t\tlet didDelayedWriteResolve = false;\n\t\tconst delayedWritePromise = (async () => {\n\t\t\tawait writeChunk('Hello');\n\t\t\tdidDelayedWriteResolve = true;\n\t\t})();\n\n\t\tawait delay(20);\n\t\tt.false(didDelayedWriteResolve);\n\t\tawait delayedWritePromise;\n\t\tt.is(delayCount, 1);\n\n\t\tlet didImmediateWriteResolve = false;\n\t\tconst immediateWritePromise = (async () => {\n\t\t\tawait writeChunk('World');\n\t\t\tdidImmediateWriteResolve = true;\n\t\t})();\n\n\t\tawait delay(0);\n\t\tt.true(didImmediateWriteResolve);\n\t\tawait immediateWritePromise;\n\t\tt.is(delayCount, 1);\n\t},\n);\n\ntest.serial(\n\t'waitUntilRenderFlush resolves after stdout write callback',\n\tasync t => {\n\t\tlet didInitialWriteCallbackFire = false;\n\n\t\tconst stdout = createDelayedWriteCallbackStdout({\n\t\t\tshouldDelay(chunk) {\n\t\t\t\treturn !isWriteBarrierChunk(chunk);\n\t\t\t},\n\t\t\tonDelayElapsed() {\n\t\t\t\tdidInitialWriteCallbackFire = true;\n\t\t\t},\n\t\t});\n\n\t\tconst {unmount, waitUntilExit, waitUntilRenderFlush} = render(\n\t\t\t<Text>Hello</Text>,\n\t\t\t{\n\t\t\t\tstdout,\n\t\t\t},\n\t\t);\n\n\t\tt.teardown(async () => {\n\t\t\tunmount();\n\t\t\tawait waitUntilExit();\n\t\t});\n\n\t\tawait waitUntilRenderFlush();\n\n\t\tt.true(didInitialWriteCallbackFire);\n\t},\n);\n\ntest.serial(\n\t'waitUntilRenderFlush flushes pending throttled render',\n\tasync t => {\n\t\tconst stdout = createStdout();\n\t\tconst {unmount, rerender, waitUntilExit, waitUntilRenderFlush} = render(\n\t\t\t<ThrottleTestComponent text=\"Hello\" />,\n\t\t\t{\n\t\t\t\tstdout,\n\t\t\t\tmaxFps: 1,\n\t\t\t},\n\t\t);\n\n\t\tt.teardown(async () => {\n\t\t\tunmount();\n\t\t\tawait waitUntilExit();\n\t\t});\n\n\t\tt.is(getContentWrites(stdout.write).length, 1);\n\n\t\trerender(<ThrottleTestComponent text=\"World\" />);\n\t\tt.is(getContentWrites(stdout.write).length, 1);\n\n\t\tawait waitUntilRenderFlush();\n\n\t\tt.is(getContentWrites(stdout.write).length, 2);\n\t\tt.is(stripAnsi(getContentWrites(stdout.write)[1]!), 'World\\n');\n\t},\n);\n\ntest.serial(\n\t'waitUntilRenderFlush resolves when stdout is not writable',\n\tasync t => {\n\t\tconst stdout = createStdout();\n\t\tconst {unmount, rerender, waitUntilExit, waitUntilRenderFlush} = render(\n\t\t\t<ThrottleTestComponent text=\"Hello\" />,\n\t\t\t{\n\t\t\t\tstdout,\n\t\t\t\tmaxFps: 1,\n\t\t\t},\n\t\t);\n\n\t\tt.teardown(async () => {\n\t\t\tunmount();\n\t\t\tawait waitUntilExit();\n\t\t});\n\n\t\tt.is(getContentWrites(stdout.write).length, 1);\n\n\t\trerender(<ThrottleTestComponent text=\"World\" />);\n\t\tt.is(getContentWrites(stdout.write).length, 1);\n\n\t\t(stdout as NodeJS.WriteStream & {writable?: boolean}).writable = false;\n\t\tawait waitUntilRenderFlush();\n\n\t\tt.is(getContentWrites(stdout.write).length, 1);\n\t},\n);\n\ntest.serial(\n\t'waitUntilRenderFlush waits for rerender write callback',\n\tasync t => {\n\t\tlet didSecondWriteCallbackFire = false;\n\n\t\tconst stdout = createDelayedWriteCallbackStdout({\n\t\t\tshouldDelay(chunk) {\n\t\t\t\treturn (\n\t\t\t\t\t!isWriteBarrierChunk(chunk) &&\n\t\t\t\t\ttoRenderedChunk(chunk).includes('World')\n\t\t\t\t);\n\t\t\t},\n\t\t\tonDelayElapsed() {\n\t\t\t\tdidSecondWriteCallbackFire = true;\n\t\t\t},\n\t\t});\n\n\t\tconst {unmount, rerender, waitUntilExit, waitUntilRenderFlush} = render(\n\t\t\t<Text>Hello</Text>,\n\t\t\t{stdout},\n\t\t);\n\n\t\tt.teardown(async () => {\n\t\t\tunmount();\n\t\t\tawait waitUntilExit();\n\t\t});\n\n\t\tawait waitUntilRenderFlush();\n\t\trerender(<Text>World</Text>);\n\t\tawait waitUntilRenderFlush();\n\n\t\tt.true(didSecondWriteCallbackFire);\n\t},\n);\n\ntest.serial(\n\t'waitUntilRenderFlush waits for concurrent rerender commit',\n\tasync t => {\n\t\tlet renderedOutput = '';\n\n\t\tconst stdout = new Writable({\n\t\t\twrite(\n\t\t\t\tchunk: string | Uint8Array,\n\t\t\t\t_encoding: BufferEncoding,\n\t\t\t\tcallback: (error?: Error) => void,\n\t\t\t) {\n\t\t\t\trenderedOutput += toRenderedChunk(chunk);\n\t\t\t\tcallback();\n\t\t\t},\n\t\t}) as unknown as NodeJS.WriteStream;\n\n\t\tstdout.columns = 100;\n\t\tstdout.isTTY = true;\n\n\t\tconst {unmount, rerender, waitUntilExit, waitUntilRenderFlush} = render(\n\t\t\t<Text>Hello</Text>,\n\t\t\t{\n\t\t\t\tstdout,\n\t\t\t\tconcurrent: true,\n\t\t\t},\n\t\t);\n\n\t\tt.teardown(async () => {\n\t\t\tunmount();\n\t\t\tawait waitUntilExit();\n\t\t});\n\n\t\tawait waitUntilRenderFlush();\n\t\trerender(<Text>World</Text>);\n\t\tawait waitUntilRenderFlush();\n\n\t\tt.true(renderedOutput.includes('World'));\n\t},\n);\n\ntest.serial(\n\t'waitUntilRenderFlush waits for all concurrent waiters on the same rerender',\n\tasync t => {\n\t\tlet didWorldWriteCallbackFire = false;\n\t\tlet didAnyWaiterResolveBeforeWorldWriteCallback = false;\n\n\t\tconst stdout = createDelayedWriteCallbackStdout({\n\t\t\tshouldDelay(chunk) {\n\t\t\t\treturn (\n\t\t\t\t\t!isWriteBarrierChunk(chunk) &&\n\t\t\t\t\ttoRenderedChunk(chunk).includes('World')\n\t\t\t\t);\n\t\t\t},\n\t\t\tonDelayElapsed() {\n\t\t\t\tdidWorldWriteCallbackFire = true;\n\t\t\t},\n\t\t});\n\n\t\tconst {unmount, rerender, waitUntilExit, waitUntilRenderFlush} = render(\n\t\t\t<Text>Hello</Text>,\n\t\t\t{stdout},\n\t\t);\n\n\t\tt.teardown(async () => {\n\t\t\tunmount();\n\t\t\tawait waitUntilExit();\n\t\t});\n\n\t\tawait waitUntilRenderFlush();\n\t\trerender(<Text>World</Text>);\n\n\t\tconst waitForFlush = async () => {\n\t\t\tawait waitUntilRenderFlush();\n\n\t\t\tif (!didWorldWriteCallbackFire) {\n\t\t\t\tdidAnyWaiterResolveBeforeWorldWriteCallback = true;\n\t\t\t}\n\t\t};\n\n\t\tawait Promise.all([waitForFlush(), waitForFlush()]);\n\n\t\tt.true(didWorldWriteCallbackFire);\n\t\tt.false(didAnyWaiterResolveBeforeWorldWriteCallback);\n\t},\n);\n\ntest.serial(\n\t'useApp waitUntilRenderFlush resolves after the first frame write callback',\n\tasync t => {\n\t\tlet didInitialWriteCallbackFire = false;\n\t\tlet didWaitUntilRenderFlushResolve = false;\n\n\t\tconst stdout = createDelayedWriteCallbackStdout({\n\t\t\tshouldDelay(chunk) {\n\t\t\t\treturn !isWriteBarrierChunk(chunk);\n\t\t\t},\n\t\t\tonDelayElapsed() {\n\t\t\t\tdidInitialWriteCallbackFire = true;\n\t\t\t},\n\t\t});\n\n\t\tfunction Test() {\n\t\t\tconst {exit, waitUntilRenderFlush} = useApp();\n\n\t\t\tuseEffect(() => {\n\t\t\t\tvoid (async () => {\n\t\t\t\t\tawait waitUntilRenderFlush();\n\t\t\t\t\tdidWaitUntilRenderFlushResolve = true;\n\t\t\t\t\texit();\n\t\t\t\t})();\n\t\t\t}, [exit, waitUntilRenderFlush]);\n\n\t\t\treturn <Text>Hello</Text>;\n\t\t}\n\n\t\tconst {waitUntilExit} = render(<Test />, {stdout});\n\t\tawait waitUntilExit();\n\n\t\tt.true(didInitialWriteCallbackFire);\n\t\tt.true(didWaitUntilRenderFlushResolve);\n\t},\n);\n\ntest.serial(\n\t'useApp waitUntilRenderFlush waits for state update frame flush',\n\tasync t => {\n\t\tlet didWorldWriteCallbackFire = false;\n\t\tlet didWaitUntilRenderFlushResolve = false;\n\n\t\tconst stdout = createDelayedWriteCallbackStdout({\n\t\t\tshouldDelay(chunk) {\n\t\t\t\treturn (\n\t\t\t\t\t!isWriteBarrierChunk(chunk) &&\n\t\t\t\t\ttoRenderedChunk(chunk).includes('World')\n\t\t\t\t);\n\t\t\t},\n\t\t\tonDelayElapsed() {\n\t\t\t\tdidWorldWriteCallbackFire = true;\n\t\t\t},\n\t\t});\n\n\t\tfunction Test() {\n\t\t\tconst {exit, waitUntilRenderFlush} = useApp();\n\t\t\tconst [text, setText] = useState('Hello');\n\n\t\t\tuseEffect(() => {\n\t\t\t\tsetText('World');\n\t\t\t}, []);\n\n\t\t\tuseEffect(() => {\n\t\t\t\tif (text !== 'World') {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tvoid (async () => {\n\t\t\t\t\tawait waitUntilRenderFlush();\n\t\t\t\t\tdidWaitUntilRenderFlushResolve = true;\n\t\t\t\t\texit();\n\t\t\t\t})();\n\t\t\t}, [exit, text, waitUntilRenderFlush]);\n\n\t\t\treturn <Text>{text}</Text>;\n\t\t}\n\n\t\tconst {waitUntilExit} = render(<Test />, {stdout});\n\t\tawait waitUntilExit();\n\n\t\tt.true(didWorldWriteCallbackFire);\n\t\tt.true(didWaitUntilRenderFlushResolve);\n\t},\n);\n\ntest.serial(\n\t'useApp waitUntilRenderFlush waits for state update queued in same effect tick',\n\tasync t => {\n\t\tlet didWorldWriteCallbackFire = false;\n\t\tlet didWaitUntilRenderFlushResolveBeforeWorldWrite = false;\n\n\t\tconst stdout = createDelayedWriteCallbackStdout({\n\t\t\tshouldDelay(chunk) {\n\t\t\t\treturn (\n\t\t\t\t\t!isWriteBarrierChunk(chunk) &&\n\t\t\t\t\ttoRenderedChunk(chunk).includes('World')\n\t\t\t\t);\n\t\t\t},\n\t\t\tonDelayElapsed() {\n\t\t\t\tdidWorldWriteCallbackFire = true;\n\t\t\t},\n\t\t});\n\n\t\tfunction Test() {\n\t\t\tconst {exit, waitUntilRenderFlush} = useApp();\n\t\t\tconst [text, setText] = useState('Hello');\n\n\t\t\tuseEffect(() => {\n\t\t\t\tvoid (async () => {\n\t\t\t\t\tsetText('World');\n\t\t\t\t\tawait waitUntilRenderFlush();\n\n\t\t\t\t\tif (!didWorldWriteCallbackFire) {\n\t\t\t\t\t\tdidWaitUntilRenderFlushResolveBeforeWorldWrite = true;\n\t\t\t\t\t}\n\n\t\t\t\t\texit();\n\t\t\t\t})();\n\t\t\t}, [exit, waitUntilRenderFlush]);\n\n\t\t\treturn <Text>{text}</Text>;\n\t\t}\n\n\t\tconst {waitUntilExit} = render(<Test />, {\n\t\t\tstdout,\n\t\t\tconcurrent: true,\n\t\t});\n\t\tawait waitUntilExit();\n\n\t\tt.true(didWorldWriteCallbackFire);\n\t\tt.false(didWaitUntilRenderFlushResolveBeforeWorldWrite);\n\t},\n);\n\ntest.serial('waitUntilRenderFlush resolves after unmount', async t => {\n\tconst stdout = createStdout();\n\tconst {unmount, waitUntilExit, waitUntilRenderFlush} = render(\n\t\t<Text>Hello</Text>,\n\t\t{\n\t\t\tstdout,\n\t\t},\n\t);\n\n\tunmount();\n\tawait waitUntilExit();\n\tawait waitUntilRenderFlush();\n\tt.pass();\n});\n\ntest.serial(\n\t'waitUntilRenderFlush waits for unmount write callback',\n\tasync t => {\n\t\tlet didUnmountWriteCallbackFire = false;\n\n\t\tconst stdout = createDelayedWriteCallbackStdout({\n\t\t\tshouldDelay(chunk) {\n\t\t\t\treturn isWriteBarrierChunk(chunk);\n\t\t\t},\n\t\t\tonDelayElapsed() {\n\t\t\t\tdidUnmountWriteCallbackFire = true;\n\t\t\t},\n\t\t});\n\n\t\tconst {unmount, waitUntilRenderFlush} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t});\n\n\t\tunmount();\n\t\tawait waitUntilRenderFlush();\n\n\t\tt.true(didUnmountWriteCallbackFire);\n\t},\n);\n\ntest.serial(\n\t'waitUntilRenderFlush after unmount does not register beforeExit listener',\n\tasync t => {\n\t\tconst stdout = createStdout();\n\t\tconst {unmount, waitUntilRenderFlush} = render(<Text>Hello</Text>, {\n\t\t\tstdout,\n\t\t});\n\t\tconst beforeWaitListenerCount = process.listenerCount('beforeExit');\n\n\t\tunmount();\n\t\tawait waitUntilRenderFlush();\n\n\t\tt.is(process.listenerCount('beforeExit'), beforeWaitListenerCount);\n\t},\n);\n\ntest.serial('waitUntilRenderFlush resolves after exit with error', async t => {\n\tconst stdout = createStdout();\n\n\tfunction Test() {\n\t\tconst {exit} = useApp();\n\n\t\tuseEffect(() => {\n\t\t\texit(new Error('boom'));\n\t\t}, []);\n\n\t\treturn <Text>Hello</Text>;\n\t}\n\n\tconst {waitUntilExit, waitUntilRenderFlush} = render(<Test />, {stdout});\n\n\t// Verify exit rejects with the error.\n\tawait t.throwsAsync(waitUntilExit(), {message: 'boom'});\n\n\t// Flush must resolve (not reject) even after an error exit.\n\tawait waitUntilRenderFlush();\n});\n\ntest.serial(\n\t'issue 596: useEffect can run before the first frame write callback',\n\tasync t => {\n\t\tlet didInitialWriteCallbackFire = false;\n\t\tlet didUseEffectRun = false;\n\n\t\tconst stdout = createDelayedWriteCallbackStdout({\n\t\t\tshouldDelay(chunk) {\n\t\t\t\treturn !isWriteBarrierChunk(chunk);\n\t\t\t},\n\t\t\tonDelayElapsed() {\n\t\t\t\tdidInitialWriteCallbackFire = true;\n\t\t\t},\n\t\t});\n\n\t\tfunction Test() {\n\t\t\tuseEffect(() => {\n\t\t\t\tdidUseEffectRun = true;\n\t\t\t}, []);\n\n\t\t\treturn <Text>Hello</Text>;\n\t\t}\n\n\t\tconst {unmount, waitUntilExit} = render(<Test />, {stdout});\n\n\t\tawait delay(20);\n\t\tt.true(didUseEffectRun);\n\t\tt.false(didInitialWriteCallbackFire);\n\n\t\tunmount();\n\t\tawait waitUntilExit();\n\n\t\tt.true(didInitialWriteCallbackFire);\n\t},\n);\n\ntest.serial(\n\t'waitUntilExit resolves first exit value when duplicate exits happen during teardown',\n\tasync t => {\n\t\tlet barrierWriteCallback: (() => void) | undefined;\n\n\t\tconst stdout = new Writable({\n\t\t\twrite(\n\t\t\t\tchunk: string | Uint8Array,\n\t\t\t\t_encoding: BufferEncoding,\n\t\t\t\tcallback: (error?: Error) => void,\n\t\t\t) {\n\t\t\t\tif (isWriteBarrierChunk(chunk)) {\n\t\t\t\t\tbarrierWriteCallback = callback;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tcallback();\n\t\t\t},\n\t\t}) as unknown as NodeJS.WriteStream;\n\n\t\tstdout.columns = 100;\n\n\t\tfunction Test() {\n\t\t\tconst {exit} = useApp();\n\n\t\t\tuseEffect(() => {\n\t\t\t\texit('first');\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\texit('second');\n\t\t\t\t}, 0);\n\t\t\t}, []);\n\n\t\t\treturn <Text>Hello</Text>;\n\t\t}\n\n\t\tconst {waitUntilExit} = render(<Test />, {stdout});\n\t\tconst exitPromise = waitUntilExit();\n\n\t\tawait delay(0);\n\n\t\tif (!barrierWriteCallback) {\n\t\t\tt.fail('Expected unmount to queue a write barrier callback');\n\t\t\treturn;\n\t\t}\n\n\t\tbarrierWriteCallback();\n\t\tconst result = await exitPromise;\n\t\tt.is(result, 'first');\n\t},\n);\n\ntest.serial(\n\t'waitUntilExit resolves first exit value when exit is re-entered during unmount writes',\n\tasync t => {\n\t\tlet exit: ((errorOrResult?: unknown) => void) | undefined;\n\t\tlet shouldReenterExit = false;\n\t\tlet didReenterExit = false;\n\n\t\tconst stdout = new Writable({\n\t\t\twrite(_chunk, _encoding, callback) {\n\t\t\t\tif (shouldReenterExit && !didReenterExit && exit) {\n\t\t\t\t\tdidReenterExit = true;\n\t\t\t\t\texit('second');\n\t\t\t\t}\n\n\t\t\t\tcallback();\n\t\t\t},\n\t\t}) as unknown as NodeJS.WriteStream;\n\n\t\tstdout.columns = 100;\n\t\tstdout.isTTY = true;\n\n\t\tfunction Test() {\n\t\t\tconst {exit: appExit} = useApp();\n\n\t\t\tuseEffect(() => {\n\t\t\t\texit = appExit;\n\t\t\t\tshouldReenterExit = true;\n\t\t\t\tappExit('first');\n\t\t\t}, []);\n\n\t\t\treturn <Text>Hello</Text>;\n\t\t}\n\n\t\tconst {waitUntilExit} = render(<Test />, {stdout});\n\t\tconst result = await waitUntilExit();\n\n\t\tt.true(didReenterExit);\n\t\tt.is(result, 'first');\n\t},\n);\n\ntest.serial(\n\t'waitUntilExit resolves first exit value when exit is re-entered during unmount writes in debug mode',\n\tasync t => {\n\t\tlet exit: ((errorOrResult?: unknown) => void) | undefined;\n\t\tlet shouldReenterExit = false;\n\t\tlet didReenterExit = false;\n\n\t\tconst stdout = new Writable({\n\t\t\twrite(_chunk, _encoding, callback) {\n\t\t\t\tif (shouldReenterExit && !didReenterExit && exit) {\n\t\t\t\t\tdidReenterExit = true;\n\t\t\t\t\texit('second');\n\t\t\t\t}\n\n\t\t\t\tcallback();\n\t\t\t},\n\t\t}) as unknown as NodeJS.WriteStream;\n\n\t\tstdout.columns = 100;\n\t\tstdout.isTTY = true;\n\n\t\tfunction Test() {\n\t\t\tconst {exit: appExit} = useApp();\n\n\t\t\tuseEffect(() => {\n\t\t\t\texit = appExit;\n\t\t\t\tshouldReenterExit = true;\n\t\t\t\tappExit('first');\n\t\t\t}, []);\n\n\t\t\treturn <Text>Hello</Text>;\n\t\t}\n\n\t\tconst {waitUntilExit} = render(<Test />, {stdout, debug: true});\n\t\tconst result = await waitUntilExit();\n\n\t\tt.true(didReenterExit);\n\t\tt.is(result, 'first');\n\t},\n);\n\ntest.serial(\n\t'waitUntilExit resolves first exit value when exit is re-entered during unmount writes with screen reader',\n\tasync t => {\n\t\tlet exit: ((errorOrResult?: unknown) => void) | undefined;\n\t\tlet shouldReenterExit = false;\n\t\tlet didReenterExit = false;\n\n\t\tconst stdout = new Writable({\n\t\t\twrite(_chunk, _encoding, callback) {\n\t\t\t\tif (shouldReenterExit && !didReenterExit && exit) {\n\t\t\t\t\tdidReenterExit = true;\n\t\t\t\t\texit('second');\n\t\t\t\t}\n\n\t\t\t\tcallback();\n\t\t\t},\n\t\t}) as unknown as NodeJS.WriteStream;\n\n\t\tstdout.columns = 100;\n\t\tstdout.isTTY = true;\n\n\t\tfunction Test() {\n\t\t\tconst {exit: appExit} = useApp();\n\n\t\t\tuseEffect(() => {\n\t\t\t\texit = appExit;\n\t\t\t\tshouldReenterExit = true;\n\t\t\t\tappExit('first');\n\t\t\t}, []);\n\n\t\t\treturn <Text>Hello</Text>;\n\t\t}\n\n\t\tconst {waitUntilExit} = render(<Test />, {\n\t\t\tstdout,\n\t\t\tisScreenReaderEnabled: true,\n\t\t\tpatchConsole: false,\n\t\t});\n\t\tconst result = await waitUntilExit();\n\n\t\tt.true(didReenterExit);\n\t\tt.is(result, 'first');\n\t},\n);\n\ntest.serial('exit rejects on cross-realm Error', async t => {\n\tconst stdout = new PassThrough() as unknown as NodeJS.WriteStream;\n\tstdout.columns = 100;\n\n\tconst foreignError = vm.runInNewContext(`new Error('boom')`) as Error;\n\n\tfunction Test() {\n\t\tconst {exit} = useApp();\n\n\t\tuseEffect(() => {\n\t\t\tsetTimeout(() => {\n\t\t\t\texit(foreignError);\n\t\t\t}, 0);\n\t\t}, []);\n\n\t\treturn <Text>Hello</Text>;\n\t}\n\n\tconst {waitUntilExit} = render(<Test />, {stdout, patchConsole: false});\n\n\tawait t.throwsAsync(waitUntilExit(), {\n\t\tmessage: 'boom',\n\t});\n});\n\ntest.serial(\n\t'exit with cross-realm Error rejects after stdout write callback',\n\tasync t => {\n\t\tlet writeCallbackFired = false;\n\t\tlet barrierWriteCallbackFired = false;\n\n\t\tconst stdout = new Writable({\n\t\t\twrite(chunk: string | Uint8Array, _encoding, callback) {\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\twriteCallbackFired = true;\n\n\t\t\t\t\tif (isWriteBarrierChunk(chunk)) {\n\t\t\t\t\t\tbarrierWriteCallbackFired = true;\n\t\t\t\t\t}\n\n\t\t\t\t\tcallback();\n\t\t\t\t}, 150);\n\t\t\t},\n\t\t}) as unknown as NodeJS.WriteStream;\n\n\t\tstdout.columns = 100;\n\n\t\tconst foreignError = vm.runInNewContext(`new Error('boom')`) as Error;\n\n\t\tfunction Test() {\n\t\t\tconst {exit} = useApp();\n\n\t\t\tuseEffect(() => {\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\texit(foreignError);\n\t\t\t\t}, 0);\n\t\t\t}, []);\n\n\t\t\treturn <Text>Hello</Text>;\n\t\t}\n\n\t\tconst {waitUntilExit} = render(<Test />, {stdout, patchConsole: false});\n\n\t\tawait t.throwsAsync(waitUntilExit(), {\n\t\t\tmessage: 'boom',\n\t\t});\n\n\t\tt.true(writeCallbackFired);\n\t\tt.true(barrierWriteCallbackFired);\n\t},\n);\n\ntest.serial('unmount does not write to ended stdout stream', async t => {\n\tconst stdout = new PassThrough() as unknown as NodeJS.WriteStream;\n\tstdout.columns = 100;\n\n\tconst writeErrors: Error[] = [];\n\tstdout.on('error', error => {\n\t\twriteErrors.push(error);\n\t});\n\n\tconst {unmount, waitUntilExit} = render(<Text>Hello</Text>, {stdout});\n\tconst exitPromise = waitUntilExit();\n\n\tstdout.end();\n\tunmount();\n\tawait exitPromise;\n\tawait delay(0);\n\n\tt.false(\n\t\twriteErrors.some(\n\t\t\terror =>\n\t\t\t\t(error as NodeJS.ErrnoException).code === 'ERR_STREAM_WRITE_AFTER_END',\n\t\t),\n\t);\n});\n\ntest.serial(\n\t'unmount cancels pending throttled log writes when stdout is ended',\n\tt => {\n\t\tconst clock = FakeTimers.install();\n\t\ttry {\n\t\t\tconst stdout = new PassThrough() as unknown as NodeJS.WriteStream;\n\t\t\tstdout.columns = 100;\n\n\t\t\tconst writeErrors: Error[] = [];\n\t\t\tstdout.on('error', error => {\n\t\t\t\twriteErrors.push(error);\n\t\t\t});\n\n\t\t\tconst {rerender, unmount} = render(\n\t\t\t\t<ThrottleTestComponent text=\"Hello\" />,\n\t\t\t\t{\n\t\t\t\t\tstdout,\n\t\t\t\t\tmaxFps: 1,\n\t\t\t\t},\n\t\t\t);\n\n\t\t\trerender(<ThrottleTestComponent text=\"World\" />);\n\t\t\tstdout.end();\n\t\t\tunmount();\n\t\t\tclock.tick(1000);\n\n\t\t\tt.false(\n\t\t\t\twriteErrors.some(\n\t\t\t\t\terror =>\n\t\t\t\t\t\t(error as NodeJS.ErrnoException).code ===\n\t\t\t\t\t\t'ERR_STREAM_WRITE_AFTER_END',\n\t\t\t\t),\n\t\t\t);\n\t\t} finally {\n\t\t\tclock.uninstall();\n\t\t}\n\t},\n);\n\ntest.serial(\n\t'unmount cancels pending throttled render when stdout is ended',\n\tt => {\n\t\tconst clock = FakeTimers.install();\n\t\ttry {\n\t\t\tconst baselineStdout = new PassThrough() as unknown as NodeJS.WriteStream;\n\t\t\tbaselineStdout.columns = 100;\n\n\t\t\tconst baselineApp = render(<ThrottleTestComponent text=\"Hello\" />, {\n\t\t\t\tstdout: baselineStdout,\n\t\t\t\tmaxFps: 1,\n\t\t\t});\n\t\t\tbaselineStdout.end();\n\t\t\tbaselineApp.unmount();\n\t\t\tconst baselineTimers = clock.countTimers();\n\t\t\tclock.runAll();\n\n\t\t\tconst stdout = new PassThrough() as unknown as NodeJS.WriteStream;\n\t\t\tstdout.columns = 100;\n\n\t\t\tconst {rerender, unmount} = render(\n\t\t\t\t<ThrottleTestComponent text=\"Hello\" />,\n\t\t\t\t{\n\t\t\t\t\tstdout,\n\t\t\t\t\tmaxFps: 1,\n\t\t\t\t},\n\t\t\t);\n\t\t\trerender(<ThrottleTestComponent text=\"World\" />);\n\t\t\tstdout.end();\n\t\t\tunmount();\n\n\t\t\tt.is(clock.countTimers(), baselineTimers);\n\t\t} finally {\n\t\t\tclock.uninstall();\n\t\t}\n\t},\n);\n\nconst createTtyStdout = (columns?: number) => {\n\tconst stdout = createStdout(columns);\n\t(stdout as any).isTTY = true;\n\treturn stdout;\n};\n\nconst withFakeClock = (\n\trun: (clock: ReturnType<typeof FakeTimers.install>) => void,\n) => {\n\tconst clock = FakeTimers.install();\n\ttry {\n\t\trun(clock);\n\t} finally {\n\t\tclock.uninstall();\n\t}\n};\n\nconst captureWrites = (stdout: NodeJS.WriteStream): string[] => {\n\tconst writes: string[] = [];\n\tconst originalWrite = stdout.write;\n\t(stdout as any).write = (...args: any[]) => {\n\t\twrites.push(args[0] as string);\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call\n\t\treturn (originalWrite as any)(...args);\n\t};\n\n\treturn writes;\n};\n\nconst assertNoBsuEsuForUnchangedTrailingRerender = (\n\tt: ExecutionContext,\n\telement: React.ReactElement,\n) => {\n\twithFakeClock(clock => {\n\t\tconst stdout = createTtyStdout();\n\t\tconst writes = captureWrites(stdout);\n\t\tconst {unmount, rerender} = render(element, {stdout, maxFps: 1});\n\t\ttry {\n\t\t\tt.true(writes.includes(bsu), 'initial render should include bsu');\n\n\t\t\twrites.length = 0;\n\t\t\trerender(element);\n\t\t\tclock.tick(1000);\n\n\t\t\tt.false(writes.includes(bsu), 'unchanged rerender should not emit bsu');\n\t\t\tt.false(writes.includes(esu), 'unchanged rerender should not emit esu');\n\t\t} finally {\n\t\t\tunmount();\n\t\t}\n\t});\n};\n\ntest.serial('no bsu/esu when output is unchanged', t => {\n\tassertNoBsuEsuForUnchangedTrailingRerender(\n\t\tt,\n\t\t<ThrottleTestComponent text=\"Hello\" />,\n\t);\n});\n\ntest.serial('no bsu/esu when output and cursor are unchanged', t => {\n\tassertNoBsuEsuForUnchangedTrailingRerender(\n\t\tt,\n\t\t<ThrottleCursorTestComponent text=\"Hello\" />,\n\t);\n});\n\ntest.serial('bsu/esu wraps throttledLog trailing call', t => {\n\twithFakeClock(clock => {\n\t\tconst stdout = createTtyStdout();\n\t\tconst writes = captureWrites(stdout);\n\t\tconst {unmount, rerender} = render(<ThrottleTestComponent text=\"Hello\" />, {\n\t\t\tstdout,\n\t\t\tmaxFps: 1,\n\t\t});\n\t\ttry {\n\t\t\t// Leading call writes: bsu, content, esu\n\t\t\tconst leadingWrites = new Set(writes);\n\t\t\tt.true(leadingWrites.has(bsu), 'leading call should include bsu');\n\t\t\tt.true(leadingWrites.has(esu), 'leading call should include esu');\n\n\t\t\t// Trigger a rerender inside the throttle window (will be deferred as trailing)\n\t\t\twrites.length = 0;\n\t\t\trerender(<ThrottleTestComponent text=\"World\" />);\n\n\t\t\t// No immediate write yet (throttled)\n\t\t\tconst midWrites = [...writes];\n\t\t\tt.false(\n\t\t\t\tmidWrites.some(w => w.includes('World')),\n\t\t\t\t'trailing call should not write immediately',\n\t\t\t);\n\n\t\t\t// Advance past throttle window to trigger trailing call\n\t\t\twrites.length = 0;\n\t\t\tclock.tick(1000);\n\n\t\t\t// Trailing call should also be wrapped with bsu/esu\n\t\t\tt.true(writes.includes(bsu), 'trailing call should include bsu');\n\t\t\tt.true(writes.includes(esu), 'trailing call should include esu');\n\n\t\t\t// Verify bsu comes before content and esu comes after\n\t\t\tconst bsuIdx = writes.indexOf(bsu);\n\t\t\tconst esuIdx = writes.indexOf(esu);\n\t\t\tt.true(bsuIdx < esuIdx, 'bsu should come before esu');\n\t\t} finally {\n\t\t\tunmount();\n\t\t}\n\t});\n});\n"
  },
  {
    "path": "test/sanitize-ansi.ts",
    "content": "import test from 'ava';\nimport stripAnsi from 'strip-ansi';\nimport sanitizeAnsi from '../src/sanitize-ansi.js';\n\ntest('preserve plain text', t => {\n\tt.is(sanitizeAnsi('hello'), 'hello');\n});\n\ntest('preserve SGR sequences', t => {\n\tconst output = sanitizeAnsi('A\\u001B[38:2::255:100:0mcolor\\u001B[0mB');\n\n\tt.true(output.includes('\\u001B[38:2::255:100:0m'));\n\tt.is(stripAnsi(output), 'AcolorB');\n});\n\ntest('preserve OSC hyperlinks', t => {\n\tconst output = sanitizeAnsi(\n\t\t'\\u001B]8;;https://example.com\\u001B\\\\link\\u001B]8;;\\u001B\\\\',\n\t);\n\n\tt.true(output.includes('\\u001B]8;;https://example.com'));\n\tt.is(stripAnsi(output), 'link');\n});\n\ntest('preserve OSC hyperlinks terminated by C1 ST', t => {\n\tconst output = sanitizeAnsi(\n\t\t'\\u001B]8;;https://example.com\\u009Clink\\u001B]8;;\\u009C',\n\t);\n\n\tt.true(output.includes('\\u001B]8;;https://example.com\\u009C'));\n\tt.is(stripAnsi(output), 'link');\n});\n\ntest('preserve C1 OSC hyperlinks terminated by C1 ST', t => {\n\tconst input = '\\u009D8;;https://example.com\\u009Clink\\u009D8;;\\u009C';\n\tconst output = sanitizeAnsi(input);\n\n\tt.true(output.includes('\\u009D8;;https://example.com\\u009C'));\n\tt.is(output, input);\n});\n\ntest('preserve C1 OSC hyperlinks terminated by ESC ST', t => {\n\tconst input = '\\u009D8;;https://example.com\\u001B\\\\link\\u009D8;;\\u001B\\\\';\n\tconst output = sanitizeAnsi(input);\n\n\tt.true(output.includes('\\u009D8;;https://example.com\\u001B\\\\'));\n\tt.is(output, input);\n});\n\ntest('preserve C1 OSC hyperlinks terminated by BEL', t => {\n\tconst input = '\\u009D8;;https://example.com\\u0007link\\u009D8;;\\u0007';\n\tconst output = sanitizeAnsi(input);\n\n\tt.true(output.includes('\\u009D8;;https://example.com\\u0007'));\n\tt.is(output, input);\n});\n\ntest('strip non-SGR CSI sequences as complete units', t => {\n\tconst output = sanitizeAnsi('A\\u001B[>4;2mB\\u001B[2 qC');\n\n\tt.false(output.includes('4;2m'));\n\tt.false(output.includes(' q'));\n\tt.is(stripAnsi(output), 'ABC');\n});\n\ntest('strip C1 non-SGR CSI sequences as complete units', t => {\n\tconst output = sanitizeAnsi('A\\u009B>4;2mB\\u009B2 qC');\n\n\tt.false(output.includes('4;2m'));\n\tt.false(output.includes(' q'));\n\tt.is(stripAnsi(output), 'ABC');\n});\n\ntest('preserve C1 SGR CSI sequences', t => {\n\tconst output = sanitizeAnsi('A\\u009B31mgreen\\u009B0mB');\n\n\tt.true(output.includes('\\u009B31m'));\n\tt.is(stripAnsi(output), 'AgreenB');\n});\n\ntest('strip private-parameter m-sequences that are not SGR', t => {\n\tconst output = sanitizeAnsi('A\\u001B[>4;2mB');\n\n\tt.false(output.includes('\\u001B[>4;2m'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('strip tmux DCS passthrough wrappers with escaped ST payload terminators', t => {\n\tconst wrappedHyperlinkStart =\n\t\t'\\u001BPtmux;\\u001B\\u001B]8;;https://example.com\\u001B\\u001B\\\\\\u001B\\\\';\n\tconst wrappedHyperlinkEnd =\n\t\t'\\u001BPtmux;\\u001B\\u001B]8;;\\u001B\\u001B\\\\\\u001B\\\\';\n\tconst output = sanitizeAnsi(\n\t\t`${wrappedHyperlinkStart}link${wrappedHyperlinkEnd}`,\n\t);\n\n\tt.false(output.includes('tmux;'));\n\tt.false(output.includes('\\u001BP'));\n\tt.is(stripAnsi(output), 'link');\n});\n\ntest('strip incomplete DCS passthrough sequences to avoid payload leaks', t => {\n\tconst output = sanitizeAnsi('A\\u001BPtmux;\\u001Blink');\n\n\tt.false(output.includes('tmux;'));\n\tt.is(stripAnsi(output), 'A');\n});\n\ntest('strip DCS control strings with BEL in payload until ST terminator', t => {\n\tconst output = sanitizeAnsi('A\\u001BPpayload\\u0007still-payload\\u001B\\\\B');\n\n\tt.false(output.includes('payload'));\n\tt.false(output.includes('still-payload'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('strip ESC SOS control strings as complete units', t => {\n\tconst output = sanitizeAnsi('A\\u001BXpayload\\u001B\\\\B');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('strip ESC SOS control strings with C1 ST terminator', t => {\n\tconst output = sanitizeAnsi('A\\u001BXpayload\\u009CB');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('strip C1 SOS control strings as complete units with C1 ST terminator', t => {\n\tconst output = sanitizeAnsi('A\\u0098payload\\u009CB');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('strip C1 SOS control strings as complete units with ESC ST terminator', t => {\n\tconst output = sanitizeAnsi('A\\u0098payload\\u001B\\\\B');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('strip ESC SOS with BEL terminator as malformed control string', t => {\n\tconst output = sanitizeAnsi('A\\u001BXpayload\\u0007B');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'A');\n});\n\ntest('strip C1 SOS with BEL terminator as malformed control string', t => {\n\tconst output = sanitizeAnsi('A\\u0098payload\\u0007B');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'A');\n});\n\ntest('strip incomplete ESC SOS control strings to avoid payload leaks', t => {\n\tconst output = sanitizeAnsi('A\\u001BXpayload');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'A');\n});\n\ntest('strip incomplete C1 SOS control strings to avoid payload leaks', t => {\n\tconst output = sanitizeAnsi('A\\u0098payload');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'A');\n});\n\ntest('strip SOS with escaped ESC in payload until final ST terminator', t => {\n\tconst output = sanitizeAnsi('A\\u001BXfoo\\u001B\\u001B\\\\bar\\u001B\\\\B');\n\n\tt.false(output.includes('foo'));\n\tt.false(output.includes('bar'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('preserve SGR around stripped SOS control strings', t => {\n\tconst output = sanitizeAnsi('A\\u001B[31mR\\u001B[0m\\u001BXpayload\\u001B\\\\B');\n\n\tt.true(output.includes('\\u001B[31m'));\n\tt.true(output.includes('\\u001B[0m'));\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'ARB');\n});\n\ntest('strip ESC ST sequences', t => {\n\tconst output = sanitizeAnsi('A\\u001B\\\\B');\n\n\tt.false(output.includes('\\u001B\\\\'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('strip malformed ESC control sequences with intermediates and non-final bytes', t => {\n\tconst output = sanitizeAnsi('A\\u001B#\\u0007payload');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'A');\n});\n\ntest('strip incomplete CSI after preserving prior SGR content', t => {\n\tconst output = sanitizeAnsi('A\\u001B[31mB\\u001B[');\n\n\tt.true(output.includes('\\u001B[31m'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('strip standalone ST bytes', t => {\n\tconst output = sanitizeAnsi('A\\u009CB');\n\n\tt.false(output.includes('\\u009C'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('strip standalone C1 control characters', t => {\n\tconst output = sanitizeAnsi('A\\u0085B\\u008EC');\n\n\tt.false(output.includes('\\u0085'));\n\tt.false(output.includes('\\u008E'));\n\tt.is(stripAnsi(output), 'ABC');\n});\n"
  },
  {
    "path": "test/screen-reader.tsx",
    "content": "import test from 'ava';\nimport React from 'react';\nimport {Box, Text} from '../src/index.js';\nimport {renderToString} from './helpers/render-to-string.js';\n\ntest('render text for screen readers', t => {\n\tconst output = renderToString(\n\t\t<Box aria-label=\"Hello World\">\n\t\t\t<Text>Not visible to screen readers</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'Hello World');\n});\n\ntest('render text for screen readers with aria-hidden', t => {\n\tconst output = renderToString(\n\t\t<Box aria-hidden>\n\t\t\t<Text>Not visible to screen readers</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, '');\n});\n\ntest('render text for screen readers with aria-role', t => {\n\tconst output = renderToString(\n\t\t<Box aria-role=\"button\">\n\t\t\t<Text>Click me</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'button: Click me');\n});\n\ntest('render select input for screen readers', t => {\n\tconst items = ['Red', 'Green', 'Blue'];\n\n\tconst output = renderToString(\n\t\t<Box aria-role=\"list\" flexDirection=\"column\">\n\t\t\t<Text>Select a color:</Text>\n\t\t\t{items.map((item, index) => {\n\t\t\t\tconst isSelected = index === 1;\n\t\t\t\tconst screenReaderLabel = `${index + 1}. ${item}`;\n\n\t\t\t\treturn (\n\t\t\t\t\t<Box\n\t\t\t\t\t\tkey={item}\n\t\t\t\t\t\taria-label={screenReaderLabel}\n\t\t\t\t\t\taria-role=\"listitem\"\n\t\t\t\t\t\taria-state={{selected: isSelected}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Text>{item}</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t);\n\t\t\t})}\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t'list: Select a color:\\nlistitem: 1. Red\\nlistitem: (selected) 2. Green\\nlistitem: 3. Blue',\n\t);\n});\n\ntest('render aria-label only Text for screen readers', t => {\n\tconst output = renderToString(<Text aria-label=\"Screen-reader only\" />, {\n\t\tisScreenReaderEnabled: true,\n\t});\n\n\tt.is(output, 'Screen-reader only');\n});\n\ntest('render aria-label only Box for screen readers', t => {\n\tconst output = renderToString(<Box aria-label=\"Screen-reader only\" />, {\n\t\tisScreenReaderEnabled: true,\n\t});\n\n\tt.is(output, 'Screen-reader only');\n});\n\ntest('omit ANSI styling in screen-reader output', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t{/* eslint-disable-next-line react/jsx-sort-props */}\n\t\t\t<Text bold color=\"green\" inverse underline>\n\t\t\t\tStyled content\n\t\t\t</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'Styled content');\n});\n\ntest('skip nodes with display:none style in screen-reader output', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Box display=\"none\">\n\t\t\t\t<Text>Hidden</Text>\n\t\t\t</Box>\n\t\t\t<Text>Visible</Text>\n\t\t</Box>,\n\t\t{isScreenReaderEnabled: true},\n\t);\n\n\tt.is(output, 'Visible');\n});\n\ntest('render multiple Text components', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>Hello</Text>\n\t\t\t<Text>World</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'Hello\\nWorld');\n});\n\ntest('render nested Box components with Text', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>Hello</Text>\n\t\t\t<Box>\n\t\t\t\t<Text>World</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'Hello\\nWorld');\n});\n\nfunction NullComponent(): undefined {\n\treturn undefined;\n}\n\ntest('render component that returns null', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>Hello</Text>\n\t\t\t<NullComponent />\n\t\t\t<Text>World</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'Hello\\nWorld');\n});\n\ntest('render with aria-state.busy', t => {\n\tconst output = renderToString(\n\t\t<Box aria-state={{busy: true}}>\n\t\t\t<Text>Loading</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, '(busy) Loading');\n});\n\ntest('render with aria-state.checked', t => {\n\tconst output = renderToString(\n\t\t<Box aria-role=\"checkbox\" aria-state={{checked: true}}>\n\t\t\t<Text>Accept terms</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'checkbox: (checked) Accept terms');\n});\n\ntest('render with aria-state.disabled', t => {\n\tconst output = renderToString(\n\t\t<Box aria-role=\"button\" aria-state={{disabled: true}}>\n\t\t\t<Text>Submit</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'button: (disabled) Submit');\n});\n\ntest('render with aria-state.expanded', t => {\n\tconst output = renderToString(\n\t\t<Box aria-role=\"combobox\" aria-state={{expanded: true}}>\n\t\t\t<Text>Select</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'combobox: (expanded) Select');\n});\n\ntest('render with aria-state.multiline', t => {\n\tconst output = renderToString(\n\t\t<Box aria-role=\"textbox\" aria-state={{multiline: true}}>\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'textbox: (multiline) Hello');\n});\n\ntest('render with aria-state.multiselectable', t => {\n\tconst output = renderToString(\n\t\t<Box aria-role=\"listbox\" aria-state={{multiselectable: true}}>\n\t\t\t<Text>Options</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'listbox: (multiselectable) Options');\n});\n\ntest('render with aria-state.readonly', t => {\n\tconst output = renderToString(\n\t\t<Box aria-role=\"textbox\" aria-state={{readonly: true}}>\n\t\t\t<Text>Hello</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'textbox: (readonly) Hello');\n});\n\ntest('render with aria-state.required', t => {\n\tconst output = renderToString(\n\t\t<Box aria-role=\"textbox\" aria-state={{required: true}}>\n\t\t\t<Text>Name</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'textbox: (required) Name');\n});\n\ntest('render with aria-state.selected', t => {\n\tconst output = renderToString(\n\t\t<Box aria-role=\"option\" aria-state={{selected: true}}>\n\t\t\t<Text>Blue</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'option: (selected) Blue');\n});\n\ntest('render multi-line text', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Text>Line 1</Text>\n\t\t\t<Text>Line 2</Text>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'Line 1\\nLine 2');\n});\n\ntest('render nested multi-line text', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"row\">\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Text>Line 1</Text>\n\t\t\t\t<Text>Line 2</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'Line 1\\nLine 2');\n});\n\ntest('render nested row', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Box flexDirection=\"row\">\n\t\t\t\t<Text>Line 1</Text>\n\t\t\t\t<Text>Line 2</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'Line 1 Line 2');\n});\n\ntest('render multi-line text with roles', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\" aria-role=\"list\">\n\t\t\t<Box aria-role=\"listitem\">\n\t\t\t\t<Text>Item 1</Text>\n\t\t\t</Box>\n\t\t\t<Box aria-role=\"listitem\">\n\t\t\t\t<Text>Item 2</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(output, 'list: listitem: Item 1\\nlistitem: Item 2');\n});\n\ntest('render listbox with multiselectable options', t => {\n\tconst output = renderToString(\n\t\t<Box\n\t\t\tflexDirection=\"column\"\n\t\t\taria-role=\"listbox\"\n\t\t\taria-state={{multiselectable: true}}\n\t\t>\n\t\t\t<Box aria-role=\"option\" aria-state={{selected: true}}>\n\t\t\t\t<Text>Option 1</Text>\n\t\t\t</Box>\n\t\t\t<Box aria-role=\"option\" aria-state={{selected: false}}>\n\t\t\t\t<Text>Option 2</Text>\n\t\t\t</Box>\n\t\t\t<Box aria-role=\"option\" aria-state={{selected: true}}>\n\t\t\t\t<Text>Option 3</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t\t{\n\t\t\tisScreenReaderEnabled: true,\n\t\t},\n\t);\n\n\tt.is(\n\t\toutput,\n\t\t'listbox: (multiselectable) option: (selected) Option 1\\noption: Option 2\\noption: (selected) Option 3',\n\t);\n});\n"
  },
  {
    "path": "test/terminal-resize.tsx",
    "content": "import process from 'node:process';\nimport test from 'ava';\nimport delay from 'delay';\nimport stripAnsi from 'strip-ansi';\nimport React from 'react';\nimport {render, Box, Text, useWindowSize} from '../src/index.js';\nimport createStdout, {type FakeStdout} from './helpers/create-stdout.js';\n\nconst getWriteContents = (stdout: FakeStdout): string[] =>\n\tstdout\n\t\t.getWrites()\n\t\t.filter(w => !w.startsWith('\\u001B[?25') && !w.startsWith('\\u001B[?2026'));\n\ntest.serial(\n\t'useWindowSize returns current terminal dimensions and updates on resize',\n\tasync t => {\n\t\tconst stdout = createStdout(100);\n\t\t(stdout as any).rows = 40;\n\n\t\tfunction Test() {\n\t\t\tconst {columns, rows} = useWindowSize();\n\t\t\treturn (\n\t\t\t\t<Text>\n\t\t\t\t\t{columns}x{rows}\n\t\t\t\t</Text>\n\t\t\t);\n\t\t}\n\n\t\tconst {waitUntilRenderFlush} = render(<Test />, {stdout});\n\t\tawait waitUntilRenderFlush();\n\n\t\tt.true(stripAnsi(getWriteContents(stdout).at(-1)!).includes('100x40'));\n\n\t\t(stdout as any).columns = 60;\n\t\t(stdout as any).rows = 20;\n\t\tstdout.emit('resize');\n\t\tawait delay(100);\n\n\t\tt.true(stripAnsi(getWriteContents(stdout).at(-1)!).includes('60x20'));\n\t},\n);\n\ntest.serial('useWindowSize removes resize listener on unmount', async t => {\n\tconst stdout = createStdout(100);\n\t(stdout as any).rows = 24;\n\n\tfunction Test() {\n\t\tconst {columns, rows} = useWindowSize();\n\t\treturn (\n\t\t\t<Text>\n\t\t\t\t{columns}x{rows}\n\t\t\t</Text>\n\t\t);\n\t}\n\n\tconst initialListenerCount = stdout.listenerCount('resize');\n\tconst {unmount, waitUntilRenderFlush} = render(<Test />, {stdout});\n\tawait waitUntilRenderFlush();\n\n\tt.true(stdout.listenerCount('resize') > initialListenerCount);\n\tunmount();\n\n\tt.is(stdout.listenerCount('resize'), initialListenerCount);\n});\n\ntest.serial(\n\t'useWindowSize does not crash when resize fires after unmount',\n\tasync t => {\n\t\tconst stdout = createStdout(100);\n\t\t(stdout as any).rows = 24;\n\n\t\tfunction Test() {\n\t\t\tconst {columns, rows} = useWindowSize();\n\t\t\treturn (\n\t\t\t\t<Text>\n\t\t\t\t\t{columns}x{rows}\n\t\t\t\t</Text>\n\t\t\t);\n\t\t}\n\n\t\tconst {unmount, waitUntilRenderFlush} = render(<Test />, {stdout});\n\t\tawait waitUntilRenderFlush();\n\t\tunmount();\n\n\t\tstdout.emit('resize');\n\t\tawait delay(50);\n\n\t\tt.pass();\n\t},\n);\n\ntest.serial(\n\t'useWindowSize falls back to a positive column count when stdout.columns is 0',\n\tasync t => {\n\t\tconst stdout = createStdout(0);\n\t\tlet capturedColumns = -1;\n\n\t\tfunction Test() {\n\t\t\tconst {columns} = useWindowSize();\n\t\t\tcapturedColumns = columns;\n\t\t\treturn <Text>{columns}</Text>;\n\t\t}\n\n\t\tconst {waitUntilRenderFlush} = render(<Test />, {stdout});\n\t\tawait waitUntilRenderFlush();\n\n\t\tt.true(capturedColumns > 0);\n\t},\n);\n\ntest.serial(\n\t'useWindowSize falls back to terminal-size rows when stdout.rows is missing',\n\tasync t => {\n\t\tconst stdout = createStdout(0);\n\t\tlet capturedRows = -1;\n\t\tconst originalColumns = process.env.COLUMNS;\n\t\tconst originalLines = process.env.LINES;\n\t\tconst originalProcessStdoutColumns = process.stdout.columns;\n\t\tconst originalProcessStdoutRows = process.stdout.rows;\n\t\tconst originalProcessStderrColumns = process.stderr.columns;\n\t\tconst originalProcessStderrRows = process.stderr.rows;\n\n\t\tt.teardown(() => {\n\t\t\tprocess.env.COLUMNS = originalColumns;\n\t\t\tprocess.env.LINES = originalLines;\n\t\t\tprocess.stdout.columns = originalProcessStdoutColumns;\n\t\t\tprocess.stdout.rows = originalProcessStdoutRows;\n\t\t\tprocess.stderr.columns = originalProcessStderrColumns;\n\t\t\tprocess.stderr.rows = originalProcessStderrRows;\n\t\t});\n\n\t\tprocess.env.COLUMNS = '123';\n\t\tprocess.env.LINES = '45';\n\t\tprocess.stdout.columns = 0;\n\t\tprocess.stdout.rows = 0;\n\t\tprocess.stderr.columns = 0;\n\t\tprocess.stderr.rows = 0;\n\t\tdelete (stdout as any).rows;\n\n\t\tfunction Test() {\n\t\t\tconst {rows} = useWindowSize();\n\t\t\tcapturedRows = rows;\n\t\t\treturn <Text>{rows}</Text>;\n\t\t}\n\n\t\tconst {waitUntilRenderFlush} = render(<Test />, {stdout});\n\t\tawait waitUntilRenderFlush();\n\n\t\tt.is(capturedRows, 45);\n\t},\n);\n\ntest.serial('clear screen when terminal width decreases', async t => {\n\tconst stdout = createStdout(100);\n\n\tfunction Test() {\n\t\treturn (\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>Hello World</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\trender(<Test />, {stdout});\n\n\tconst initialOutput = stripAnsi(getWriteContents(stdout)[0]!);\n\tt.true(initialOutput.includes('Hello World'));\n\tt.true(initialOutput.includes('╭')); // Box border\n\n\t// Decrease width - should trigger clear and rerender\n\tstdout.columns = 50;\n\tstdout.emit('resize');\n\tawait delay(100);\n\n\t// Verify the output was updated for smaller width\n\tconst lastOutput = stripAnsi(getWriteContents(stdout).at(-1)!);\n\tt.true(lastOutput.includes('Hello World'));\n\tt.true(lastOutput.includes('╭')); // Box border\n\tt.not(initialOutput, lastOutput); // Output should change due to width\n});\n\ntest.serial('no screen clear when terminal width increases', async t => {\n\tconst stdout = createStdout(50);\n\n\tfunction Test() {\n\t\treturn (\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>Test</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\trender(<Test />, {stdout});\n\n\tconst initialOutput = getWriteContents(stdout)[0]!;\n\n\t// Increase width - should rerender but not clear\n\tstdout.columns = 100;\n\tstdout.emit('resize');\n\tawait delay(100);\n\n\tconst lastOutput = getWriteContents(stdout).at(-1)!;\n\n\t// When increasing width, we don't clear, so we should see eraseLines used for incremental update\n\t// But when decreasing, the clear() is called which also uses eraseLines\n\t// The key difference: decreasing width triggers an explicit clear before render\n\tt.not(stripAnsi(initialOutput), stripAnsi(lastOutput));\n\tt.true(stripAnsi(lastOutput).includes('Test'));\n});\n\ntest.serial(\n\t'consecutive width decreases trigger screen clear each time',\n\tasync t => {\n\t\tconst stdout = createStdout(100);\n\n\t\tfunction Test() {\n\t\t\treturn (\n\t\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t\t<Text>Content</Text>\n\t\t\t\t</Box>\n\t\t\t);\n\t\t}\n\n\t\trender(<Test />, {stdout});\n\n\t\tconst initialOutput = stripAnsi(getWriteContents(stdout)[0]!);\n\n\t\t// First decrease\n\t\tstdout.columns = 80;\n\t\tstdout.emit('resize');\n\t\tawait delay(100);\n\n\t\tconst afterFirstDecrease = stripAnsi(getWriteContents(stdout).at(-1)!);\n\t\tt.not(initialOutput, afterFirstDecrease);\n\t\tt.true(afterFirstDecrease.includes('Content'));\n\n\t\t// Second decrease\n\t\tstdout.columns = 60;\n\t\tstdout.emit('resize');\n\t\tawait delay(100);\n\n\t\tconst afterSecondDecrease = stripAnsi(getWriteContents(stdout).at(-1)!);\n\t\tt.not(afterFirstDecrease, afterSecondDecrease);\n\t\tt.true(afterSecondDecrease.includes('Content'));\n\t},\n);\n\ntest.serial('width decrease clears lastOutput to force rerender', async t => {\n\tconst stdout = createStdout(100);\n\n\tfunction Test() {\n\t\treturn (\n\t\t\t<Box borderStyle=\"round\">\n\t\t\t\t<Text>Test Content</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test />, {stdout});\n\n\tconst initialOutput = stripAnsi(getWriteContents(stdout)[0]!);\n\n\t// Decrease width - with a border, this will definitely change the output\n\tstdout.columns = 50;\n\tstdout.emit('resize');\n\tawait delay(100);\n\n\tconst afterResizeOutput = stripAnsi(getWriteContents(stdout).at(-1)!);\n\n\t// Outputs should be different because the border width changed\n\tt.not(initialOutput, afterResizeOutput);\n\tt.true(afterResizeOutput.includes('Test Content'));\n\n\t// Now try to rerender with a different component\n\trerender(\n\t\t<Box borderStyle=\"round\">\n\t\t\t<Text>Updated Content</Text>\n\t\t</Box>,\n\t);\n\tawait delay(100);\n\n\t// Verify content was updated\n\tt.true(\n\t\tstripAnsi(getWriteContents(stdout).at(-1)!).includes('Updated Content'),\n\t);\n});\n"
  },
  {
    "path": "test/text-width.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport stripAnsi from 'strip-ansi';\nimport {Box, Text} from '../src/index.js';\nimport {renderToString} from './helpers/render-to-string.js';\n\ntest('wide characters do not add extra space inside fixed-width Box', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Box>\n\t\t\t\t<Box width={2}>\n\t\t\t\t\t<Text>🍔</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>|</Text>\n\t\t\t</Box>\n\t\t\t<Box>\n\t\t\t\t<Box width={2}>\n\t\t\t\t\t<Text>⏳</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>|</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tconst lines = output.split('\\n');\n\tt.is(lines.length, 2);\n\tt.is(lines[0], '🍔|');\n\tt.is(lines[1], '⏳|');\n});\n\ntest('CJK characters occupy correct width in fixed-width Box', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Box width={4}>\n\t\t\t\t<Text>你好</Text>\n\t\t\t</Box>\n\t\t\t<Text>|</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '你好|');\n});\n\ntest('mixed ASCII and wide characters align correctly', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Box>\n\t\t\t\t<Box width={6}>\n\t\t\t\t\t<Text>ab🍔cd</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>|</Text>\n\t\t\t</Box>\n\t\t\t<Box>\n\t\t\t\t<Box width={6}>\n\t\t\t\t\t<Text>abcdef</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>|</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tconst lines = output.split('\\n');\n\tt.is(lines.length, 2);\n\tt.is(lines[0], 'ab🍔cd|');\n\tt.is(lines[1], 'abcdef|');\n});\n\ntest('ANSI styled text does not affect layout width', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Box width={5}>\n\t\t\t\t<Text color=\"red\">hello</Text>\n\t\t\t</Box>\n\t\t\t<Text>|</Text>\n\t\t</Box>,\n\t);\n\n\tconst stripped = stripAnsi(output);\n\tt.is(stripped, 'hello|');\n});\n\ntest('empty Text does not affect sibling layout', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Text />\n\t\t\t<Text>hello</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'hello');\n});\n"
  },
  {
    "path": "test/text.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport chalk from 'chalk';\nimport stripAnsi from 'strip-ansi';\nimport {render, Box, Text} from '../src/index.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\nimport createStdout from './helpers/create-stdout.js';\nimport {renderAsync} from './helpers/test-renderer.js';\n\nconst renderText = (text: string): string =>\n\trenderToString(\n\t\t<Box>\n\t\t\t<Text>{text}</Text>\n\t\t</Box>,\n\t);\n\ntest('<Text> with undefined children', t => {\n\tconst output = renderToString(<Text />);\n\tt.is(output, '');\n});\n\ntest('<Text> with null children', t => {\n\tconst output = renderToString(<Text>{null}</Text>);\n\tt.is(output, '');\n});\n\ntest('text with standard color', t => {\n\tconst output = renderToString(<Text color=\"green\">Test</Text>);\n\tt.is(output, chalk.green('Test'));\n});\n\ntest('text with dim+bold', t => {\n\tconst originalLevel = chalk.level;\n\tchalk.level = 3;\n\tt.teardown(() => {\n\t\tchalk.level = originalLevel;\n\t});\n\n\tconst output = renderToString(\n\t\t<Text dimColor bold>\n\t\t\tTest\n\t\t</Text>,\n\t);\n\n\tt.is(stripAnsi(output), 'Test');\n\tt.not(output, 'Test'); // Ensure ANSI codes are present\n});\n\ntest('text with dimmed color', t => {\n\tconst output = renderToString(\n\t\t<Text dimColor color=\"green\">\n\t\t\tTest\n\t\t</Text>,\n\t);\n\n\tt.is(output, chalk.green.dim('Test'));\n});\n\ntest('text with hex color', t => {\n\tconst output = renderToString(<Text color=\"#FF8800\">Test</Text>);\n\tt.is(output, chalk.hex('#FF8800')('Test'));\n});\n\ntest('text with rgb color', t => {\n\tconst output = renderToString(<Text color=\"rgb(255, 136, 0)\">Test</Text>);\n\tt.is(output, chalk.rgb(255, 136, 0)('Test'));\n});\n\ntest('text with ansi256 color', t => {\n\tconst output = renderToString(<Text color=\"ansi256(194)\">Test</Text>);\n\tt.is(output, chalk.ansi256(194)('Test'));\n});\n\ntest('text with standard background color', t => {\n\tconst output = renderToString(<Text backgroundColor=\"green\">Test</Text>);\n\tt.is(output, chalk.bgGreen('Test'));\n});\n\ntest('text with hex background color', t => {\n\tconst output = renderToString(<Text backgroundColor=\"#FF8800\">Test</Text>);\n\tt.is(output, chalk.bgHex('#FF8800')('Test'));\n});\n\ntest('text with rgb background color', t => {\n\tconst output = renderToString(\n\t\t<Text backgroundColor=\"rgb(255, 136, 0)\">Test</Text>,\n\t);\n\n\tt.is(output, chalk.bgRgb(255, 136, 0)('Test'));\n});\n\ntest('text with ansi256 background color', t => {\n\tconst output = renderToString(\n\t\t<Text backgroundColor=\"ansi256(194)\">Test</Text>,\n\t);\n\n\tt.is(output, chalk.bgAnsi256(194)('Test'));\n});\n\ntest('text with inversion', t => {\n\tconst output = renderToString(<Text inverse>Test</Text>);\n\tt.is(output, chalk.inverse('Test'));\n});\n\n// See https://github.com/vadimdemedes/ink/issues/867\ntest('text with empty-to-nonempty sibling does not wrap', t => {\n\tfunction Test({show}: {readonly show?: boolean}) {\n\t\treturn (\n\t\t\t<Box>\n\t\t\t\t<Text>\n\t\t\t\t\t{show ? 'x' : ''}\n\t\t\t\t\t{'hello'}\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst stdout = createStdout();\n\tconst {rerender} = render(<Test />, {stdout, debug: true});\n\tt.is((stdout.write as any).lastCall.args[0], 'hello');\n\n\trerender(<Test show />);\n\tt.is((stdout.write as any).lastCall.args[0], 'xhello');\n});\n\ntest('remeasure text when text is changed', t => {\n\tfunction Test({add}: {readonly add?: boolean}) {\n\t\treturn (\n\t\t\t<Box>\n\t\t\t\t<Text>{add ? 'abcx' : 'abc'}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst stdout = createStdout();\n\tconst {rerender} = render(<Test />, {stdout, debug: true});\n\tt.is((stdout.write as any).lastCall.args[0], 'abc');\n\n\trerender(<Test add />);\n\tt.is((stdout.write as any).lastCall.args[0], 'abcx');\n});\n\ntest('remeasure text when text nodes are changed', t => {\n\tfunction Test({add}: {readonly add?: boolean}) {\n\t\treturn (\n\t\t\t<Box>\n\t\t\t\t<Text>\n\t\t\t\t\tabc\n\t\t\t\t\t{add ? <Text>x</Text> : null}\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst stdout = createStdout();\n\n\tconst {rerender} = render(<Test />, {stdout, debug: true});\n\tt.is((stdout.write as any).lastCall.args[0], 'abc');\n\n\trerender(<Test add />);\n\tt.is((stdout.write as any).lastCall.args[0], 'abcx');\n});\n\n// See https://github.com/vadimdemedes/ink/issues/743\n// Without the fix, the output was ''.\ntest('text with content \"constructor\" wraps correctly', t => {\n\tconst output = renderToString(<Text>constructor</Text>);\n\tt.is(output, 'constructor');\n});\n\n// See https://github.com/vadimdemedes/ink/issues/362\ntest('strip ANSI cursor movement sequences from text', t => {\n\t// \\x1b[1A = cursor up, \\x1b[2K = clear line, \\x1b[1B = cursor down\n\t// \\x1b[32m = green (SGR, preserved), \\x1b[0m = reset (SGR, preserved)\n\tconst input =\n\t\t'\\u001B[1A\\u001B[2KStarting client ... \\u001B[32mdone\\u001B[0m\\u001B[1B';\n\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Text>{input}</Text>\n\t\t</Box>,\n\t);\n\n\tt.false(output.includes('\\u001B[1A'));\n\tt.false(output.includes('\\u001B[2K'));\n\tt.false(output.includes('\\u001B[1B'));\n\tt.is(stripAnsi(output), 'Starting client ... done');\n});\n\ntest('strip ANSI cursor position and erase sequences from text', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Text>{'Hello\\u001B[5;10HWorld\\u001B[2J!'}</Text>\n\t\t</Box>,\n\t);\n\n\tt.false(output.includes('\\u001B[5;10H'));\n\tt.false(output.includes('\\u001B[2J'));\n\tt.is(stripAnsi(output), 'HelloWorld!');\n});\n\ntest('preserve SGR color sequences in text', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Text>{'\\u001B[32mgreen\\u001B[0m normal'}</Text>\n\t\t</Box>,\n\t);\n\n\tt.true(output.includes('\\u001B['));\n\tt.is(stripAnsi(output), 'green normal');\n});\n\ntest('preserve OSC hyperlink sequences in text', t => {\n\tconst output = renderText(\n\t\t'\\u001B]8;;https://example.com\\u0007link\\u001B]8;;\\u0007',\n\t);\n\n\tt.true(output.includes('\\u001B]8;;'));\n\tt.is(stripAnsi(output), 'link');\n});\n\ntest('preserve OSC hyperlink sequences with ST terminator in text', t => {\n\tconst output = renderText(\n\t\t'\\u001B]8;;https://example.com\\u001B\\\\link\\u001B]8;;\\u001B\\\\',\n\t);\n\n\tt.true(output.includes('\\u001B]8;;'));\n\tt.true(output.includes('\\u001B\\\\'));\n\tt.is(stripAnsi(output), 'link');\n});\n\ntest('preserve C1 OSC sequences in text', t => {\n\tconst input = '\\u009D8;;https://example.com\\u0007link\\u009D8;;\\u0007';\n\tconst output = renderText(input);\n\n\tt.true(output.includes('\\u009D8;;https://example.com'));\n\tt.true(output.includes('\\u009D8;;\\u0007'));\n\tt.is(output, input);\n});\n\ntest('preserve C1 OSC hyperlink sequences with ST terminator in text', t => {\n\tconst input = '\\u009D8;;https://example.com\\u001B\\\\link\\u009D8;;\\u001B\\\\';\n\tconst output = renderText(input);\n\n\tt.true(output.includes('\\u009D8;;https://example.com'));\n\tt.true(output.includes('\\u001B\\\\'));\n\tt.is(output, input);\n});\n\ntest('preserve SGR sequences with colon parameters', t => {\n\tconst output = renderText('A\\u001B[38:2::255:100:0mcolor\\u001B[0mB');\n\n\tt.true(output.includes('\\u001B[38:2::255:100:0m'));\n\tt.is(stripAnsi(output), 'AcolorB');\n});\n\ntest('strip complete non-SGR CSI sequences without leaking parameters', t => {\n\tconst input = 'A\\u001B[>4;2mB\\u001B[2 qC';\n\tconst output = renderText(input);\n\n\tt.false(output.includes('4;2m'));\n\tt.false(output.includes(' q'));\n\tt.is(stripAnsi(output), 'ABC');\n});\n\ntest('strip complete C1 non-SGR CSI sequences without leaking parameters', t => {\n\tconst output = renderText('A\\u009B>4;2mB\\u009B2 qC');\n\n\tt.false(output.includes('4;2m'));\n\tt.false(output.includes(' q'));\n\tt.is(stripAnsi(output), 'ABC');\n});\n\ntest('strip complete ESC control sequences with intermediates', t => {\n\tconst output = renderText('A\\u001B#8B\\u001BcC');\n\n\tt.false(output.includes('\\u001B#8'));\n\tt.false(output.includes('\\u001Bc'));\n\tt.is(stripAnsi(output), 'ABC');\n});\n\ntest('strip tmux DCS passthrough wrappers without leaking payload', t => {\n\tconst wrappedHyperlinkStart =\n\t\t'\\u001BPtmux;\\u001B\\u001B]8;;https://example.com\\u0007\\u001B\\\\';\n\tconst wrappedHyperlinkEnd = '\\u001BPtmux;\\u001B\\u001B]8;;\\u0007\\u001B\\\\';\n\tconst output = renderText(\n\t\t`${wrappedHyperlinkStart}link${wrappedHyperlinkEnd}`,\n\t);\n\n\tt.false(output.includes('tmux;'));\n\tt.false(output.includes('\\u001BP'));\n\tt.false(output.includes('\\u001B\\\\'));\n\tt.is(stripAnsi(output), 'link');\n});\n\ntest('strip tmux DCS passthrough wrappers with ST-terminated OSC payload', t => {\n\tconst wrappedHyperlinkStart =\n\t\t'\\u001BPtmux;\\u001B\\u001B]8;;https://example.com\\u001B\\u001B\\\\\\u001B\\\\';\n\tconst wrappedHyperlinkEnd =\n\t\t'\\u001BPtmux;\\u001B\\u001B]8;;\\u001B\\u001B\\\\\\u001B\\\\';\n\tconst output = renderText(\n\t\t`${wrappedHyperlinkStart}link${wrappedHyperlinkEnd}`,\n\t);\n\n\tt.false(output.includes('tmux;'));\n\tt.false(output.includes('\\u001B\\\\'));\n\tt.is(stripAnsi(output), 'link');\n});\n\ntest('strip C1 DCS control strings as complete units', t => {\n\tconst output = renderText('A\\u0090payload\\u001B\\\\B\\u0090payload\\u009CC');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'ABC');\n});\n\ntest('strip PM and APC control strings as complete units', t => {\n\tconst output = renderText(\n\t\t'A\\u001B^pm-payload\\u001B\\\\B\\u001B_apc-payload\\u001B\\\\C',\n\t);\n\n\tt.false(output.includes('pm-payload'));\n\tt.false(output.includes('apc-payload'));\n\tt.is(stripAnsi(output), 'ABC');\n});\n\ntest('strip C1 PM and APC control strings as complete units', t => {\n\tconst output = renderText('A\\u009Epm-payload\\u009CB\\u009Fapc-payload\\u009CC');\n\n\tt.false(output.includes('pm-payload'));\n\tt.false(output.includes('apc-payload'));\n\tt.is(stripAnsi(output), 'ABC');\n});\n\ntest('strip ESC SOS control strings as complete units', t => {\n\tconst output = renderText('A\\u001BXpayload\\u001B\\\\B');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('strip C1 SOS control strings as complete units', t => {\n\tconst output = renderText('A\\u0098payload\\u001B\\\\B\\u0098payload\\u009CC');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'ABC');\n});\n\ntest('strip malformed SOS control strings to avoid payload leaks', t => {\n\tconst output = renderText('A\\u001BXpayload\\u0007B\\u0098payload');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'A');\n});\n\ntest('preserve SGR sequences around stripped SOS control strings', t => {\n\tconst output = renderText('A\\u001B[32mgreen\\u001B[0m\\u001BXpayload\\u001B\\\\B');\n\n\tt.true(output.includes('\\u001B['));\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'AgreenB');\n});\n\ntest('strip tmux DCS passthrough containing BEL until the final ST terminator', t => {\n\tconst input = 'A\\u001BPtmux;\\u001B\\u001B]0;title\\u0007\\u001B\\\\B';\n\tconst output = renderText(input);\n\n\tt.false(output.includes('tmux;'));\n\tt.false(output.includes('title'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('strip incomplete DCS passthrough sequences to avoid payload leaks', t => {\n\tconst incompleteSequence = '\\u001BPtmux;\\u001B';\n\tconst output = renderText(`${incompleteSequence}link`);\n\n\tt.false(output.includes('tmux;'));\n\tt.is(stripAnsi(output), '');\n});\n\ntest('strip incomplete C1 DCS control strings to avoid payload leaks', t => {\n\tconst output = renderText('A\\u0090payload');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'A');\n});\n\ntest('strip incomplete OSC control strings to avoid payload leaks', t => {\n\tconst output = renderText('A\\u001B]8;;https://example.comlink');\n\n\tt.false(output.includes('https://example.com'));\n\tt.is(stripAnsi(output), 'A');\n});\n\ntest('strip incomplete C1 OSC control strings to avoid payload leaks', t => {\n\tconst output = renderText('A\\u009D8;;https://example.comlink');\n\n\tt.false(output.includes('https://example.com'));\n\tt.is(stripAnsi(output), 'A');\n});\n\ntest('strip incomplete ESC control sequences with intermediates to avoid payload leaks', t => {\n\tconst output = renderText('A\\u001B#');\n\n\tt.false(output.includes('\\u001B#'));\n\tt.is(stripAnsi(output), 'A');\n});\n\ntest('strip malformed ESC control sequences with intermediates and non-final bytes', t => {\n\tconst output = renderText('A\\u001B#\\u0007payload');\n\n\tt.false(output.includes('payload'));\n\tt.is(stripAnsi(output), 'A');\n});\n\ntest('strip standalone ST bytes from text output', t => {\n\tconst output = renderText('A\\u009CB');\n\n\tt.false(output.includes('\\u009C'));\n\tt.is(stripAnsi(output), 'AB');\n});\n\ntest('strip standalone C1 control characters from text output', t => {\n\tconst output = renderText('A\\u0085B\\u008EC');\n\n\tt.false(output.includes('\\u0085'));\n\tt.false(output.includes('\\u008E'));\n\tt.is(stripAnsi(output), 'ABC');\n});\n\n// Concurrent mode tests\ntest('<Text> with undefined children - concurrent', async t => {\n\tconst output = await renderToStringAsync(<Text />);\n\tt.is(output, '');\n});\n\ntest('<Text> with null children - concurrent', async t => {\n\tconst output = await renderToStringAsync(<Text>{null}</Text>);\n\tt.is(output, '');\n});\n\ntest('text with standard color - concurrent', async t => {\n\tconst output = await renderToStringAsync(<Text color=\"green\">Test</Text>);\n\tt.is(output, chalk.green('Test'));\n});\n\ntest('text with dim+bold - concurrent', async t => {\n\tconst originalLevel = chalk.level;\n\tchalk.level = 3;\n\tt.teardown(() => {\n\t\tchalk.level = originalLevel;\n\t});\n\n\tconst output = await renderToStringAsync(\n\t\t<Text dimColor bold>\n\t\t\tTest\n\t\t</Text>,\n\t);\n\n\tt.is(stripAnsi(output), 'Test');\n\tt.not(output, 'Test'); // Ensure ANSI codes are present\n});\n\ntest('text with hex color - concurrent', async t => {\n\tconst output = await renderToStringAsync(<Text color=\"#FF8800\">Test</Text>);\n\tt.is(output, chalk.hex('#FF8800')('Test'));\n});\n\ntest('text with inversion - concurrent', async t => {\n\tconst output = await renderToStringAsync(<Text inverse>Test</Text>);\n\tt.is(output, chalk.inverse('Test'));\n});\n\ntest('remeasure text when text is changed - concurrent', async t => {\n\tfunction Test({add}: {readonly add?: boolean}) {\n\t\treturn (\n\t\t\t<Box>\n\t\t\t\t<Text>{add ? 'abcx' : 'abc'}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {getOutput, rerenderAsync} = await renderAsync(<Test />);\n\tt.is(getOutput(), 'abc');\n\n\tawait rerenderAsync(<Test add />);\n\tt.is(getOutput(), 'abcx');\n});\n\ntest('remeasure text when text nodes are changed - concurrent', async t => {\n\tfunction Test({add}: {readonly add?: boolean}) {\n\t\treturn (\n\t\t\t<Box>\n\t\t\t\t<Text>\n\t\t\t\t\tabc\n\t\t\t\t\t{add ? <Text>x</Text> : null}\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {getOutput, rerenderAsync} = await renderAsync(<Test />);\n\tt.is(getOutput(), 'abc');\n\n\tawait rerenderAsync(<Test add />);\n\tt.is(getOutput(), 'abcx');\n});\n"
  },
  {
    "path": "test/tsconfig.json",
    "content": "{\n\t\"extends\": \"../tsconfig.json\",\n\t\"include\": [\".\"]\n}\n"
  },
  {
    "path": "test/use-box-metrics.tsx",
    "content": "import React, {useRef, useState} from 'react';\nimport test from 'ava';\nimport delay from 'delay';\nimport stripAnsi from 'strip-ansi';\nimport {\n\tBox,\n\tText,\n\trender,\n\tuseBoxMetrics,\n\ttype DOMElement,\n} from '../src/index.js';\nimport createStdout from './helpers/create-stdout.js';\n\ntest('returns correct size on first render', async t => {\n\tconst stdout = createStdout(100);\n\n\tfunction Test() {\n\t\tconst ref = useRef<DOMElement>(null);\n\t\tconst {width, height} = useBoxMetrics(ref);\n\t\treturn (\n\t\t\t<Box ref={ref}>\n\t\t\t\t<Text>\n\t\t\t\t\t{width}x{height}\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\t// Width fills terminal (100); single-line text renders as height 1\n\tt.true(stripAnsi(stdout.get()).includes('100x1'));\n});\n\ntest('returns correct position', async t => {\n\tconst stdout = createStdout(100);\n\n\tfunction Test() {\n\t\tconst ref = useRef<DOMElement>(null);\n\t\tconst {left, top} = useBoxMetrics(ref);\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Text>first line</Text>\n\t\t\t\t<Box ref={ref} marginLeft={5}>\n\t\t\t\t\t<Text>\n\t\t\t\t\t\t{left},{top}\n\t\t\t\t\t</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\t// MarginLeft=5 → left=5; second row → top=1\n\tt.true(stripAnsi(stdout.get()).includes('5,1'));\n});\n\ntest('updates when terminal is resized', async t => {\n\tconst stdout = createStdout(100);\n\n\tfunction Test() {\n\t\tconst ref = useRef<DOMElement>(null);\n\t\tconst {width} = useBoxMetrics(ref);\n\t\treturn (\n\t\t\t<Box ref={ref}>\n\t\t\t\t<Text>Width: {width}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Width: 100'));\n\n\t(stdout as any).columns = 60;\n\tstdout.emit('resize');\n\tawait delay(200);\n\n\tt.true(stripAnsi(stdout.get()).includes('Width: 60'));\n});\n\ntest('uses latest tracked ref when terminal is resized', async t => {\n\tconst stdout = createStdout(100);\n\tlet trackSecondRef!: () => void;\n\n\tfunction Test() {\n\t\tconst firstRef = useRef<DOMElement>(null);\n\t\tconst secondRef = useRef<DOMElement>(null);\n\t\tconst [isSecondRefTracked, setIsSecondRefTracked] = useState(false);\n\t\tconst trackedRef = isSecondRefTracked ? secondRef : firstRef;\n\t\tconst {height} = useBoxMetrics(trackedRef);\n\n\t\ttrackSecondRef = () => {\n\t\t\tsetIsSecondRefTracked(true);\n\t\t};\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Box ref={firstRef}>\n\t\t\t\t\t<Text>short</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Box ref={secondRef}>\n\t\t\t\t\t<Text>\n\t\t\t\t\t\tABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\n\t\t\t\t\t</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>Tracked height: {height}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Tracked height: 1'));\n\n\ttrackSecondRef();\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Tracked height: 1'));\n\n\t(stdout as any).columns = 20;\n\tstdout.emit('resize');\n\tawait delay(200);\n\n\tt.true(stripAnsi(stdout.get()).includes('Tracked height: 4'));\n});\n\ntest('updates when sibling content changes', async t => {\n\tconst stdout = createStdout(100);\n\tlet externalSetSiblingText!: (text: string) => void;\n\n\tfunction Test() {\n\t\tconst ref = useRef<DOMElement>(null);\n\t\tconst [siblingText, setSiblingText] = useState('short');\n\t\tconst {height} = useBoxMetrics(ref);\n\n\t\texternalSetSiblingText = setSiblingText;\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Box ref={ref} flexDirection=\"column\">\n\t\t\t\t\t<Text>{siblingText}</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>Height: {height}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Height: 1'));\n\n\texternalSetSiblingText('line 1\\nline 2\\nline 3');\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Height: 3'));\n});\n\ntest('updates when sibling content changes but tracked component is memoized', async t => {\n\tconst stdout = createStdout(100);\n\tlet externalSetSiblingText!: (text: string) => void;\n\n\tconst MemoizedTrackedBox = React.memo(function () {\n\t\tconst ref = useRef<DOMElement>(null);\n\t\tconst {top} = useBoxMetrics(ref);\n\n\t\treturn (\n\t\t\t<Box ref={ref}>\n\t\t\t\t<Text>Top: {top}</Text>\n\t\t\t</Box>\n\t\t);\n\t});\n\n\tfunction Test() {\n\t\tconst [siblingText, setSiblingText] = useState('line 1');\n\t\texternalSetSiblingText = setSiblingText;\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Text>{siblingText}</Text>\n\t\t\t\t<MemoizedTrackedBox />\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Top: 1'));\n\n\texternalSetSiblingText('line 1\\nline 2\\nline 3');\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Top: 3'));\n});\n\ntest('updates when tracked ref attaches after initial render and component is memoized', async t => {\n\tconst stdout = createStdout(100);\n\tlet externalSetSiblingText!: (text: string) => void;\n\tlet externalSetIsTrackedElementMounted!: (value: boolean) => void;\n\n\tconst MemoizedTrackedBox = React.memo(function ({\n\t\tisTrackedElementMounted,\n\t}: {\n\t\treadonly isTrackedElementMounted: boolean;\n\t}) {\n\t\tconst ref = useRef<DOMElement>(null);\n\t\tconst {top} = useBoxMetrics(ref);\n\n\t\treturn isTrackedElementMounted ? (\n\t\t\t<Box ref={ref}>\n\t\t\t\t<Text>Top: {top}</Text>\n\t\t\t</Box>\n\t\t) : (\n\t\t\t<Text>Top: {top}</Text>\n\t\t);\n\t});\n\n\tfunction Test() {\n\t\tconst [siblingText, setSiblingText] = useState('line 1');\n\t\tconst [isTrackedElementMounted, setIsTrackedElementMounted] =\n\t\t\tuseState(false);\n\t\texternalSetSiblingText = setSiblingText;\n\t\texternalSetIsTrackedElementMounted = setIsTrackedElementMounted;\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Text>{siblingText}</Text>\n\t\t\t\t<MemoizedTrackedBox isTrackedElementMounted={isTrackedElementMounted} />\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Top: 0'));\n\n\texternalSetIsTrackedElementMounted(true);\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Top: 1'));\n\n\texternalSetSiblingText('line 1\\nline 2\\nline 3');\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Top: 3'));\n});\n\ntest('does not trigger extra re-renders when layout is unchanged', async t => {\n\tconst stdout = createStdout(100);\n\tlet renderCount = 0;\n\n\tfunction Test() {\n\t\tconst ref = useRef<DOMElement>(null);\n\t\tuseBoxMetrics(ref);\n\t\trenderCount++;\n\t\treturn (\n\t\t\t<Box ref={ref}>\n\t\t\t\t<Text>Hello</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(100);\n\n\t// Renders settle at 2: initial render (layout all zeros) → setLayout triggers\n\t// re-render (layout measured) → bail-out prevents any further renders.\n\tt.true(renderCount >= 2 && renderCount <= 3);\n});\n\nfunction SimpleBox() {\n\tconst ref = useRef<DOMElement>(null);\n\tuseBoxMetrics(ref);\n\treturn (\n\t\t<Box ref={ref}>\n\t\t\t<Text>Hello</Text>\n\t\t</Box>\n\t);\n}\n\ntest.serial('removes resize listener on unmount', async t => {\n\tconst stdout = createStdout(100);\n\n\tconst initialListenerCount = stdout.listenerCount('resize');\n\tconst {unmount, waitUntilRenderFlush} = render(<SimpleBox />, {stdout});\n\tawait waitUntilRenderFlush();\n\n\tt.true(stdout.listenerCount('resize') > initialListenerCount);\n\tunmount();\n\n\tt.is(stdout.listenerCount('resize'), initialListenerCount);\n});\n\ntest.serial('does not crash when resize fires after unmount', async t => {\n\tconst stdout = createStdout(100);\n\n\tconst {unmount, waitUntilRenderFlush} = render(<SimpleBox />, {stdout});\n\tawait waitUntilRenderFlush();\n\tunmount();\n\n\tstdout.emit('resize');\n\tawait delay(50);\n\n\tt.pass();\n});\n\ntest('returns zeros when ref is not attached', async t => {\n\tconst stdout = createStdout(100);\n\n\tfunction Test() {\n\t\tconst ref = useRef<DOMElement>(null);\n\t\tconst {width, height, left, top, hasMeasured} = useBoxMetrics(ref);\n\t\treturn (\n\t\t\t<Box>\n\t\t\t\t<Text>\n\t\t\t\t\t{width},{height},{left},{top},{String(hasMeasured)}\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('0,0,0,0,false'));\n});\n\ntest('hasMeasured becomes true when tracked element is mounted on initial render', async t => {\n\tconst stdout = createStdout(100);\n\n\tfunction Test() {\n\t\tconst ref = useRef<DOMElement>(null);\n\t\tconst {hasMeasured} = useBoxMetrics(ref);\n\n\t\treturn (\n\t\t\t<Box ref={ref}>\n\t\t\t\t<Text>Has measured: {String(hasMeasured)}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Has measured: true'));\n});\n\ntest('hasMeasured resets when tracked ref switches to a detached element', async t => {\n\tconst stdout = createStdout(100);\n\tlet trackSecondRef!: () => void;\n\tlet mountSecondRef!: () => void;\n\n\tfunction Test() {\n\t\tconst firstRef = useRef<DOMElement>(null);\n\t\tconst secondRef = useRef<DOMElement>(null);\n\t\tconst [isSecondRefTracked, setIsSecondRefTracked] = useState(false);\n\t\tconst [isSecondRefMounted, setIsSecondRefMounted] = useState(false);\n\t\tconst trackedRef = isSecondRefTracked ? secondRef : firstRef;\n\t\tconst {hasMeasured} = useBoxMetrics(trackedRef);\n\n\t\ttrackSecondRef = () => {\n\t\t\tsetIsSecondRefTracked(true);\n\t\t};\n\n\t\tmountSecondRef = () => {\n\t\t\tsetIsSecondRefMounted(true);\n\t\t};\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Box ref={firstRef}>\n\t\t\t\t\t<Text>First</Text>\n\t\t\t\t</Box>\n\t\t\t\t{isSecondRefMounted ? (\n\t\t\t\t\t<Box ref={secondRef}>\n\t\t\t\t\t\t<Text>Second</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t) : undefined}\n\t\t\t\t<Text>Has measured: {String(hasMeasured)}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Has measured: true'));\n\n\ttrackSecondRef();\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Has measured: false'));\n\n\tmountSecondRef();\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Has measured: true'));\n});\n\ntest('hasMeasured becomes true after the tracked element is measured', async t => {\n\tconst stdout = createStdout(100);\n\tlet mountTrackedElement!: () => void;\n\n\tfunction Test() {\n\t\tconst ref = useRef<DOMElement>(null);\n\t\tconst [isTrackedElementMounted, setIsTrackedElementMounted] =\n\t\t\tuseState(false);\n\t\tconst {hasMeasured} = useBoxMetrics(ref);\n\n\t\tmountTrackedElement = () => {\n\t\t\tsetIsTrackedElementMounted(true);\n\t\t};\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t{isTrackedElementMounted ? (\n\t\t\t\t\t<Box ref={ref}>\n\t\t\t\t\t\t<Text>Tracked</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t) : undefined}\n\t\t\t\t<Text>Has measured: {String(hasMeasured)}</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Has measured: false'));\n\n\tmountTrackedElement();\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Has measured: true'));\n});\n\ntest('resets metrics when tracked element unmounts', async t => {\n\tconst stdout = createStdout(100);\n\tlet unmountTrackedElement!: () => void;\n\n\tfunction Test() {\n\t\tconst ref = useRef<DOMElement>(null);\n\t\tconst [isTrackedElementMounted, setIsTrackedElementMounted] =\n\t\t\tuseState(true);\n\t\tconst {width, height, left, top, hasMeasured} = useBoxMetrics(ref);\n\n\t\tunmountTrackedElement = () => {\n\t\t\tsetIsTrackedElementMounted(false);\n\t\t};\n\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t{isTrackedElementMounted ? (\n\t\t\t\t\t<Box ref={ref} width={10}>\n\t\t\t\t\t\t<Text>1234567890</Text>\n\t\t\t\t\t</Box>\n\t\t\t\t) : undefined}\n\t\t\t\t<Text>\n\t\t\t\t\tMetrics: {width},{height},{left},{top},{String(hasMeasured)}\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {waitUntilRenderFlush} = render(<Test />, {stdout, debug: true});\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Metrics: 10,1,0,0,true'));\n\n\tunmountTrackedElement();\n\tawait waitUntilRenderFlush();\n\tawait delay(50);\n\n\tt.true(stripAnsi(stdout.get()).includes('Metrics: 0,0,0,0,false'));\n});\n"
  },
  {
    "path": "test/width-height.tsx",
    "content": "import React from 'react';\nimport test from 'ava';\nimport {Box, Text, render} from '../src/index.js';\nimport {\n\trenderToString,\n\trenderToStringAsync,\n} from './helpers/render-to-string.js';\nimport createStdout from './helpers/create-stdout.js';\n\ntest('set width', t => {\n\tconst output = renderToString(\n\t\t<Box>\n\t\t\t<Box width={5}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A    B');\n});\n\ntest('set width in percent', t => {\n\tconst output = renderToString(\n\t\t<Box width={10}>\n\t\t\t<Box width=\"50%\">\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A    B');\n});\n\ntest('set min width', t => {\n\tconst smallerOutput = renderToString(\n\t\t<Box>\n\t\t\t<Box minWidth={5}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(smallerOutput, 'A    B');\n\n\tconst largerOutput = renderToString(\n\t\t<Box>\n\t\t\t<Box minWidth={2}>\n\t\t\t\t<Text>AAAAA</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(largerOutput, 'AAAAAB');\n});\n\ntest.failing('set min width in percent', t => {\n\tconst output = renderToString(\n\t\t<Box width={10}>\n\t\t\t<Box minWidth=\"50%\">\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A    B');\n});\n\ntest('set height', t => {\n\tconst output = renderToString(\n\t\t<Box height={4}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'AB\\n\\n\\n');\n});\n\ntest('set height in percent', t => {\n\tconst output = renderToString(\n\t\t<Box height={6} flexDirection=\"column\">\n\t\t\t<Box height=\"50%\">\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\n\\n\\nB\\n\\n');\n});\n\ntest('cut text over the set height', t => {\n\tconst output = renderToString(\n\t\t<Box height={2}>\n\t\t\t<Text>AAAABBBBCCCC</Text>\n\t\t</Box>,\n\t\t{columns: 4},\n\t);\n\n\tt.is(output, 'AAAA\\nBBBB');\n});\n\ntest('set min height', t => {\n\tconst smallerOutput = renderToString(\n\t\t<Box minHeight={4}>\n\t\t\t<Text>A</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(smallerOutput, 'A\\n\\n\\n');\n\n\tconst largerOutput = renderToString(\n\t\t<Box minHeight={2}>\n\t\t\t<Box height={4}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(largerOutput, 'A\\n\\n\\n');\n});\n\ntest('set min height in percent', t => {\n\tconst output = renderToString(\n\t\t<Box height={6} flexDirection=\"column\">\n\t\t\t<Box minHeight=\"50%\">\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\n\\n\\nB\\n\\n');\n});\n\ntest('set max width', t => {\n\tconst constrainedOutput = renderToString(\n\t\t<Box>\n\t\t\t<Box maxWidth={3}>\n\t\t\t\t<Text>AAAAA</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t\t{columns: 10},\n\t);\n\n\tt.is(constrainedOutput, 'AAAB\\nAA');\n\n\tconst unconstrainedOutput = renderToString(\n\t\t<Box>\n\t\t\t<Box maxWidth={10}>\n\t\t\t\t<Text>AAA</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(unconstrainedOutput, 'AAAB');\n});\n\ntest('clears maxWidth on rerender', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({maxWidth}: {readonly maxWidth?: number}) {\n\t\treturn (\n\t\t\t<Box>\n\t\t\t\t<Box maxWidth={maxWidth}>\n\t\t\t\t\t<Text>AAAAA</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>B</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test maxWidth={3} />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is(stdout.write.lastCall.args[0], 'AAAB\\nAA');\n\n\trerender(<Test maxWidth={undefined} />);\n\tt.is(stdout.write.lastCall.args[0], 'AAAAAB');\n});\n\ntest('set max height', t => {\n\tconst constrainedOutput = renderToString(\n\t\t<Box maxHeight={2}>\n\t\t\t<Box height={4}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t</Box>,\n\t);\n\n\tt.is(constrainedOutput, 'A\\n');\n\n\tconst unconstrainedOutput = renderToString(\n\t\t<Box maxHeight={4}>\n\t\t\t<Text>A</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(unconstrainedOutput, 'A');\n});\n\ntest('clears maxHeight on rerender', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({maxHeight}: {readonly maxHeight?: number}) {\n\t\treturn (\n\t\t\t<Box maxHeight={maxHeight}>\n\t\t\t\t<Box height={4}>\n\t\t\t\t\t<Text>A</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test maxHeight={2} />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is(stdout.write.lastCall.args[0], 'A\\n');\n\n\trerender(<Test maxHeight={undefined} />);\n\tt.is(stdout.write.lastCall.args[0], 'A\\n\\n\\n');\n});\n\ntest('set aspect ratio with width', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Box width={8} aspectRatio={2} borderStyle=\"single\">\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t\t<Text>Y</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '┌──────┐\\n│X     │\\n│      │\\n└──────┘\\nY');\n});\n\ntest('set aspect ratio with height', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Box height={3} aspectRatio={2} borderStyle=\"single\">\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t\t<Text>Y</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '┌────┐\\n│X   │\\n└────┘\\nY');\n});\n\ntest('set aspect ratio with width and height', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Box width={8} height={3} aspectRatio={2} borderStyle=\"single\">\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t\t<Text>Y</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '┌────┐\\n│X   │\\n└────┘\\nY');\n});\n\ntest('set aspect ratio with maxHeight constraint', t => {\n\tconst output = renderToString(\n\t\t<Box flexDirection=\"column\">\n\t\t\t<Box width={10} maxHeight={3} aspectRatio={2} borderStyle=\"single\">\n\t\t\t\t<Text>X</Text>\n\t\t\t</Box>\n\t\t\t<Text>Y</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, '┌────┐\\n│X   │\\n└────┘\\nY');\n});\n\ntest('clears aspectRatio on rerender', t => {\n\tconst stdout = createStdout();\n\n\tfunction Test({aspectRatio}: {readonly aspectRatio?: number}) {\n\t\treturn (\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Box width={8} aspectRatio={aspectRatio} borderStyle=\"single\">\n\t\t\t\t\t<Text>X</Text>\n\t\t\t\t</Box>\n\t\t\t\t<Text>Y</Text>\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tconst {rerender} = render(<Test aspectRatio={2} />, {\n\t\tstdout,\n\t\tdebug: true,\n\t});\n\n\tt.is(\n\t\tstdout.write.lastCall.args[0],\n\t\t'┌──────┐\\n│X     │\\n│      │\\n└──────┘\\nY',\n\t);\n\n\trerender(<Test aspectRatio={undefined} />);\n\tt.is(stdout.write.lastCall.args[0], '┌──────┐\\n│X     │\\n└──────┘\\nY');\n});\n\ntest.failing('set max width in percent', t => {\n\tconst output = renderToString(\n\t\t<Box width={10}>\n\t\t\t<Box maxWidth=\"50%\">\n\t\t\t\t<Text>AAAAAAAAAA</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'AAAAAB');\n});\n\ntest('set max height in percent', t => {\n\tconst output = renderToString(\n\t\t<Box height={6} flexDirection=\"column\">\n\t\t\t<Box maxHeight=\"50%\">\n\t\t\t\t<Box height={6}>\n\t\t\t\t\t<Text>A</Text>\n\t\t\t\t</Box>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A\\n\\n\\nB\\n\\n');\n});\n\n// Concurrent mode tests\ntest('set width - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box>\n\t\t\t<Box width={5}>\n\t\t\t\t<Text>A</Text>\n\t\t\t</Box>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'A    B');\n});\n\ntest('set height - concurrent', async t => {\n\tconst output = await renderToStringAsync(\n\t\t<Box height={4}>\n\t\t\t<Text>A</Text>\n\t\t\t<Text>B</Text>\n\t\t</Box>,\n\t);\n\n\tt.is(output, 'AB\\n\\n\\n');\n});\n"
  },
  {
    "path": "test/write-synchronized.tsx",
    "content": "import EventEmitter from 'node:events';\nimport test from 'ava';\nimport isInCi from 'is-in-ci';\nimport {bsu, esu, shouldSynchronize} from '../src/write-synchronized.js';\n\nconst createStream = ({tty = false} = {}) => {\n\tconst stream = new EventEmitter() as unknown as NodeJS.WriteStream;\n\tif (tty) {\n\t\tstream.isTTY = true;\n\t}\n\n\treturn stream;\n};\n\nfor (const [sequenceName, sequence, expected] of [\n\t['bsu', bsu, '\\u001B[?2026h'],\n\t['esu', esu, '\\u001B[?2026l'],\n] as const) {\n\ttest(`${sequenceName} is the expected synchronized update sequence`, t => {\n\t\tt.is(sequence, expected);\n\t});\n}\n\ntest('shouldSynchronize returns true for interactive TTY stream', t => {\n\tconst stream = createStream({tty: true});\n\tt.true(shouldSynchronize(stream, true));\n});\n\ntest('shouldSynchronize returns false for non-interactive TTY stream', t => {\n\tconst stream = createStream({tty: true});\n\tt.false(shouldSynchronize(stream, false));\n});\n\ntest('shouldSynchronize returns false for non-TTY stream', t => {\n\tconst stream = createStream({tty: false});\n\tt.false(shouldSynchronize(stream, true));\n});\n\ntest('shouldSynchronize uses CI detection when interactive is not specified', t => {\n\tconst ttyStream = createStream({tty: true});\n\t// When interactive is omitted, shouldSynchronize falls back to is-in-ci.\n\t// In CI the result is false (non-interactive by design); outside CI it's true.\n\tif (isInCi) {\n\t\tt.false(shouldSynchronize(ttyStream));\n\t} else {\n\t\tt.true(shouldSynchronize(ttyStream));\n\t}\n});\n\ntest('shouldSynchronize returns false for non-TTY stream when interactive is not specified', t => {\n\tconst stream = createStream({tty: false});\n\tt.false(shouldSynchronize(stream));\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n\t\"extends\": \"@sindresorhus/tsconfig\",\n\t\"compilerOptions\": {\n\t\t\"outDir\": \"build\",\n\t\t\"lib\": [\n\t\t\t\"DOM\",\n\t\t\t\"DOM.Iterable\",\n\t\t\t\"ES2023\"\n\t\t],\n\t\t\"sourceMap\": true,\n\t\t\"jsx\": \"react\",\n\t\t\"isolatedModules\": true\n\t},\n\t\"include\": [\"src\"]\n}\n"
  },
  {
    "path": "xo.config.ts",
    "content": "import {type FlatXoConfig} from 'xo';\n\nconst xoConfig: FlatXoConfig = [\n\t{\n\t\tignores: ['src/parse-keypress.ts'],\n\t},\n\t{\n\t\treact: true,\n\t\tprettier: true,\n\t\tsemicolon: true,\n\t\trules: {\n\t\t\t'react/no-unescaped-entities': 'off',\n\t\t\t'react/state-in-constructor': 'off',\n\t\t\t'react/jsx-indent': 'off',\n\t\t\t'react/prop-types': 'off',\n\t\t\t'unicorn/import-index': 'off',\n\t\t\t'import-x/no-useless-path-segments': 'off',\n\t\t\t'react-hooks/exhaustive-deps': 'off',\n\t\t\tcomplexity: 'off',\n\t\t},\n\t},\n\t{\n\t\tfiles: ['src/**/*.{ts,tsx}', 'test/**/*.{ts,tsx}'],\n\t\trules: {\n\t\t\t'no-unused-expressions': 'off',\n\t\t\tcamelcase: ['error', {allow: ['^unstable__', '^internal_']}],\n\t\t\t'unicorn/filename-case': 'off',\n\t\t\t'react/default-props-match-prop-types': 'off',\n\t\t\t'unicorn/prevent-abbreviations': 'off',\n\t\t\t'react/require-default-props': 'off',\n\t\t\t'react/jsx-curly-brace-presence': 'off',\n\t\t\t'@typescript-eslint/no-empty-function': 'off',\n\t\t\t'@typescript-eslint/promise-function-async': 'warn',\n\t\t\t'@typescript-eslint/explicit-function-return': 'off',\n\t\t\t'@typescript-eslint/explicit-function-return-type': 'off',\n\t\t\t'dot-notation': 'off',\n\t\t\t'react/boolean-prop-naming': 'off',\n\t\t\t'unicorn/prefer-dom-node-remove': 'off',\n\t\t\t'unicorn/prefer-event-target': 'off',\n\t\t\t'unicorn/consistent-existence-index-check': 'off',\n\t\t\t'unicorn/prefer-string-raw': 'off',\n\t\t\t'promise/prefer-await-to-then': 'off',\n\t\t},\n\t},\n\t{\n\t\tfiles: ['examples/**/*.{ts,tsx}', 'benchmark/**/*.{ts,tsx}'],\n\t\trules: {\n\t\t\t'import-x/no-unassigned-import': 'off',\n\t\t\t'@typescript-eslint/no-unsafe-call': 'off',\n\t\t\t'@typescript-eslint/no-unsafe-assignment': 'off',\n\t\t\t'@typescript-eslint/no-unsafe-return': 'off',\n\t\t\t'@typescript-eslint/no-unsafe-argument': 'off',\n\t\t\t'@typescript-eslint/restrict-plus-operands': 'off',\n\t\t},\n\t},\n];\n\nexport default xoConfig;\n"
  }
]