[
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.create/\n.dist/\n.npm/\nnode_modules/\n"
  },
  {
    "path": "bin/font.js",
    "content": "#!/usr/bin/env node\n\n// prettier-ignore\nconst letters = [\n  [ //  !\n    0b00000100,\n    0b00000100,\n    0b00000100,\n    0b00000000,\n    0b00000100,\n    0b00000000\n  ], [ // \"#\n    0b10101010,\n    0b10101110,\n    0b00001010,\n    0b00001110,\n    0b00001010,\n    0b00000000\n  ], [ // $%\n    0b11101010,\n    0b10000010,\n    0b11100100,\n    0b00101000,\n    0b11101010,\n    0b01000000\n  ], [ // &'\n    0b01100100,\n    0b10000100,\n    0b01000000,\n    0b10100000,\n    0b11100000,\n    0b00000000\n  ], [ // ()\n    0b00101000,\n    0b01000100,\n    0b01000100,\n    0b01000100,\n    0b00101000,\n    0b00000000\n  ], [ // *+\n    0b00000000,\n    0b01000100,\n    0b11101110,\n    0b01000100,\n    0b10100000,\n    0b00000000\n  ], [ // ,-\n    0b00000000,\n    0b00000000,\n    0b00001110,\n    0b00000000,\n    0b01000000,\n    0b10000000\n  ], [ // ./\n    0b00000010,\n    0b00000100,\n    0b00000100,\n    0b00000100,\n    0b01001000,\n    0b00000000\n  ], [ // 01\n    0b11100100,\n    0b10101100,\n    0b10100100,\n    0b10100100,\n    0b11101110,\n    0b00000000\n  ], [ // 23\n    0b11101110,\n    0b00100010,\n    0b11101110,\n    0b10000010,\n    0b11101110,\n    0b00000000\n  ], [ // 45\n    0b10101110,\n    0b10101000,\n    0b11101110,\n    0b00100010,\n    0b00101110,\n    0b00000000\n  ], [ // 67\n    0b11101110,\n    0b10000010,\n    0b11100010,\n    0b10100010,\n    0b11100010,\n    0b00000000\n  ], [ // 89\n    0b11101110,\n    0b10101010,\n    0b11101110,\n    0b10100010,\n    0b11101110,\n    0b00000000\n  ], [ // :;\n    0b00000000,\n    0b01000100,\n    0b00000000,\n    0b00000000,\n    0b01000100,\n    0b00001000\n  ], [ // <=\n    0b00100000,\n    0b01001110,\n    0b10000000,\n    0b01001110,\n    0b00100000,\n    0b00000000\n  ], [ // >?\n    0b10001110,\n    0b01000010,\n    0b00100100,\n    0b01000000,\n    0b10000100,\n    0b00000000\n  ], [ // @A\n    0b01001110,\n    0b10101010,\n    0b10001110,\n    0b11101010,\n    0b01001010,\n    0b00000000\n  ], [ // BC\n    0b11101110,\n    0b10101000,\n    0b11001000,\n    0b10101000,\n    0b11101110,\n    0b00000000\n  ], [ // DE\n    0b11001110,\n    0b10101000,\n    0b10101100,\n    0b10101000,\n    0b11001110,\n    0b00000000\n  ], [ // FG\n    0b11101110,\n    0b10001000,\n    0b11001000,\n    0b10001010,\n    0b10001110,\n    0b00000000\n  ], [ // HI\n    0b10101110,\n    0b10100100,\n    0b11100100,\n    0b10100100,\n    0b10101110,\n    0b00000000\n  ], [ // JK\n    0b11101010,\n    0b00101010,\n    0b00101100,\n    0b10101010,\n    0b11101010,\n    0b00000000\n  ], [ // LM\n    0b10001010,\n    0b10001110,\n    0b10001010,\n    0b10001010,\n    0b11101010,\n    0b00000000\n  ], [ // NO\n    0b11001110,\n    0b10101010,\n    0b10101010,\n    0b10101010,\n    0b10101110,\n    0b00000000\n  ], [ // PQ\n    0b11101110,\n    0b10101010,\n    0b11101010,\n    0b10001010,\n    0b10001110,\n    0b00000010\n  ], [ // RS\n    0b11101110,\n    0b10101000,\n    0b11001110,\n    0b10100010,\n    0b10101110,\n    0b00000000\n  ], [ // TU\n    0b11101010,\n    0b01001010,\n    0b01001010,\n    0b01001010,\n    0b01001110,\n    0b00000000\n  ], [ // VW\n    0b10101010,\n    0b10101010,\n    0b10101010,\n    0b10101110,\n    0b01001010,\n    0b00000000\n  ], [ // XY\n    0b10101010,\n    0b10101010,\n    0b01000100,\n    0b10100100,\n    0b10100100,\n    0b00000000\n  ], [ // Z[\n    0b11100110,\n    0b00100100,\n    0b01000100,\n    0b10000100,\n    0b11100110,\n    0b00000000\n  ], [ // \\]\n    0b10001100,\n    0b01000100,\n    0b01000100,\n    0b01000100,\n    0b00101100,\n    0b00000000\n  ], [ // ^_\n    0b01000000,\n    0b10100000,\n    0b00000000,\n    0b00000000,\n    0b00001110,\n    0b00000000\n  ], [ // `{\n    0b10000110,\n    0b01000100,\n    0b00001000,\n    0b00000100,\n    0b00000110,\n    0b00000000\n  ], [ // |}\n    0b01001100,\n    0b01000100,\n    0b01000010,\n    0b01000100,\n    0b01001100,\n    0b00000000\n  ], [ // ~\n    0b01010000,\n    0b10100000,\n    0b00000000,\n    0b00000000,\n    0b00000000,\n    0b00000000\n  ]\n]\n\nconsole.log(Buffer.from(letters.flat()).toString('base64'))\n"
  },
  {
    "path": "bin/logger.js",
    "content": "#!/usr/bin/env node\nimport { rmSync } from 'node:fs'\nimport { createServer } from 'node:net'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport process from 'node:process'\n\nconst filename = join(tmpdir(), 'node-log.sock')\n\ntry {\n  rmSync(filename)\n} catch {\n  //\n}\n\ncreateServer(stream => {\n  stream.on('data', data => {\n    data = data.toString()\n    if (data === '\\0') return process.stdout.write('\\x1bc')\n\n    process.stdout.write(data)\n  })\n}).listen(filename)\n"
  },
  {
    "path": "bin/postcreate.js",
    "content": "#!/usr/bin/env node\nimport { execSync } from 'node:child_process'\nimport { readFileSync, writeFileSync, chmodSync } from 'node:fs'\n\nconst makeJs = () => {\n  const file = `.create/index.js`\n  writeFileSync(file, `#!/usr/bin/env node\\n${readFileSync(file, 'utf8')}`)\n  chmodSync(file, '755')\n}\n\nconst makeJson = () => {\n  const json = JSON.parse(readFileSync('package.json', 'utf8'))\n\n  const keys = ['name', 'version', 'description', 'keywords', 'author', 'repository', 'homepage', 'license']\n  const jsonNew = {\n    ...Object.fromEntries(Object.entries(json).filter(([key]) => keys.includes(key))),\n    ...{\n      name: 'create-react-curse',\n      description: 'Create React-Curse app',\n      keywords: [...json.keywords, 'react-curse'],\n      bin: 'index.js'\n    }\n  }\n  writeFileSync('.create/package.json', JSON.stringify(jsonNew, null, 2))\n}\n\nmakeJs()\nmakeJson()\nexecSync('cp -r create/{template,README.md} .create')\n"
  },
  {
    "path": "bin/postdist.js",
    "content": "#!/usr/bin/env node\nimport { readFileSync, writeFileSync, chmodSync, unlinkSync } from 'node:fs'\nimport { argv } from 'node:process'\n\nlet [, , arg] = argv\nif (arg === undefined) {\n  const json = JSON.parse(readFileSync('package.json', 'utf8'))\n  arg = json.main.replace(/\\.[jt]sx?$/, '')\n}\n\nconst src = '.dist/index.cjs'\nconst dest = `.dist/${arg}.cjs`\nwriteFileSync(dest, `#!/usr/bin/env node\\n${readFileSync(src, 'utf8')}`)\nunlinkSync(src)\nchmodSync(dest, '755')\n"
  },
  {
    "path": "bin/postnpm.js",
    "content": "#!/usr/bin/env node\nimport { readFileSync, writeFileSync } from 'node:fs'\n\nconst makeJson = () => {\n  const json = JSON.parse(readFileSync('package.json', 'utf8'))\n\n  const keys = [\n    'name',\n    'version',\n    'description',\n    'keywords',\n    'author',\n    'repository',\n    'homepage',\n    'main',\n    'license',\n    'type',\n    'dependencies'\n  ]\n  const jsonNew = {\n    ...Object.fromEntries(Object.entries(json).filter(([key]) => keys.includes(key))),\n    ...{\n      main: 'index.js'\n    }\n  }\n  writeFileSync('.npm/package.json', JSON.stringify(jsonNew, null, 2))\n}\n\nconst makeReadme = () => {\n  const data = readFileSync('README.md', 'utf8')\n\n  const dataNew = data\n    .split('\\n')\n    .map(i => i.replace('media/', 'https://raw.githubusercontent.com/infely/react-curse/HEAD/media/'))\n    .join('\\n')\n  writeFileSync('.npm/README.md', dataNew)\n}\n\nmakeJson()\nmakeReadme()\n"
  },
  {
    "path": "components/Banner.tsx",
    "content": "import chunk from '../utils/chunk'\nimport Text, { type TextProps } from './Text'\nimport { useMemo } from 'react'\n\nconst FONT =\n  'BAQEAAQAqq4KDgoA6oLkKOpAZIRAoOAAKERERCgAAETuRKAAAAAOAECAAgQEBEgA5KykpO4A7iLugu4ArqjuIi4A7oLiouIA7qruou4AAEQAAEQIIE6ATiAAjkIkQIQATqqO6koA7qjIqO4AzqisqM4A7ojIio4ArqTkpK4A6iosquoAio6KiuoAzqqqqq4A7qrqio4C7qjOoq4A6kpKSk4AqqqqrkoAqqpEpKQA5iREhOYAjERERCwAQKAAAA4AhkQIBAYATERCREwAUKAAAAAA'\n\nconst letters = chunk(Buffer.from(FONT, 'base64'), 6)\n\nconst Letter = ({ children }: { children: string }) => {\n  const text = useMemo(() => {\n    let code = children.toUpperCase().charCodeAt(0)\n    if (code >= 123 && code <= 126) code -= 26\n    const font = letters[Math.floor((code - 32) / 2)]\n    if (!font) return\n\n    const bits = code % 2 === 0 ? 4 : 0\n    return chunk(font, 2)\n      .map(([top, bot]) => {\n        return [3, 2, 1, 0]\n          .map(i => {\n            const b = Math.pow(2, i + bits)\n            const code = 0 | (top & b && 0x04) | (bot & b && 0x08)\n            return code ? String.fromCharCode(0x257c + code) : ' '\n          })\n          .join('')\n      })\n      .join('\\n')\n  }, [children])\n\n  return <>{text}</>\n}\n\nexport interface BannerProps extends TextProps {\n  children: string\n}\n\nexport default function Banner({ children, ...props }: BannerProps) {\n  if (children === undefined || children === null) return null\n\n  const lines = children.toString().split('\\n')\n  const length = Math.max(...lines.map((i: string) => i.length))\n\n  return (\n    <Text {...props} height={lines.length * 3} width={length * 4}>\n      {lines.map((line: string, key: number) => (\n        <Text key={key} x={0} y={key * 3}>\n          {line.split('').map((char: string, key: number) => (\n            <Text key={key} x={key * 4} y={0}>\n              <Letter>{char}</Letter>\n            </Text>\n          ))}\n        </Text>\n      ))}\n    </Text>\n  )\n}\n"
  },
  {
    "path": "components/Bar.tsx",
    "content": "import Text, { TextProps } from './Text'\n\nconst getSize = (offset: number, size: number) => {\n  offset = Math.round(offset * 8)\n  size = Math.round(size * 8)\n  if (offset < 0) {\n    size += offset\n    offset = 0\n  }\n  size = Math.max(0, size)\n\n  return [offset, size]\n}\n\nconst getSections = (offset: number, size: number) => [\n  size >= 8 || ((offset + size) % 8 === 0 && size > 0 && size < 8),\n  size >= 8,\n  (size >= 8 || (offset % 8 === 0 && size < 8)) && (offset + size) % 8 !== 0\n]\n\nconst Vertical = (y: number, height: number, props: object) => {\n  const [offset, size] = getSize(y, height)\n  const sections = getSections(offset, size)\n\n  const char = (value: number) => {\n    if (value > 7) return String.fromCharCode(0x2588)\n    return String.fromCharCode(0x2588 - Math.min(8, value))\n  }\n\n  return (\n    <Text {...props} y={Math.floor(offset / 8)}>\n      {sections[0] && <Text block>{char(offset % 8)}</Text>}\n      {sections[1] &&\n        [...Array(Math.floor(((offset % 8) + size) / 8) - 1)].map((_, key) => (\n          <Text key={key} block>\n            {char(8)}\n          </Text>\n        ))}\n      {sections[2] && <Text inverse>{char((offset + size) % 8)}</Text>}\n    </Text>\n  )\n}\n\nconst Horizontal = (x: number, width: number, props: object) => {\n  const [offset, size] = getSize(x, width)\n  const sections = getSections(offset, size)\n\n  const char = (value: number) => {\n    if (value <= 0) return ' '\n    return String.fromCharCode(0x2590 - Math.min(8, value))\n  }\n\n  return (\n    <Text {...props} x={Math.floor(offset / 8)}>\n      {sections[0] && <Text inverse>{char(offset % 8)}</Text>}\n      {sections[1] && <Text>{char(8).repeat(Math.floor(((offset % 8) + size) / 8) - 1)}</Text>}\n      {sections[2] && <Text>{char((offset + size) % 8)}</Text>}\n    </Text>\n  )\n}\n\nexport interface BarProps extends TextProps {\n  type: 'vertical' | 'horizontal'\n  y?: number\n  x?: number\n  height?: number\n  width?: number\n}\n\nexport default function Bar({ type = 'vertical', y, x, height, width, ...props }: BarProps) {\n  if (type === 'vertical') return Vertical(y || 0, height || 0, { x, width, ...props })\n  if (type === 'horizontal') return Horizontal(x || 0, width || 0, { y, height, ...props })\n\n  return null\n}\n"
  },
  {
    "path": "components/Block.tsx",
    "content": "import useSize from '../hooks/useSize'\nimport Text, { TextProps } from './Text'\n\nexport interface BlockProps extends TextProps {\n  width?: number | undefined\n  align?: 'left' | 'center' | 'right'\n  children: any\n}\n\nexport default function Block({ width = undefined, align = 'left', children, ...props }: BlockProps) {\n  const handle = (line: any, key: any = undefined) => {\n    if (typeof line === 'object') return line\n    if (line === '\\n') return\n\n    let x: number | string = 0\n    switch (align) {\n      case 'center':\n        width ??= useSize().width\n        x = Math.round(width / 2 - line.length / 2)\n        break\n      case 'right':\n        x = `100%-${line.length}`\n        break\n    }\n    return (\n      <Text key={key} x={x} {...props} block>\n        {line}\n      </Text>\n    )\n  }\n\n  if (Array.isArray(children)) return children.map(handle)\n  return handle(children)\n}\n"
  },
  {
    "path": "components/Canvas.tsx",
    "content": "import { Color } from '../screen'\nimport chunk from '../utils/chunk'\nimport Text, { TextProps } from './Text'\nimport { Children, useEffect, useMemo, useRef } from 'react'\n\nclass CanvasClass {\n  // prettier-ignore\n  MODES = {\n    '1x1': { map: [[0x1]], table: [0x20, 0x88] },\n    '1x2': { map: [[0x1], [0x2]], table: [0x20, 0x80, 0x84, 0x88] },\n    '2x2': { map: [[0x1, 0x4], [0x2, 0x8]], table: [0x20, 0x98, 0x96, 0x8c, 0x9d, 0x80, 0x9e, 0x9b, 0x97, 0x9a, 0x84, 0x99, 0x90, 0x9c, 0x9f, 0x88] },\n    '2x4': { map: [[0x1, 0x8], [0x2, 0x10], [0x4, 0x20], [0x40, 0x80]] }\n  }\n\n  mode: { w: number; h: number }\n  multicolor: boolean\n  w: number\n  h: number\n  buffer: Buffer\n  colors: Color[]\n\n  constructor(width: number, height: number, mode = { w: 1, h: 2 }) {\n    this.mode = mode\n    this.multicolor = mode.w === 1 && mode.h === 2\n    this.w = Math.ceil(width / this.mode.w) * this.mode.w\n    this.h = Math.ceil(height / this.mode.h) * this.mode.h\n\n    const size = ((this.w / this.mode.w) * this.h) / this.mode.h\n    this.buffer = Buffer.alloc(size)\n    this.colors = [...Array(size * (this.multicolor ? 2 : 1))]\n  }\n\n  clear() {\n    this.buffer.fill(0)\n    this.colors.fill(0)\n  }\n\n  set(x: number, y: number, color: Color) {\n    if (x < 0 || x >= this.w || y < 0 || y >= this.h) return\n    const index = (this.w / this.mode.w) * Math.floor(y / this.mode.h) + Math.floor(x / this.mode.w)\n    this.buffer[index] |= (this.MODES as any)[`${this.mode.w}x${this.mode.h}`].map[y % this.mode.h][x % this.mode.w]\n\n    if (color) this.colors[this.multicolor ? this.w * y + x : index] = color\n  }\n\n  line(x0: number, y0: number, x1: number, y1: number, color: Color) {\n    const dx = x1 - x0\n    const dy = y1 - y0\n    const adx = Math.abs(dx)\n    const ady = Math.abs(dy)\n    let eps = 0\n    const sx = dx > 0 ? 1 : -1\n    const sy = dy > 0 ? 1 : -1\n    if (adx > ady) {\n      for (let x = x0, y = y0; sx < 0 ? x >= x1 : x <= x1; x += sx) {\n        this.set(x, y, color)\n        eps += ady\n        if (eps << 1 >= adx) {\n          y += sy\n          eps -= adx\n        }\n      }\n    } else {\n      for (let x = x0, y = y0; sy < 0 ? y >= y1 : y <= y1; y += sy) {\n        this.set(x, y, color)\n        eps += adx\n        if (eps << 1 >= ady) {\n          x += sx\n          eps -= ady\n        }\n      }\n    }\n  }\n\n  render() {\n    return [...this.buffer].map((i, index) => {\n      const table = (this.MODES as any)[`${this.mode.w}x${this.mode.h}`].table\n      let res = String.fromCharCode(table ? (i && 0x2500) + table[i] : 0x2800 + i)\n\n      let colors: Color[] = []\n      if (res !== ' ') {\n        if (this.multicolor) {\n          const y = Math.floor(index / this.w) * this.mode.h\n          const x = (index % this.w) * this.mode.w\n          const color1 = this.colors[this.w * y + x]\n          const color2 = this.colors[this.w * (y + 1) + x]\n          if (res === '\\u2588' && color1 !== color2) {\n            res = '\\u2580'\n            colors = [color1, color2]\n          } else {\n            colors = [color1 || color2]\n          }\n        } else {\n          colors = [this.colors[index]]\n        }\n      }\n\n      return [res, colors]\n    })\n  }\n}\n\ninterface Point {\n  x: number\n  y: number\n  color?: Color\n}\n\nexport const Point = (_props: Point) => <></>\n\ninterface Line {\n  x: number\n  y: number\n  dx: number\n  dy: number\n  color?: Color\n}\n\nexport const Line = (_props: Line) => <></>\n\ninterface CanvasProps extends TextProps {\n  mode?: { w: number; h: number }\n  width: number\n  height: number\n  children: any[]\n}\n\nexport default function Canvas({ mode = { w: 1, h: 2 }, width, height, children, ...props }: CanvasProps) {\n  const canvas = useRef(new CanvasClass(width, height, mode))\n\n  useEffect(() => {\n    canvas.current = new CanvasClass(width, height, mode)\n  }, [width, height, mode])\n\n  const text = useMemo(() => {\n    canvas.current.clear()\n\n    Children.forEach(children, i => {\n      if (i.type === Point) {\n        const { x, y, color } = i.props\n        canvas.current.set(x, y, color)\n      } else if (i.type === Line) {\n        const { x, y, dx, dy, color } = i.props\n        canvas.current.line(x, y, dx, dy, color)\n      }\n    })\n\n    return canvas.current.render()\n  }, [children])\n\n  return (\n    <Text {...props}>\n      {chunk(text, canvas.current.w / canvas.current.mode.w).map((line: any, y) => (\n        <Text key={y} x={0} y={y}>\n          {line.map(\n            ([char, [color, background]]: any, x: number) =>\n              char !== ' ' && (\n                <Text\n                  key={x}\n                  x={x}\n                  y={0}\n                  color={color ? color : undefined}\n                  background={background ? background : undefined}\n                >\n                  {char}\n                </Text>\n              )\n          )}\n        </Text>\n      ))}\n    </Text>\n  )\n}\n"
  },
  {
    "path": "components/Frame.tsx",
    "content": "import useChildrenSize from '../hooks/useChildrenSize'\nimport Text, { TextProps } from './Text'\n\nconst FRAMES = {\n  single: '┌─┐│└┘',\n  double: '╔═╗║╚╝',\n  rounded: '╭─╮│╰╯'\n} as const\n\nexport interface FrameProps extends TextProps {\n  type?: 'single' | 'double' | 'rounded'\n  height?: number\n  width?: number\n  children: React.ReactNode\n}\n\nexport default function Frame({ type = 'single', height: _height, width: _width, children, ...props }: FrameProps) {\n  const frames = FRAMES[type]\n\n  const size = _height === undefined || _width === undefined ? useChildrenSize(children) : undefined\n  const height = _height ?? size!.height\n  const width = _width ?? size!.width\n\n  const { color } = props\n\n  return (\n    <Text {...props}>\n      <Text color={color} block>\n        {frames[0]}\n        {frames[1].repeat(width)}\n        {frames[2]}\n      </Text>\n      {[...Array(height)].map((_, key) => (\n        <Text key={key} block>\n          <Text color={color}>{frames[3]}</Text>\n          {' '.repeat(width)}\n          <Text color={color}>{frames[3]}</Text>\n        </Text>\n      ))}\n      <Text y={1} x={1} block>\n        {children}\n      </Text>\n      <Text y={height + 1} color={color}>\n        {frames[4]}\n        {frames[1].repeat(width)}\n        {frames[5]}\n      </Text>\n    </Text>\n  )\n}\n"
  },
  {
    "path": "components/Input.tsx",
    "content": "import useInput from '../hooks/useInput'\nimport Renderer from '../renderer'\nimport { Color } from '../screen'\nimport Text, { TextProps } from './Text'\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\nconst mutate = (value: string, pos: number, str: string, multiline: boolean): [string, number, string | null] => {\n  const edit = (value: string, pos: number, callback: (string: string) => string) => {\n    const left = callback(value.substring(0, pos))\n    const right = value.substring(pos)\n    return [left, right].join('')\n  }\n\n  const arr = Renderer.input.parse(str) // str.split('')\n  for (const input of arr) {\n    switch (input) {\n      case '\\x01': // C-a\n      case '\\x1b\\x5b\\x31\\x7e': {\n        // home\n        if (pos > 0) pos = 0\n        break\n      }\n      case '\\x05': // C-e\n      case '\\x1b\\x5b\\x34\\x7e': {\n        // end\n        if (pos < value.length) pos = value.length\n        break\n      }\n      case '\\x02': // C-b\n      case '\\x1b\\x5b\\x44': {\n        // left\n        if (pos > 0) pos -= 1\n        break\n      }\n      case '\\x06': // C-f\n      case '\\x1b\\x5b\\x43': {\n        // right\n        if (pos < value.length) pos += 1\n        break\n      }\n      case '\\x1b': {\n        // esc\n        return [value, pos, 'cancel']\n      }\n      case '\\x04': // C-d\n      case '\\x0d': {\n        // cr\n        if (input === '\\x0d' && multiline) {\n          value = edit(value, pos, i => i + '\\n')\n          pos += 1\n          break\n        }\n        return [value, pos, 'submit']\n      }\n      case '\\x08': // C-h\n      case '\\x7f': {\n        // backspace\n        if (pos < 1) break\n        value = edit(value, pos, i => i.substring(0, i.length - 1))\n        pos -= 1\n        break\n      }\n      case '\\x15': {\n        // C-u\n        if (pos < 1) break\n        value = edit(value, pos, () => '')\n        pos = 0\n        break\n      }\n      case '\\x0b': {\n        // C-k\n        if (pos > value.length - 1) break\n        value = value.substring(0, pos)\n        break\n      }\n      case '\\x1b\\x62': // M-b\n      case '\\x17': {\n        // C-w\n        if (pos < 1) break\n        const index = value.substring(0, pos).trimEnd().lastIndexOf(' ')\n        if (input === '\\x17') value = edit(value, pos, i => (index !== -1 ? i.substring(0, index + 1) : ''))\n        pos = Math.max(0, index + 1)\n        break\n      }\n      case '\\x1b\\x66': {\n        // M-f\n        if (pos > value.length - 1) break\n        const nextWordIndex = value.substring(pos).match(/\\s(\\w)/)?.index ?? -1\n        pos = nextWordIndex === -1 ? value.length : pos + nextWordIndex + 1\n        break\n      }\n      case '\\x1b\\x64': {\n        // M-d\n        const nextEndIndex = value.substring(pos).match(/\\w(\\b)/)?.index ?? -1\n        value = value.substring(0, pos) + (nextEndIndex !== -1 ? value.substring(pos + nextEndIndex + 1) : '')\n        break\n      }\n      case '\\x1b\\x5b\\x41': {\n        // up\n        if (!multiline) break\n\n        const currentLine = value.substring(0, pos).lastIndexOf('\\n')\n        if (currentLine === -1) break\n\n        const targetLine = value.substring(0, currentLine).lastIndexOf('\\n')\n        pos = targetLine + Math.min(pos - currentLine, currentLine - targetLine)\n        break\n      }\n      case '\\x1b\\x5b\\x42': {\n        // down\n        if (!multiline) break\n\n        let targetLine_ = value.substring(pos).indexOf('\\n')\n        if (targetLine_ === -1) break\n\n        targetLine_ += pos + 1\n        let nextLine = value.substring(targetLine_).indexOf('\\n')\n        nextLine = (nextLine !== -1 ? targetLine_ + nextLine : value.length) + 1\n        const currentLine_ = value.substring(0, pos).lastIndexOf('\\n')\n        pos = targetLine_ + Math.min(pos - currentLine_ - 1, nextLine - targetLine_ - 1)\n        break\n      }\n      default: {\n        if (input.charCodeAt(0) < 32) break\n        value = edit(value, pos, i => i + input)\n        pos += 1\n      }\n    }\n  }\n  return [value, pos, null]\n}\n\ninterface InputProps extends TextProps {\n  focus?: boolean\n  type?: 'text' | 'password' | 'hidden'\n  initialValue?: string\n  cursorBackground?: Color\n  onCancel?: () => void\n  onChange?: (_: any) => void\n  onSubmit?: (_: any) => void\n}\n\nexport default function Input({\n  focus = true,\n  type = 'text',\n  initialValue = '',\n  cursorBackground = undefined,\n  onCancel = () => {},\n  onChange = (_: string) => {},\n  onSubmit = (_: string) => {},\n  width = undefined,\n  height = undefined,\n  ...props\n}: InputProps) {\n  const [value, setValue] = useState(initialValue)\n  const [pos, setPos] = useState(initialValue.length)\n  const offset = useRef({ y: 0, x: 0 })\n\n  const multiline = useMemo(() => {\n    return typeof height === 'number' && height > 1\n  }, [height])\n\n  useInput(\n    (_, raw) => {\n      if (raw === undefined) return\n      if (!focus) return\n\n      const [valueNew, posNew, action] = mutate(value, pos, raw(), multiline)\n      switch (action) {\n        case 'cancel':\n          onCancel()\n          break\n        case 'submit':\n          onSubmit(valueNew)\n          setValue('')\n          setPos(0)\n          break\n        default:\n          setValue(valueNew)\n          setPos(posNew)\n      }\n    },\n    [focus, value, pos, onCancel, onSubmit]\n  )\n\n  useEffect(() => {\n    onChange(value)\n  }, [value])\n\n  if (type === 'hidden') return null\n\n  const text = useMemo(() => {\n    if (type === 'password') return '*'.repeat(value.length)\n    return value\n  }, [value, type])\n\n  const { y: yo, x: xo } = useMemo(() => {\n    if (typeof width !== 'number') return offset.current\n\n    let posLine = pos\n    let valueLine = value\n    if (multiline && typeof height === 'number') {\n      const line = value.substring(0, pos).split('\\n').length - 1\n      if (offset.current.y < line - height + 1) offset.current.y = line - height + 1\n      if (offset.current.y > line) offset.current.y = line\n\n      const currentLine = value.substring(0, pos).lastIndexOf('\\n')\n      posLine = pos - (currentLine !== -1 ? currentLine + 1 : 0)\n      const nextLine = value.substring(pos).indexOf('\\n')\n      valueLine = value.substring(currentLine + 1, nextLine !== -1 ? pos + nextLine : value.length)\n    }\n\n    if (!multiline && offset.current.x + valueLine.length + 1 > width)\n      offset.current.x = Math.max(0, valueLine.length - width + 1)\n    if (offset.current.x < posLine - width + 1) offset.current.x = posLine - width + 1\n    if (offset.current.x > posLine) offset.current.x = posLine\n\n    return offset.current\n  }, [value, pos, width])\n\n  return (\n    <Text height={height} width={width} {...props}>\n      <Text y={-yo} x={-xo}>\n        {text.substring(0, pos)}\n        {focus && (\n          <>\n            <Text inverse={cursorBackground === undefined} background={cursorBackground}>\n              {(text[pos] !== '\\n' && text[pos]) || ' '}\n            </Text>\n            {text[pos] === '\\n' && '\\n'}\n          </>\n        )}\n        {text.length > pos && text.substring(pos + (focus ? 1 : 0))}\n      </Text>\n    </Text>\n  )\n}\n"
  },
  {
    "path": "components/List.tsx",
    "content": "import useInput from '../hooks/useInput'\nimport useSize from '../hooks/useSize'\nimport { Color } from '../screen'\nimport Scrollbar from './Scrollbar'\nimport Text from './Text'\nimport { useEffect, useMemo, useState } from 'react'\n\nexport const getYO = (offset: number, limit: number, y: number) => {\n  if (offset <= y - limit) return y - limit + 1\n  if (offset > y) return y\n  return offset\n}\n\nexport const inputHandler =\n  (\n    vi: boolean,\n    pos: ListPos,\n    setPos: (_: any) => void,\n    height: number,\n    dataLength: number,\n    onChange: (_: any) => void\n  ) =>\n  (input: string) => {\n    let y: undefined | number\n    let yo: undefined | number\n    if (((vi && input === 'k') || input === '\\x1b\\x5b\\x41') /* up */ && pos.y > 0) y = pos.y - 1\n    if (((vi && input === 'j') || input === '\\x1b\\x5b\\x42') /* down */ && pos.y < dataLength - 1) y = pos.y + 1\n    if (((vi && input === '\\x02') /* C-b */ || input === '\\x1b\\x5b\\x35\\x7e') /* pageup */ && pos.y > 0)\n      y = Math.max(0, pos.y - height)\n    if (((vi && input === '\\x06') /* C-f */ || input === '\\x1b\\x5b\\x36\\x7e') /* pagedown */ && pos.y < dataLength - 1)\n      y = Math.min(dataLength - 1, pos.y + height)\n    if (vi && input === '\\x15' /* C-u */ && pos.y > 0) y = Math.max(0, pos.y - Math.floor(height / 2))\n    if (vi && input === '\\x04' /* C-d */ && pos.y < dataLength - 1)\n      y = Math.min(dataLength - 1, pos.y + Math.floor(height / 2))\n    if (((vi && input === 'g') || input === '\\x1b\\x5b\\x31\\x7e') /* home */ && pos.y > 0) y = 0\n    if (((vi && input === 'G') || input === '\\x1b\\x5b\\x34\\x7e') /* end */ && pos.y < dataLength - 1) y = dataLength - 1\n    if (y !== undefined) yo = getYO(pos.yo, height, y)\n\n    if (vi && input === 'H') y = pos.yo\n    if (vi && input === 'M') y = pos.yo + Math.floor(height / 2)\n    if (vi && input === 'L') y = pos.yo + height - 1\n\n    if (y !== undefined) {\n      let newPos = { ...pos, y }\n      if (yo !== undefined) newPos = { ...newPos, yo }\n      setPos(newPos)\n      onChange(newPos)\n    }\n  }\n\nexport interface ListPos {\n  y: number\n  x: number\n  yo: number\n  xo: number\n  x1: number\n  x2: number\n  xm?: number\n}\n\nexport interface ListBase {\n  focus?: boolean\n  initialPos?: ListPos\n  height?: number\n  width?: number\n  renderItem?: (_: any) => any\n  scrollbar?: boolean\n  scrollbarBackground?: Color\n  scrollbarColor?: Color\n  vi?: boolean\n  pass?: any\n  onChange?: (pos: ListPos) => void\n  onSubmit?: (pos: ListPos) => void\n}\n\ninterface ListProps extends ListBase {\n  data?: any[]\n}\n\nexport default function List({\n  focus = true,\n  initialPos = { y: 0, x: 0, yo: 0, xo: 0, x1: 0, x2: 0 },\n  data = [''],\n  renderItem = (_: any) => <Text></Text>,\n  height: _height = undefined,\n  width: _width = undefined,\n  scrollbar = undefined,\n  scrollbarBackground = undefined,\n  scrollbarColor = undefined,\n  vi = true,\n  pass = undefined,\n  onChange = (_: ListPos) => {},\n  onSubmit = (_: ListPos) => {}\n}: ListProps) {\n  const size = _height === undefined || _width === undefined ? useSize() : undefined\n  const height = _height ?? size!.height\n  const width = _width ?? size!.width\n\n  const [pos, setPos] = useState<ListPos>({ ...{ y: 0, x: 0, yo: 0, xo: 0, x1: 0, x2: 0 }, ...initialPos })\n\n  const isScrollbarRequired = useMemo(() => {\n    return scrollbar === undefined ? data.length > height : scrollbar\n  }, [scrollbar, data.length, height])\n\n  useEffect(() => {\n    let newPos: undefined | ListPos\n    let { y } = initialPos\n    if (y > 0 && y >= data.length) {\n      y = data.length - 1\n      onChange({ ...pos, y })\n    }\n    if (y !== pos.y) {\n      y = Math.max(0, y)\n      newPos = { ...(newPos || pos), y, yo: getYO(pos.yo, height - 1, y) }\n    }\n    if (newPos) {\n      setPos(newPos)\n      onChange(newPos)\n    }\n  }, [initialPos.y])\n\n  useEffect(() => {\n    if (pos.y > 0 && pos.y > data.length - 1) {\n      const y = Math.max(0, data.length - 1)\n      const newPos = { ...pos, y, yo: getYO(pos.yo, data.length, y) }\n      setPos(newPos)\n      onChange(newPos)\n    }\n  }, [data])\n\n  useInput(\n    (input: string) => {\n      if (!focus) return\n\n      inputHandler(vi, pos, setPos, height, data.length, onChange)(input)\n\n      if (input === '\\x0d' /* cr */) onSubmit(pos)\n    },\n    [focus, vi, pos, setPos, height, data, onChange, onSubmit]\n  )\n\n  return (\n    <Text width={width} height={height}>\n      {data\n        .filter((_: any, index: number) => index >= pos.yo && index < height + pos.yo)\n        .map((row: any, index: number) => (\n          <Text key={index} height={1} block>\n            {renderItem({\n              focus,\n              item: row,\n              selected: index + pos.yo === pos.y,\n              pass\n            })}\n          </Text>\n        ))}\n      {isScrollbarRequired && (\n        <Text y={0} x=\"100%-1\">\n          <Scrollbar\n            offset={pos.yo}\n            limit={height}\n            length={data.length}\n            background={scrollbarBackground}\n            color={scrollbarColor}\n          />\n        </Text>\n      )}\n    </Text>\n  )\n}\n"
  },
  {
    "path": "components/ListTable.tsx",
    "content": "import useInput from '../hooks/useInput'\nimport useSize from '../hooks/useSize'\nimport { getYO, inputHandler, ListBase, ListPos } from './List'\nimport Scrollbar from './Scrollbar'\nimport Text from './Text'\nimport { useEffect, useMemo, useState } from 'react'\n\nconst getX = (index: number, widths: number[]) => {\n  const [x1, x2] = widths.reduce((acc, i, k) => [acc[0] + (k < index ? i : 0), acc[1] + (k <= index ? i : 0)], [0, 0])\n  return { x1, x2 }\n}\n\nconst getXO = (offsetX: number, limit: number, x1: number, x2: number) => {\n  if (x1 <= offsetX) return x1\n  if (x2 >= offsetX + limit) return x2 - limit + 1\n  return offsetX\n}\n\ninterface ListTableProps extends ListBase {\n  mode?: 'cell' | 'row'\n  head?: any[]\n  renderHead?: (_: any) => any\n  data?: any[][]\n}\n\nexport default function List({\n  mode = 'cell',\n  focus = true,\n  initialPos = { y: 0, x: 0, yo: 0, xo: 0, x1: 0, x2: 0 },\n  height: _height = undefined,\n  width: _width = undefined,\n  head = [''],\n  renderHead = (_: any) => <Text></Text>,\n  data = [['']],\n  renderItem = (_: any) => <Text></Text>,\n  scrollbar = undefined,\n  scrollbarBackground = undefined,\n  scrollbarColor = undefined,\n  vi = true,\n  pass = undefined,\n  onChange = (_pos: ListPos) => {},\n  onSubmit = (_pos: ListPos) => {}\n}: ListTableProps) {\n  const size = _height === undefined || _width === undefined ? useSize() : undefined\n  const height = _height ?? size!.height\n  const width = _width ?? size!.width\n\n  const [pos, setPos] = useState<ListPos>({ ...{ y: 0, x: 0, yo: 0, xo: 0, x1: 0, x2: 0 }, ...initialPos })\n\n  const isScrollbarRequired = useMemo(() => {\n    return scrollbar === undefined ? data.length > height - 1 : scrollbar\n  }, [scrollbar, data.length, height])\n\n  const widths = useMemo(() => {\n    const widths = data\n      .reduce(\n        (acc: number[], row: string[]) => {\n          row.forEach((i, k) => (acc[k] = Math.max(acc[k], (i?.toString() || 'null').length)))\n          return acc\n        },\n        head.map((i: any) => i.toString().length)\n      )\n      .map((i, index) => i + (index <= head.length - 2 ? 2 : 0))\n\n    const sum = widths.reduce((acc, i) => acc + i, 0)\n    if (sum >= width - 1) return widths.map(i => Math.min(32, i))\n\n    // const left = width - sum - 2\n    // if (sum > 0) widths[widths.length - 1] += left\n\n    return widths\n  }, [data, head, width])\n\n  const isCropped = useMemo(() => {\n    const sum = widths.reduce((acc, i) => acc + i, 0)\n    return sum - pos.xo >= width + (isScrollbarRequired ? -1 : 0)\n  }, [widths, pos.xo, width, isScrollbarRequired])\n\n  const dataFiltered = useMemo(() => {\n    return data.filter((_: any, index: number) => index >= pos.yo && index < height + pos.yo)\n  }, [data, pos.yo, height])\n\n  useEffect(() => {\n    let newPos: undefined | ListPos\n    let { y, x } = initialPos\n    if (y > 0 && y >= data.length) {\n      y = data.length - 1\n      onChange({ ...pos, y })\n    }\n    if (y !== pos.y) {\n      y = Math.max(0, y)\n      newPos = { ...(newPos || pos), y, yo: getYO(pos.yo, height - 1, y) }\n    }\n    if (initialPos.xm) {\n      let acc = 0\n      x = widths.map(i => (acc += i)).findIndex(i => i >= Math.min(acc, (initialPos.xm ?? 0) + pos.xo))\n    }\n    if (x > 0 && x >= head.length) {\n      x = head.length - 1\n      onChange({ ...pos, x })\n    }\n    if (x !== pos.x) {\n      const { x1, x2 } = getX(x, widths)\n      newPos = { ...(newPos || pos), x, xo: getXO(pos.xo, width + (isScrollbarRequired ? -1 : 0), x1, x2) }\n    }\n    if (newPos) {\n      setPos(newPos)\n      onChange(newPos)\n    }\n  }, [initialPos.y, initialPos.x, initialPos.xm])\n\n  useEffect(() => {\n    if (pos.y > 0 && head.length > 0 && pos.y > data.length - 1) {\n      const y = Math.max(0, data.length - 1)\n      const newPos = { ...pos, y, yo: getYO(pos.yo, data.length, y) }\n      setPos(newPos)\n      onChange(newPos)\n    }\n    if (pos.x > 0 && head.length > 0 && pos.x > head.length - 1) {\n      const newPos = { ...pos, x: head.length - 1 }\n      setPos(newPos)\n      onChange(newPos)\n    }\n  }, [head, data])\n\n  useInput(\n    (input: string) => {\n      if (!focus) return\n\n      inputHandler(vi, pos, setPos, height - 1, data.length, onChange)(input)\n\n      let x: undefined | number\n      switch (mode) {\n        case 'cell':\n          if (((vi && input === 'h') || input === '\\x1b\\x5b\\x44') /* left */ && pos.x > 0) x = pos.x - 1\n          if (((vi && input === 'l') || input === '\\x1b\\x5b\\x43') /* right */ && pos.x < head.length - 1) x = pos.x + 1\n          if (vi && input === '^' && pos.x > 0) x = 0\n          if (vi && input === '$' && pos.x < head.length - 1) x = head.length - 1\n          if (x !== undefined) {\n            const { x1, x2 } = getX(x, widths)\n            const xo = getXO(pos.xo, width + (isScrollbarRequired ? -1 : 0), x1, x2)\n            const newPos = { ...pos, x, xo, x1, x2 }\n            setPos(newPos)\n            onChange(newPos)\n          }\n          break\n        case 'row':\n          if (((vi && input === 'h') || input === '\\x1b\\x5b\\x44') /* left */ && pos.x > 0) x = pos.x - 1\n          if (((vi && input === 'l') || input === '\\x1b\\x5b\\x43') /* right */ && pos.x < head.length - 1) x = pos.x + 1\n          if (x !== undefined) {\n            const { x1, x2 } = getX(x, widths)\n            const newPos = { ...pos, x, xo: x1, x1, x2 }\n            setPos(newPos)\n            onChange(newPos)\n          }\n          break\n      }\n\n      if (input === '\\x0d' /* cr */) onSubmit({ ...pos, ...getX(pos.x, widths) })\n    },\n    [focus, vi, pos, width, height, head, data, widths, isScrollbarRequired, setPos, onChange, onSubmit]\n  )\n\n  return (\n    <Text width={width} height={height}>\n      <Text x={-pos.xo} height={1}>\n        {renderHead({\n          focus,\n          item: head,\n          widths,\n          pass\n        })}\n      </Text>\n      <Text y={1} x={-pos.xo}>\n        {dataFiltered.map((item: any, index: number) => (\n          <Text key={index} height={1} block>\n            {renderItem({\n              mode,\n              focus,\n              item,\n              y: pos.y,\n              x: pos.x,\n              widths,\n              index: index + pos.yo,\n              pass\n            })}\n          </Text>\n        ))}\n      </Text>\n      {isCropped && (\n        <Text y={0} x=\"100%-1\" dim>\n          ~\n        </Text>\n      )}\n      {isScrollbarRequired && (\n        <Text y={1} x=\"100%-1\">\n          <Scrollbar\n            offset={pos.yo}\n            limit={height - 1}\n            length={data.length}\n            background={scrollbarBackground}\n            color={scrollbarColor}\n          />\n        </Text>\n      )}\n    </Text>\n  )\n}\n"
  },
  {
    "path": "components/Scrollbar.tsx",
    "content": "import { Color } from '../screen'\nimport Bar from './Bar'\nimport Text from './Text'\n\nexport interface ScrollbarProps {\n  type?: 'vertical' | 'horizontal'\n  offset: number\n  limit: number\n  length: number\n  background?: Color\n  color?: Color\n}\n\nexport default function Scrollbar({ type = 'vertical', offset, limit, length, background, color }: ScrollbarProps) {\n  length ||= limit\n  offset = (limit / length) * offset\n  let size = limit / (length / limit)\n  if (size < 1) {\n    offset *= (length - limit / size) / (length - limit)\n    size = 1\n  }\n\n  return (\n    <Text background={background} height={type === 'vertical' ? limit : 1} width={type === 'horizontal' ? limit : 1}>\n      <Bar\n        type={type}\n        y={type === 'vertical' ? offset : undefined}\n        x={type === 'horizontal' ? offset : undefined}\n        height={type === 'vertical' ? size : undefined}\n        width={type === 'horizontal' ? size : undefined}\n        color={color}\n      />\n    </Text>\n  )\n}\n"
  },
  {
    "path": "components/Separator.tsx",
    "content": "import useSize from '../hooks/useSize'\nimport Text, { TextProps } from './Text'\n\nexport interface SeparatorProps extends TextProps {\n  type?: 'vertical' | 'horizontal'\n  height?: number\n  width?: number\n}\n\nexport default function Separator({ type = 'vertical', height: _height, width: _width, ...props }: SeparatorProps) {\n  const size = _height === undefined || _width === undefined ? useSize() : undefined\n  const height = _height ?? size!.height\n  const width = _width ?? size!.width\n\n  if (type === 'vertical' && height < 1) return null\n  if (type === 'horizontal' && width < 1) return null\n\n  return (\n    <Text height={type === 'horizontal' ? 1 : undefined} width={type === 'vertical' ? 1 : undefined} {...props}>\n      {type === 'vertical' &&\n        [...Array(height)].map((_, key) => (\n          <Text key={key} block>\n            │\n          </Text>\n        ))}\n      {type === 'horizontal' && '─'.repeat(width)}\n    </Text>\n  )\n}\n"
  },
  {
    "path": "components/Spinner.tsx",
    "content": "import useAnimation from '../hooks/useAnimation'\nimport Text, { TextProps } from './Text'\n\nexport interface SpinnerProps extends TextProps {\n  children?: string\n}\n\nexport default function Spinner({ children, ...props }: SpinnerProps) {\n  const frames = children ? children.split('') : ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']\n  const { ms, interpolate } = useAnimation(Infinity)\n  const frame = Math.floor(interpolate(0, frames.length, 0, 500, ms % 500))\n  const color = 255 - Math.abs(Math.floor(interpolate(-16, 16, 0, 1500, ms % 1500)))\n\n  return (\n    <Text color={color} {...props}>\n      {frames[frame]}\n    </Text>\n  )\n}\n"
  },
  {
    "path": "components/Text.tsx",
    "content": "import { Modifier } from '../screen'\n\nexport interface TextProps extends Modifier {\n  readonly absolute?: boolean\n  readonly x?: number | string\n  readonly y?: number | string\n  readonly width?: number | string\n  readonly height?: number | string\n  readonly block?: boolean\n  readonly children?: React.ReactNode\n}\n\nexport default function Text({ children, ...props }: TextProps) {\n  // @ts-ignore\n  return <text {...props}>{children}</text>\n}\n"
  },
  {
    "path": "components/View.tsx",
    "content": "import useChildrenSize from '../hooks/useChildrenSize'\nimport useInput from '../hooks/useInput'\nimport useSize from '../hooks/useSize'\nimport Scrollbar from './Scrollbar'\nimport Text, { TextProps } from './Text'\nimport { useState } from 'react'\n\nexport interface ViewProps extends TextProps {\n  focus?: boolean\n  height?: number\n  scrollbar?: boolean\n  vi?: boolean\n}\n\nexport default function View({ focus = true, height: _height, scrollbar, vi = true, children, ...props }: ViewProps) {\n  const height = _height ?? useSize().height\n  const [yo, setYo] = useState(0)\n  const { height: length } = useChildrenSize(children)\n\n  useInput(\n    (input: string) => {\n      if (!focus) return\n\n      if (((vi && input === 'k') || input === '\\x1b\\x5b\\x41') /* up */ && yo > 0) setYo(yo - 1)\n      if (((vi && input === 'j') || input === '\\x1b\\x5b\\x42') /* down */ && yo < length - height) setYo(yo + 1)\n      if (((vi && input === '\\x02') /* C-b */ || input === '\\x1b\\x5b\\x35\\x7e') /* pageup */ && yo > 0)\n        setYo(Math.max(0, yo - height))\n      if (((vi && input === '\\x06') /* C-f */ || input === '\\x1b\\x5b\\x36\\x7e') /* pagedown */ && yo < length - height)\n        setYo(Math.min(length - height, yo + height))\n      if (vi && input === '\\x15' /* C-u */ && yo > 0) setYo(Math.max(0, yo - Math.floor(height / 2)))\n      if (vi && input === '\\x04' /* C-d */ && yo < length - height)\n        setYo(Math.min(length - height, yo + Math.floor(height / 2)))\n      if (((vi && input === 'g') || input === '\\x1b\\x5b\\x31\\x7e') /* home */ && yo > 0) setYo(0)\n      if (((vi && input === 'G') || input === '\\x1b\\x5b\\x34\\x7e') /* end */ && yo < length - height)\n        setYo(length - height)\n    },\n    [focus, yo, length, height]\n  )\n\n  const isScrollbarRequired = scrollbar === undefined ? length > height : scrollbar\n\n  return (\n    <Text height={height} {...props}>\n      <Text y={-yo}>{children}</Text>\n      {isScrollbarRequired && (\n        <Text y={0} x=\"100%-1\">\n          <Scrollbar offset={yo} limit={height} length={length} />\n        </Text>\n      )}\n    </Text>\n  )\n}\n"
  },
  {
    "path": "create/Create.tsx",
    "content": "import ReactCurse, { Input, Spinner, Text, useAnimation, useInput } from '..'\nimport { Logo } from '../mediacreators/logo'\nimport { exec } from 'child_process'\nimport { copyFileSync, existsSync, mkdirSync, readdirSync } from 'fs'\nimport { join } from 'path'\nimport { cwd } from 'process'\nimport { useState } from 'react'\n\nconst pwd = cwd()\n\nconst install = (value: string) => {\n  value ??= ''\n  // console.log({ value })\n  // if (!value) return\n\n  const dest = join(pwd, value)\n  if (!existsSync(dest)) mkdirSync(dest)\n\n  if (existsSync(join(dest, 'package.json'))) return\n  const src = join(__dirname, 'template')\n  const files = readdirSync(src)\n  for (const file of files) {\n    copyFileSync(join(src, file), join(dest, file))\n  }\n\n  const cp = exec(`cd ${dest} && npm i`)\n  return new Promise(resolve => {\n    cp.on('exit', () => {\n      resolve(true)\n    })\n  })\n}\n\n// const Logo = ({ text }: { text: string }) => {\n//   const { interpolate } = useAnimation(1000)\n//   const w = Math.floor(interpolate(0, 22))\n//\n//   return (\n//     <Text block>\n//       <Text y={0} dim block>\n//         {text.replace(/[^\\s]/g, '#')}\n//       </Text>\n//       <Text y={0} color=\"BrightGreen\">\n//         {text.substring(0, w)}\n//       </Text>\n//     </Text>\n//   )\n// }\n\nconst App = () => {\n  const [focus, setFocus] = useState(1)\n  const [value, setValue] = useState(null)\n\n  const onSubmit = async (value: string) => {\n    setFocus(2)\n\n    const res = await install(value)\n    setFocus(res ? 3 : -1)\n\n    setTimeout(ReactCurse.exit, 1000 / 60)\n  }\n\n  useInput()\n\n  return (\n    <>\n      <Text block />\n      {/* <Logo text=\"Welcome to ReactCurse!\" /> */}\n      <Logo block />\n      <Text block />\n      <Text block>\n        {focus === 1 && <Text>? </Text>}\n        {focus !== 1 && <Text color=\"Green\">{'✔ '}</Text>}\n        <Text dim>Where would you like to create your app</Text>\n        <Text> {pwd}/</Text>\n        {focus === 1 && <Input onChange={setValue} onSubmit={onSubmit} color=\"Green\" />}\n        {focus !== 1 && <Text>{value}</Text>}\n      </Text>\n      {focus >= 2 && (\n        <Text block>\n          {focus === 2 && (\n            <>\n              <Spinner /> Installing...\n            </>\n          )}\n          {focus > 2 && (\n            <>\n              <Text color=\"Green\">{'✔ '}</Text>\n              <Text dim>Done</Text>\n            </>\n          )}\n        </Text>\n      )}\n      {focus === 3 && (\n        <>\n          <Text block />\n          {value && (\n            <Text block>\n              <Text dim>{'# '}</Text>cd {value}\n            </Text>\n          )}\n          <Text block>\n            <Text dim>{'# '}</Text>npm start\n          </Text>\n          <Text block />\n          <Text color=\"Green\">Enjoy!</Text>\n        </>\n      )}\n      {focus === -1 && <Text color=\"Red\">Canceled</Text>}\n    </>\n  )\n}\n\nReactCurse.inline(<App />)\n"
  },
  {
    "path": "create/readme.md",
    "content": "# create-react-curse\n\nGenerate a [react-curse](https://www.npmjs.com/package/react-curse) app\n"
  },
  {
    "path": "create/template/App.tsx",
    "content": "import { useState } from 'react'\nimport ReactCurse, { Text, useInput } from 'react-curse'\n\nconst App = () => {\n  const [counter, setCounter] = useState(0)\n\n  useInput(\n    input => {\n      if (input === 'q') ReactCurse.exit()\n      else setCounter(counter + 1)\n    },\n    [counter]\n  )\n\n  return (\n    <Text>\n      <Text block>\n        Counter: <Text color=\"Green\">{counter.toString()}</Text>\n      </Text>\n      <Text dim block>\n        Press q to exit or any key to increment the counter\n      </Text>\n      <Text>\n        Edit <Text inverse>App.tsx</Text>\n      </Text>\n    </Text>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "create/template/package.json",
    "content": "{\n  \"name\": \"react-curse-app\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"start\": \"npx esbuild App.tsx --outfile=.dist/index.js --bundle --platform=node --format=esm --external:'./node_modules/*' --sourcemap && node --enable-source-maps .dist\",\n    \"dist\": \"npx esbuild App.tsx --outfile=.dist/index.cjs --bundle --platform=node --define:'process.env.NODE_ENV=\\\"production\\\"' --minify --tree-shaking=true\"\n  },\n  \"dependencies\": {\n    \"react-curse\": \"^1.0.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^18.11.18\",\n    \"@types/react\": \"^18.0.27\"\n  }\n}\n"
  },
  {
    "path": "create/template/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"allowJs\": false,\n    \"checkJs\": false,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"jsx\": \"react-jsx\",\n    \"noEmit\": true\n  },\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import js from '@eslint/js'\nimport pluginReact from 'eslint-plugin-react'\nimport { defineConfig } from 'eslint/config'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default defineConfig([\n  {\n    files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],\n    plugins: { js },\n    extends: ['js/recommended'],\n    languageOptions: { globals: globals.browser }\n  },\n  tseslint.configs.recommended,\n  pluginReact.configs.flat.recommended,\n  {\n    rules: {\n      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],\n      '@typescript-eslint/no-explicit-any': 'off',\n\n      'react/react-in-jsx-scope': 'off'\n    }\n  }\n])\n"
  },
  {
    "path": "examples/Banner.tsx",
    "content": "import ReactCurse, { Text, useInput } from '..'\nimport Banner from '../components/Banner'\nimport { useEffect, useState } from 'react'\n\nconst lorem = [...Array(3)]\n  .map((_, offset) => [...Array(32)].map((_, index) => String.fromCharCode((offset + 1) * 32 + index)).join(''))\n  .join('\\n')\n\nconst getTime = () => new Date().toTimeString().substring(0, 8)\n\nconst Clock = (props: any) => {\n  const [time, setTime] = useState(getTime())\n\n  useEffect(() => {\n    const interval = setInterval(() => setTime(getTime()), 1000)\n\n    return () => clearInterval(interval)\n  }, [])\n\n  useInput((input: string) => {\n    if (input === 'q') ReactCurse.exit()\n  })\n\n  return <Banner {...props}>{time}</Banner>\n}\n\nReactCurse.render(\n  <>\n    <Clock color=\"Red\" x=\"50%-15\" block />\n    <Text>\n      <Text x={2} width={6} dim>\n        0x20{'\\n'.repeat(3)}0x40{'\\n'.repeat(3)}0x60\n      </Text>\n      <Banner color=\"Blue\">{lorem}</Banner>\n    </Text>\n  </>\n)\n"
  },
  {
    "path": "examples/Canvas.tsx",
    "content": "import ReactCurse, { Text, useInput, useSize } from '..'\nimport Canvas, { Line } from '../components/Canvas'\nimport { useEffect, useMemo, useState } from 'react'\n\nconst CELL = 4\nconst COLORS = ['Red', 'Green', 'Blue']\nconst DATA = [...Array(COLORS.length)].map(() => {\n  let prev = 0\n  return [...Array(1024)].map(() => {\n    prev += Math.round(Math.random() * 2 - 1)\n    prev = Math.max(0, Math.min(4, prev))\n    return prev\n  })\n})\n\nconst Graph = ({ mode, play }: any) => {\n  const { width } = useSize()\n\n  const h = useMemo(() => CELL * mode.h, [mode])\n  const w = useMemo(() => CELL * mode.w, [mode])\n  const c = useMemo(() => h * 4, [h])\n  const [lines, setLines] = useState<any[]>([])\n  const [offset, setOffset] = useState(0)\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      if (!play) return\n\n      setOffset(offset + 1)\n    }, 1000 / 60)\n\n    return () => clearInterval(interval)\n  }, [offset, play])\n\n  useEffect(() => {\n    setLines(\n      DATA.flatMap((line, colorIndex) => {\n        return line\n          .map((i, index) => {\n            if (index - (offset * mode.w) / w > (width * mode.w) / w) return\n            return {\n              x: index * w - offset * mode.w,\n              y: c - (line[index - 1] || 0) * h,\n              dx: index * w + w - offset * mode.w,\n              dy: c - i * h,\n              color: COLORS[colorIndex]\n            }\n          })\n          .filter(i => i)\n      })\n    )\n  }, [mode, offset])\n\n  return (\n    <Canvas width={width * mode.w} height={c + 1} mode={mode}>\n      {lines.map((props, key) => (\n        <Line key={key} {...props} />\n      ))}\n    </Canvas>\n  )\n}\n\nconst App = () => {\n  const [mode, setMode] = useState({ w: 1, h: 2 })\n  const [play, setPlay] = useState(true)\n\n  useInput((input: string) => {\n    if (input === '\\x10\\x0d') ReactCurse.exit()\n    if (input === 'q') ReactCurse.exit()\n\n    if (input === '1') setMode({ w: 1, h: 1 })\n    if (input === '2') setMode({ w: 1, h: 2 })\n    if (input === '3') setMode({ w: 2, h: 2 })\n    if (input === '4') setMode({ w: 2, h: 4 })\n\n    if (input === ' ') setPlay(play => !play)\n  })\n\n  return (\n    <>\n      <Text x={3} block>\n        {COLORS.map((color, key) => (\n          <Text key={key} color={color}>\n            Line {key + 1}{' '}\n          </Text>\n        ))}\n      </Text>\n      <Text>\n        <Text width={3} dim>\n          {[...Array(5)].map((_, key) => (\n            <Text key={key} x={1} y={CELL * 4 - key * CELL}>\n              {key.toString()}\n            </Text>\n          ))}\n        </Text>\n        <Graph mode={mode} play={play} />\n      </Text>\n    </>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "examples/Chat.tsx",
    "content": "import ReactCurse, { Banner, Input, Separator, Text, Trail, useAnimation, useWordWrap } from '..'\nimport { useState } from 'react'\n\nconst Message = ({ color: toColor, children }: { color: string; children: string }) => {\n  const { interpolateColor } = useAnimation(1000)\n  const color = interpolateColor('#888888', toColor)\n\n  return <Text color={color}>{useWordWrap(children)}</Text>\n}\n\nconst App = () => {\n  const [messages, setMessages] = useState<{ role: 'system' | 'user'; message: string }[]>([\n    { role: 'system', message: 'Hello' },\n    { role: 'system', message: 'Type anything' },\n    { role: 'system', message: 'And press enter' }\n  ])\n\n  const submitHandler = (message: string) => {\n    if (message) setMessages([...messages, { role: 'user', message }])\n  }\n\n  return (\n    <>\n      <Banner block>CHAT</Banner>\n      <Text height=\"100%-5\" block>\n        <Trail delay={250}>\n          {messages.map((i, key) => (\n            <Text key={key} color={i.role === 'system' ? 'Red' : 'Blue'} block>\n              <Text>{`${i.role}:`.padEnd(7, ' ')}</Text>{' '}\n              <Message color={i.role === 'system' ? '#e02020' : '#2020e0'}>{i.message}</Message>\n            </Text>\n          ))}\n        </Trail>\n      </Text>\n      <Separator type=\"horizontal\" dim block />\n      <Text>\n        <Text dim>#</Text> <Input initialValue=\"Hi there\" width=\"100%\" color=\"Blue\" onSubmit={submitHandler} />\n      </Text>\n    </>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "examples/Example.tsx",
    "content": "import ReactCurse, { Text, useInput } from '..'\nimport Frame from '../components/Frame'\nimport { useState } from 'react'\n\nconst App = () => {\n  const [counter, setCounter] = useState(0)\n\n  useInput(\n    input => {\n      if (input === 'q') ReactCurse.exit()\n\n      if (input === 'k') setCounter(counter + 1)\n      if (input === 'j') setCounter(counter - 1)\n    },\n    [counter]\n  )\n\n  return (\n    <>\n      <Frame type=\"rounded\" block>\n        <Text italic>Counter:</Text>{' '}\n        <Text color=\"red\" underline>\n          {counter}\n        </Text>\n      </Frame>\n      <Text dim>j,k - change counter, q - quit</Text>\n    </>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "examples/Inline.tsx",
    "content": "import ReactCurse, { Text } from '..'\n\nconst App = () => {\n  return (\n    <>\n      <Text block>Line 1</Text>\n      <Text block>Line 2</Text>\n      <Text block>Line 3</Text>\n    </>\n  )\n}\n\nReactCurse.inline(<App />)\n"
  },
  {
    "path": "examples/Pong.tsx",
    "content": "import ReactCurse, { Banner, Canvas, Point, Line, useSize, useInput } from '..'\nimport { useEffect, useState } from 'react'\n\nconst Game = () => {\n  const { width, height } = useSize()\n\n  const [scores, setScores] = useState([0, 0])\n  const [y, setY] = useState(height - 4)\n  const [ball, setBall] = useState({ x: Math.floor(width / 2), y: height, dx: 1, dy: 1 })\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setBall(({ x, y, dx, dy }) => {\n        x += dx\n        y += dy\n        if (x <= 1 || x >= width - 2) {\n          dx = -dx\n          scores[0]++\n          setScores(scores)\n        }\n        if (y <= 0 || y >= height * 2 - 1) {\n          dy = -dy\n          scores[1]++\n          setScores(scores)\n        }\n        return { x, y, dx, dy }\n      })\n    }, 1000 / 60)\n\n    return () => clearInterval(interval)\n  }, [scores])\n\n  useInput((input: string) => {\n    if (input === '\\x10\\x0d') ReactCurse.exit()\n    if (input === 'q') ReactCurse.exit()\n\n    if (input === 'k') setY(y => y - 1)\n    if (input === 'j') setY(y => y + 1)\n  })\n\n  return (\n    <>\n      <Banner y={0} x=\"50%-8\">\n        {scores.join('   ')}\n      </Banner>\n      <Canvas y={0} x={0} width={width} height={height * 2}>\n        <Line x={1} y={y} dx={1} dy={y + 4} />\n        <Point x={ball.x} y={ball.y} />\n        <Line x={width - 2} y={y} dx={width - 2} dy={y + 4} />\n      </Canvas>\n    </>\n  )\n}\n\nReactCurse.render(<Game />)\n"
  },
  {
    "path": "examples/Prompt.tsx",
    "content": "import ReactCurse, { Block, Frame, Input, Text, useAnimation, useInput } from '..'\nimport { useState } from 'react'\n\nconst InputText = ({ text, type, color }: { text: string; type: any; color: any }) => {\n  const [focus, setFocus] = useState(true)\n  const [value, setValue] = useState('')\n\n  const onSubmit = (input: string) => {\n    setFocus(false)\n    setValue(input)\n    ReactCurse.exit(input)\n  }\n\n  return (\n    <>\n      <Text color={color} underline>\n        {text}\n      </Text>\n      <Text>: </Text>\n      {focus && <Input type={type} onSubmit={onSubmit} color={color} />}\n      {!focus && <Text bold>{value}</Text>}\n    </>\n  )\n}\n\nconst InputList = ({ items, color }: { items: string[]; color: any }) => {\n  const [, setFocus] = useState(true)\n  const [selected, setSelected] = useState(0)\n\n  useInput(\n    (input: string) => {\n      if (input === 'q') ReactCurse.exit()\n\n      if (input === 'k') setSelected(i => Math.max(0, i - 1))\n      if (input === 'j') setSelected(i => Math.min(items.length - 1, i + 1))\n      if (input === '\\x0d') {\n        ReactCurse.exit(items[selected])\n        setFocus(false)\n      }\n    },\n    [selected]\n  )\n\n  return (\n    <>\n      <Text block>\n        <Text color={color}>Please select an option</Text>:\n      </Text>\n      {items.map((i, key) => (\n        <Text key={key} block>\n          <Text color={color}>{selected === key ? '>' : ' '}</Text> {i}\n        </Text>\n      ))}\n    </>\n  )\n}\n\nconst Spinner = ({ text }: { text: string }) => {\n  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] // ['|', '/', '-', '\\\\']\n\n  const { ms, interpolate } = useAnimation(Infinity)\n  const frame = Math.floor(interpolate(0, frames.length, 0, 500, ms % 500))\n  const color = 255 - Math.abs(Math.floor(interpolate(-16, 16, 0, 1500, ms % 1500)))\n\n  return (\n    <>\n      <Text color={color}>{frames[frame]}</Text> {text}\n    </>\n  )\n}\n\n;(async () => {\n  await ReactCurse.frame(\n    <Frame type=\"rounded\" width={32} block color=\"Blue\">\n      <Block color=\"Yellow\" width={32} align=\"center\">\n        hello world\n      </Block>\n    </Frame>\n  )\n\n  const res1 = await ReactCurse.prompt(<InputText text=\"Question 1\" type=\"text\" color=\"Red\" />)\n  console.log(`Answer 1: ${res1}`)\n\n  const res2 = await ReactCurse.prompt(<InputText text=\"Question 2\" type=\"password\" color=\"Green\" />)\n  console.log(`Answer 2: ${res2}`)\n\n  const res3 = await ReactCurse.prompt(<InputText text=\"Question 3\" type=\"hidden\" color=\"Blue\" />)\n  console.log(`Answer 3: ${res3}`)\n\n  const res4 = await ReactCurse.prompt(<InputList items={['Item 1', 'Item 2', 'Item 3', 'Item 4']} color=\"Yellow\" />)\n  console.log(`Answer 3: ${res4}`)\n\n  ReactCurse.inline(<Spinner text={JSON.stringify({ res1, res2, res3, res4 })} />)\n  await new Promise(resolve => setTimeout(resolve, 500))\n\n  process.exit()\n})()\n"
  },
  {
    "path": "examples/Speed.tsx",
    "content": "import ReactCurse, { Text, useInput } from '..'\nimport { useEffect, useState } from 'react'\n\nconst TEXT = ''\nconst width = process.stdout.columns\nconst height = process.stdout.rows\n\nconst rand = () => {\n  return [...Array(128)].map(() => [\n    // 512\n    width / 2,\n    0,\n    Math.floor(Math.random() * 256),\n    Math.random() * 2 - 1, // 3 - 1.5\n    Math.random() * 1 // 1.5\n  ])\n}\n\nconst App = () => {\n  const [texts, setTexts] = useState(rand())\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setTexts(texts =>\n        texts\n          .map(([x, y, color, dx, dy]) => {\n            x += dx\n            y += dy\n            if (x >= width - 1 - TEXT.length || x <= 0) dx = -dx\n            if (y >= height - 1 || y <= 0) {\n              dy = -dy\n              if (dy < 0) dy *= 0.9\n            }\n            return [x, y, color, dx, dy]\n          })\n          .filter(i => Math.round(i[4] * 10))\n      )\n    }, 1000 / 60)\n\n    return () => clearInterval(interval)\n  }, [])\n\n  useInput((input: string) => {\n    if (input === 'q') ReactCurse.exit()\n  })\n\n  return (\n    <>\n      {texts.map(([x, y, color], key) => (\n        <Text key={key} x={Math.round(x)} y={Math.round(y)} color={color}>\n          {TEXT}\n        </Text>\n      ))}\n    </>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "examples/Todo.tsx",
    "content": "import ReactCurse, { Block, Input, List, Text, useInput, useSize } from '..'\nimport useAnimation, { useTrail } from '../hooks/useAnimation'\nimport { useCallback, useState } from 'react'\n\nconst Task = ({ title, completed, selected }: { title: string; completed: boolean; selected: boolean }) => {\n  const { interpolateColor } = useAnimation(250)\n\n  return (\n    <Text color={interpolateColor('#282828', selected ? '#b8bb26' : '#ebdbb2')} block>\n      {completed ? '' : ''} {title}\n    </Text>\n  )\n}\n\nconst Tasks = ({ focus, setFocus }: { focus: boolean; setFocus: (focus: boolean) => void }) => {\n  const { height, width } = useSize()\n  const [pos, setPos] = useState<{ y: number }>({ y: 0 })\n  const [tasks, setTasks] = useState(() =>\n    [...Array(32)].map((_, i) => ({ id: i + 1, title: `Task ${i + 1}`, completed: Math.random() >= 0.5 }))\n  )\n\n  useInput(\n    input => {\n      if (focus) return\n      if (input === 'D') setTasks(tasks.filter((_, index) => index !== pos.y))\n      if (input === ' ') setTasks(tasks.map((i, index) => (index === pos.y ? { ...i, completed: !i.completed } : i)))\n      if (input === '\\x0d') setFocus(true)\n    },\n    [pos, tasks]\n  )\n\n  const onSubmit = useCallback(\n    (title: string) => {\n      setFocus(false)\n      setTasks(tasks => [...tasks, { id: tasks.length + 1, title, completed: false }])\n    },\n    [tasks]\n  )\n\n  return (\n    <>\n      <List\n        height={height - 3}\n        width={width}\n        data={useTrail(1000 / 30, tasks, 'id')}\n        renderItem={({ item, selected }) => {\n          return <Task title={item.title} completed={item.completed} selected={selected} />\n        }}\n        onChange={setPos}\n      />\n      <Input absolute y=\"100%-1\" x={0} focus={focus} onSubmit={onSubmit} onCancel={() => setFocus(false)} />\n    </>\n  )\n}\n\nconst Fade = ({ children }: { children: React.ReactNode }) => {\n  const { interpolateColor } = useAnimation(1000)\n\n  const color = interpolateColor('#3c3836', '#ebdbb2', 500)\n  if (color === '#3c3836') return null\n\n  return (\n    <Block align=\"center\" color={color}>\n      {children}\n    </Block>\n  )\n}\n\nconst App = () => {\n  const { width } = useSize()\n  const [show, setShow] = useState(true)\n  const [focus, setFocus] = useState(false)\n\n  const { interpolate, interpolateColor } = useAnimation(500)\n  const x = Math.round(interpolate(0, width))\n  const background = interpolateColor('#282828', '#3c3836')\n\n  useInput(\n    input => {\n      if (input === '\\x10\\x0d') ReactCurse.exit()\n      if (focus) return\n\n      if (input === 'q') ReactCurse.exit()\n      if (input === 't') setShow(i => !i)\n    },\n    [focus]\n  )\n\n  return (\n    <Text>\n      <Text height={1} width={x} background={background}>\n        <Fade>hello</Fade>\n      </Text>\n      <Text y={1} x={1}>\n        {show && <Tasks focus={focus} setFocus={setFocus} />}\n      </Text>\n      <Text y=\"100%-2\" x={width - x} height={1} background={background}>\n        <Fade>world</Fade>\n      </Text>\n    </Text>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "examples/Visualizer.tsx",
    "content": "import ReactCurse, { Bar, Text, Trail, useAnimation, useSize } from '..'\nimport { useMemo } from 'react'\n\n// const App = () => {\n//   const { height, width } = useSize()\n//\n//   const { ms, interpolate } = useAnimation(2000)\n//\n//   const lines = useMemo(() => {\n//     return [...Array(Math.floor(width / 2))].map(_ => ({\n//       h: Math.floor(Math.random() * height),\n//       t: Math.floor(Math.random() * 250) + 250,\n//       c: 255 - Math.floor(Math.random() * 16)\n//     }))\n//   }, [])\n//\n//   return (\n//     <>\n//       {lines.map(({ h, t, c }, key) => {\n//         h = ms < 1000 ? interpolate(0, h, 0, t, ms % t) : interpolate(h, 0, 1000, 2000)\n//         return (\n//           <Text y={0} x={key * 2} key={key}>\n//             <Bar type=\"vertical\" y={height - h} height={h} color={c} />\n//           </Text>\n//         )\n//       })}\n//     </>\n//   )\n// }\n\nconst Line = ({ h, c }: { h: number; c: number }) => {\n  const { height } = useSize()\n  const { interpolate } = useAnimation(500)\n\n  h = interpolate(h, 0)\n\n  return <Bar type=\"vertical\" y={height - h} height={h} color={c} />\n}\n\nconst App2 = () => {\n  const { height, width } = useSize()\n\n  const lines = useMemo(() => {\n    return [...Array(Math.floor(width / 2))].map(_ => ({\n      h: Math.floor(Math.random() * height),\n      c: 255 - Math.floor(Math.random() * 16)\n    }))\n  }, [])\n\n  return (\n    <Trail delay={1000 / width}>\n      {lines.map((props, key) => (\n        <Text y={0} x={key * 2} key={key}>\n          <Line {...props} />\n        </Text>\n      ))}\n    </Trail>\n  )\n}\n\nReactCurse.render(<App2 />)\n"
  },
  {
    "path": "hooks/useAnimation.ts",
    "content": "import Renderer from '../renderer'\nimport { useState, useEffect, useRef } from 'react'\n\nconst interpolate = (toLow: number, toHigh: number, fromLow: number, fromHigh: number, value: number) => {\n  const res = toLow + ((((toHigh - toLow) / 100) * 100) / (fromHigh - fromLow)) * (value - fromLow)\n  return Math.max(Math.min(toLow, toHigh), Math.min(Math.max(toLow, toHigh), res))\n}\n\nconst interpolateColor = (toLow: string, toHigh: string, fromLow: number, fromHigh: number, value: number) => {\n  if (!toLow.startsWith('#')) return toHigh\n  if (!toHigh.startsWith('#')) return toHigh\n\n  const toLowColor = Renderer.term.parseHexColor(toLow)\n  return (\n    '#' +\n    Buffer.from(\n      Renderer.term.parseHexColor(toHigh).map((i: number, index: number) => {\n        return Math.round(interpolate(toLowColor[index], i, fromLow, fromHigh, value))\n      })\n    ).toString('hex')\n  )\n}\n\nexport const Trail = ({ delay, children }: { delay: number; children: any }): React.JSX.Element => {\n  return useTrail(delay, children)\n}\n\nexport const useTrail = (delay: number, children: any[], key: string = 'key'): any => {\n  const ms = useRef(0)\n  const timeout = useRef<NodeJS.Timeout>(undefined)\n  const [keys, setKeys] = useState<any[]>([])\n\n  useEffect(() => {\n    const keysNew = children.map((i: any) => i[key])\n    const keyOld = keys.find(key => !keysNew.includes(key))\n    if (keyOld) {\n      setKeys(keys.filter(i => i !== keyOld))\n      return\n    }\n\n    const keyNew = keysNew.find((i: any) => !keys.includes(i))\n    if (!keyNew) return\n\n    const at = Date.now()\n    const nextAt = Math.max(0, delay - (at - ms.current))\n\n    clearTimeout(timeout.current)\n    timeout.current = setTimeout(() => {\n      ms.current = Date.now()\n      setKeys([...keys, keyNew])\n    }, nextAt)\n  }, [children.map((i: any) => i[key]).join('\\n'), keys])\n\n  return children.filter((i: any) => keys.includes(i[key]))\n}\n\ninterface useAnimation {\n  ms: number\n  interpolate: (toLow: number, toHigh: number, fromLow?: number, fromHigh?: number, value?: number) => number\n  interpolateColor: (toLow: string, toHigh: string, fromLow?: number, fromHigh?: number, value?: number) => string\n}\n\nexport default (time = Infinity, fps = 60): useAnimation => {\n  if (time <= 0 || fps <= 0) return { ms: 0, interpolate: i => i, interpolateColor: i => i }\n\n  const at = useRef(Date.now())\n  const interval = useRef<NodeJS.Timeout>(undefined)\n  const [ms, setMs] = useState(0)\n\n  useEffect(() => {\n    const frameMs = 1000 / Math.min(60, fps)\n\n    interval.current = setInterval(() => {\n      const msNew = Date.now() - at.current\n      if (msNew >= time) clearInterval(interval.current)\n      setMs(Math.min(msNew, time))\n    }, frameMs)\n\n    return () => {\n      clearInterval(interval.current)\n    }\n  }, [])\n\n  return {\n    ms,\n    interpolate: (toLow, toHigh, fromLow = 0, fromHigh = time, value = ms) => {\n      return interpolate(toLow, toHigh, fromLow, fromHigh, value)\n    },\n    interpolateColor: (toLow, toHigh, fromLow = 0, fromHigh = time, value = ms) => {\n      return interpolateColor(toLow, toHigh, fromLow, fromHigh, value)\n    }\n  }\n}\n"
  },
  {
    "path": "hooks/useBell.ts",
    "content": "/**\n * @deprecated\n */\nexport default () => {\n  process.stdout.write('\\x07')\n}\n"
  },
  {
    "path": "hooks/useChildrenSize.ts",
    "content": "import { type ReactElement, useEffect, useState } from 'react'\n\nconst render = (element: ReactElement | ReactElement[] | any): any => {\n  if (Array.isArray(element)) return element.map(i => render(i)).join('')\n\n  const { children } = ((element as ReactElement).props as any) ?? { children: element }\n  if (Array.isArray(children) || children?.props) return render(children)\n\n  return children.toString()\n}\n\nconst getSize = (children: ReactElement | ReactElement[] | any) => {\n  const string = render(children).split('\\n')\n  const width = string.reduce((acc: number, i: string) => Math.max(acc, i.length), 0)\n  const height = string.length\n\n  return { width, height }\n}\n\nexport default (children: ReactElement | ReactElement[] | any) => {\n  const [size, setSize] = useState(getSize(children))\n\n  useEffect(() => {\n    setSize(getSize(children))\n  }, [children])\n\n  return size\n}\n"
  },
  {
    "path": "hooks/useClipboard.ts",
    "content": "import { spawnSync } from 'child_process'\n\nexport default (): [() => string, (input: string) => string] => {\n  const getClipboard = () => {\n    switch (process.platform) {\n      case 'darwin':\n        return spawnSync('pbpaste', [], { encoding: 'utf8' }).stdout\n    }\n\n    return ''\n  }\n\n  const setClipboard = (input: any) => {\n    if (typeof input !== 'string') input = input.toString()\n\n    switch (process.platform) {\n      case 'darwin':\n        spawnSync('pbcopy', [], { input })\n        break\n      default:\n        input = ''\n    }\n\n    return input\n  }\n\n  return [getClipboard, setClipboard]\n}\n"
  },
  {
    "path": "hooks/useExit.ts",
    "content": "import Renderer from '../renderer'\nimport process from 'process'\n\n/**\n * @deprecated\n */\nexport default (code: number | any = 0) => {\n  if (typeof code === 'number') process.exit(code)\n\n  Renderer.term.setResult(code)\n}\n"
  },
  {
    "path": "hooks/useInput.ts",
    "content": "import Renderer from '../renderer'\nimport { useEffect } from 'react'\n\nexport default (callback: (input: string, raw: () => string) => void = () => {}, deps: React.DependencyList = []) => {\n  useEffect(() => {\n    if (!process.stdin.isRaw) process.stdin.setRawMode?.(true)\n  }, [])\n\n  useEffect(() => {\n    const handler = (input: string, raw: () => string) => {\n      if (input === '\\x03') process.exit()\n      if (input.startsWith('\\x1b\\x5b\\x4d')) return\n\n      callback(input, raw)\n    }\n\n    Renderer.input.on(handler)\n    return () => {\n      Renderer.input.off(handler)\n    }\n  }, deps)\n}\n"
  },
  {
    "path": "hooks/useMouse.ts",
    "content": "import Renderer from '../renderer'\nimport { type DependencyList, useEffect } from 'react'\n\ninterface Event {\n  type: 'mousedown' | 'mouseup' | 'wheeldown' | 'wheelup'\n  x: number\n  y: number\n}\n\nexport default (callback: (event: Event) => void, deps: DependencyList = []) => {\n  useEffect(() => {\n    if (!process.stdin.isRaw) process.stdin.setRawMode?.(true)\n    Renderer.term.enableMouse()\n  }, [])\n\n  useEffect(() => {\n    const handler = (input: string) => {\n      if (!input.startsWith('\\x1b\\x5b\\x4d')) return\n\n      const b = input.charCodeAt(3)\n      const type = (1 << 6) & b ? (1 & b ? 'wheelup' : 'wheeldown') : (3 & b) === 3 ? 'mouseup' : 'mousedown'\n\n      const x = input.charCodeAt(4) - 0o41\n      const y = input.charCodeAt(5) - 0o41\n\n      callback({ type, x, y })\n    }\n\n    Renderer.input.on(handler)\n    return () => {\n      Renderer.input.off(handler)\n    }\n  }, deps)\n}\n"
  },
  {
    "path": "hooks/useSize.ts",
    "content": "import { useEffect, useState } from 'react'\n\nconst subscribers = new Set<(size: { width: number; height: number }) => void>()\n\nconst getSize = () => {\n  const { columns: width, rows: height } = process.stdout\n  return { width, height }\n}\n\nprocess.stdout.on('resize', () => {\n  const size = getSize()\n  subscribers.forEach((_, fn) => fn(size))\n})\n\nexport default () => {\n  const [size, setSize] = useState(getSize())\n\n  useEffect(() => {\n    subscribers.add(setSize)\n\n    return () => {\n      subscribers.delete(setSize)\n    }\n  }, [])\n\n  return size\n}\n"
  },
  {
    "path": "hooks/useWordWrap.ts",
    "content": "import useSize from './useSize'\n\nexport default (text: string, _width: number | undefined = undefined) => {\n  const width = _width ?? useSize().width\n\n  return text\n    .split('\\n')\n    .map((line: string) => {\n      if (line.length <= width) return line\n\n      return line\n        .split(' ')\n        .reduce(\n          (acc, i) => {\n            if (acc[acc.length - 1].length + i.length > width) acc.push('')\n            acc[acc.length - 1] += `${i} `\n            return acc\n          },\n          ['']\n        )\n        .map(i => i.trimEnd())\n        .join('\\n')\n    })\n    .join('\\n')\n}\n"
  },
  {
    "path": "index.ts",
    "content": "export { default } from './renderer'\nexport { default as Banner } from './components/Banner'\nexport { default as Bar } from './components/Bar'\nexport { default as Block } from './components/Block'\nexport { default as Canvas, Point, Line } from './components/Canvas'\nexport { default as Frame } from './components/Frame'\nexport { default as Input } from './components/Input'\nexport { default as List, type ListPos } from './components/List'\nexport { default as ListTable } from './components/ListTable'\nexport { default as Scrollbar } from './components/Scrollbar'\nexport { default as Separator } from './components/Separator'\nexport { default as Spinner } from './components/Spinner'\nexport { default as Text } from './components/Text'\nexport { default as View } from './components/View'\nexport { default as useAnimation, useTrail, Trail } from './hooks/useAnimation'\nexport { default as useBell } from './hooks/useBell'\nexport { default as useChildrenSize } from './hooks/useChildrenSize'\nexport { default as useClipboard } from './hooks/useClipboard'\nexport { default as useExit } from './hooks/useExit'\nexport { default as useInput } from './hooks/useInput'\nexport { default as useMouse } from './hooks/useMouse'\nexport { default as useSize } from './hooks/useSize'\nexport { default as useWordWrap } from './hooks/useWordWrap'\nexport { default as log } from './utils/log'\n"
  },
  {
    "path": "input.ts",
    "content": "import EventEmitter from 'events'\n\nexport default class Input {\n  ee: EventEmitter\n  queue: string[] = []\n\n  constructor() {\n    this.ee = new EventEmitter()\n  }\n\n  terminate() {\n    this.ee.removeAllListeners()\n  }\n\n  private onData = (key: Buffer) => {\n    const raw = key.toString()\n    const chunks = this.parse(raw)\n\n    if (chunks.length > 1) this.queue = chunks.slice(1)\n    this.ee.emit('data', chunks[0], () => {\n      this.queue = []\n      return raw\n    })\n  }\n\n  parse(input: string) {\n    const chars = input.split('')\n\n    let res: any\n    const chunks: string[] = []\n    while ((res = chars.shift())) {\n      if (['\\x10', '\\x1b'].includes(res)) {\n        // length >= 2, example: M-a (1b 61)\n        res += chars.shift() || ''\n\n        if (res.endsWith('\\x5b')) {\n          // length >= 3, example: arrowup (1b 5b 41)\n          res += chars.shift() || ''\n\n          if (res.endsWith('\\x31') || res.endsWith('\\x34') || res.endsWith('\\x35') || res.endsWith('\\x36')) {\n            // length >= 4, example: pageup (1b 5b 35 7e, 1b 5b 36 7e)\n            res += chars.shift() || ''\n          } else if (res.endsWith('\\x4d')) {\n            // length >= 4, example: mousedown (1b 5b 4d 20 21 21, 1b 5b 4d 20 c3 80 21)\n            res += chars.shift() || ''\n            res += chars.shift() || ''\n            res += chars.shift() || ''\n          }\n        }\n      }\n\n      chunks.push(res)\n    }\n\n    return chunks\n  }\n  on(callback: (input: string, raw: () => string) => void) {\n    if (this.ee.listenerCount('data') === 0) process.stdin.on('data', this.onData)\n    this.ee.on('data', callback)\n  }\n\n  off(callback: (input: string, raw: () => string) => void) {\n    this.ee.off('data', callback)\n    if (this.ee.listenerCount('data') === 0) process.stdin.off('data', this.onData)\n  }\n\n  render() {\n    const chunk = this.queue.shift()\n    if (chunk) setTimeout(() => this.ee.emit('data', chunk), 0)\n  }\n}\n"
  },
  {
    "path": "mediacreators/Banner.tsx",
    "content": "/* @vhs 80x3@1 */\nimport ReactCurse, { Banner, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return <Banner>{'12:34:56' /* new Date().toTimeString().substring(0, 8) */}</Banner>\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/Bar-1.tsx",
    "content": "/* @vhs 80x3@1 */\nimport ReactCurse, { Bar, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return (\n    <>\n      {[...Array(24)].map((_, index) => (\n        <Bar key={index} type=\"vertical\" x={index * 2} height={(index + 1) / 8} />\n      ))}\n    </>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/Bar-2.tsx",
    "content": "/* @vhs 80x3\nHide\nType@0 npm_start\nEnter\nSleep 40ms\n\nShow\nSleep 2s */\nimport ReactCurse, { Bar, Text, useAnimation, useInput } from '..'\n\nconst App = () => {\n  useInput()\n  const { interpolate } = useAnimation(2000)\n\n  return (\n    <>\n      <Text y={0} x={0}>\n        {'<Bar>'} <Bar type=\"horizontal\" x={interpolate(7, 14)} width={interpolate(1, 16)} />\n      </Text>\n      <Text y={2} x={0}>\n        {'<Text>'}{' '}\n        <Text inverse x={Math.round(interpolate(7, 14))} width={Math.round(interpolate(1, 16))}>\n          {' '.repeat(22)}\n        </Text>\n      </Text>\n    </>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/Block.tsx",
    "content": "/* @vhs 80x3@1 */\nimport ReactCurse, { Block } from '..'\n\nconst App = () => {\n  setTimeout(() => {}, 1000)\n\n  return (\n    <>\n      <Block>left</Block>\n      <Block align=\"center\">center</Block>\n      <Block align=\"right\">right</Block>\n    </>\n  )\n}\n\nprocess.stdout.write('\\x1bc')\nReactCurse.inline(<App />)\n"
  },
  {
    "path": "mediacreators/Canvas-1.tsx",
    "content": "/* @vhs 80x3@1 */\nimport ReactCurse, { Canvas, Point, Line, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return (\n    <Canvas width={80} height={6}>\n      <Point x={1} y={1} color=\"BrightGreen\" />\n      <Line x={0} y={5} dx={79} dy={0} />\n    </Canvas>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/Canvas-2.tsx",
    "content": "/* @vhs 80x3@1\nSet FontFamily \"Apple Symbols\"\nSet FontSize 21\n\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 250ms */\nimport ReactCurse, { Canvas, Point, Line, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return (\n    <Canvas width={160} height={12} mode={{ h: 4, w: 2 }}>\n      <Point x={1} y={1} color=\"BrightGreen\" />\n      <Line x={0} y={11} dx={159} dy={0} />\n    </Canvas>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/Frame.tsx",
    "content": "/* @vhs 80x3@1 */\nimport ReactCurse, { Frame, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return (\n    <>\n      <Frame type=\"single\" color=\"Red\">\n        single border type\n      </Frame>\n      <Frame type=\"double\" color=\"Green\" y={0}>\n        double border type\n      </Frame>\n      <Frame type=\"rounded\" color=\"Blue\" y={0}>\n        rounded border type\n      </Frame>\n    </>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/Input-1.tsx",
    "content": "/* @vhs 80x3\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 250ms\nType hello world\nLeft 11\nSleep 1s */\nimport ReactCurse, { Input } from '..'\n\nconst App = () => {\n  return <Input background=\"White\" height={1} width={8} />\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/Input-2.tsx",
    "content": "/* @vhs 80x3\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 250ms\nType hello\nEnter\nType world\nEnter\nEnter\nUp@200ms 4\nDown@200ms 4\nSleep 1s */\nimport ReactCurse, { Input } from '..'\n\nconst App = () => {\n  return <Input background=\"White\" height={3} width={16} />\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/List.tsx",
    "content": "/* @vhs 80x8\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 500ms\nType@100ms jjj\nSleep 500ms\nType@100ms kkk\nSleep 1000ms */\nimport ReactCurse, { List, Text } from '..'\n\nconst App = () => {\n  const items = [...Array(8)].map((_, index) => ({ id: index + 1, title: `Task ${index + 1}` }))\n  return (\n    <List\n      data={items}\n      renderItem={({ item, selected }) => <Text color={selected ? 'BrightGreen' : undefined}>{item.title}</Text>}\n    />\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/ListTable.tsx",
    "content": "/* @vhs 80x8\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 500ms\nType@200ms jjl\nSleep 500ms\nType@200ms kkh\nSleep 1000ms */\nimport ReactCurse, { ListTable, Text } from '..'\n\nconst App = () => {\n  const head = ['id', 'title']\n  const items = [...Array(8)].map((_, index) => [index + 1, `Task ${index + 1}`])\n  return (\n    <ListTable\n      head={head}\n      renderHead={({ item }) =>\n        item.map((i: string, key: string) => (\n          <Text key={key} width={8}>\n            {i}\n          </Text>\n        ))\n      }\n      data={items}\n      renderItem={({ item, x, y, index }) =>\n        item.map((text: string, key: string) => (\n          <Text key={key} color={y === index && x === key ? 'BrightGreen' : undefined} width={8}>\n            {text}\n          </Text>\n        ))\n      }\n    />\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/Scrollbar.tsx",
    "content": "/* @vhs 80x3@1 */\nimport ReactCurse, { Scrollbar, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return <Scrollbar type=\"horizontal\" offset={10} limit={80} length={160} background={254} />\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/Separator.tsx",
    "content": "/* @vhs 80x3@1 */\nimport ReactCurse, { Separator, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return (\n    <>\n      <Separator type=\"vertical\" height={3} />\n      <Separator type=\"horizontal\" y={1} x={1} width={79} />\n    </>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/Spinner.tsx",
    "content": "/* @vhs 80x3\nHide\nType@0 npm_start\nEnter\nSleep 40ms\n\nShow\nSleep 1.5s */\nimport ReactCurse, { Spinner, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return (\n    <>\n      <Spinner block />\n      <Spinner color=\"BrightGreen\">-\\|/</Spinner>\n    </>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/Text.tsx",
    "content": "/* @vhs 80x3@1 */\nimport ReactCurse, { Text, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return (\n    <>\n      <Text color=\"Red\" block>\n        hello world\n      </Text>\n      <Text color=\"Green\" bold block>\n        hello world\n      </Text>\n      <Text color=\"BrightBlue\" underline block>\n        hello world\n      </Text>\n      <Text y={0} x=\"50%\">\n        <Text color={128} italic block>\n          hello world\n        </Text>\n        <Text x=\"100%-11\" color=\"#1ff\" strikethrough block>\n          hello world\n        </Text>\n        <Text x=\"50%-5\" color=\"#e94691\" inverse>\n          hello world\n        </Text>\n      </Text>\n    </>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/Trail.tsx",
    "content": "/* @vhs 80x8\nHide\nType@0 npm_start\nEnter\nSleep 40ms\n\nShow\nSleep 2s */\nimport ReactCurse, { Text, Trail, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  const items = [...Array(8)].map((_, index) => ({ id: index + 1, title: `Task ${index + 1}` }))\n  return (\n    <Trail delay={100}>\n      {items.map(({ id, title }) => (\n        <Text key={id} block>\n          {title}\n        </Text>\n      ))}\n    </Trail>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/View.tsx",
    "content": "/* @vhs 80x8\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 750ms\nType@20ms jjjjjjjjjj\nSleep 250ms\nType@20ms kkkkkkkkkk */\nimport ReactCurse, { View } from '..'\nimport json from '../package.json'\n\nconst App = () => {\n  return <View>{JSON.stringify(json, null, 2)}</View>\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/demo.tsx",
    "content": "/* @vhs 80x16\nSet Theme { \"name\": \"gruvbox\", \"black\": \"#32302f\", \"red\": \"#cc241d\", \"green\": \"#98971a\", \"yellow\": \"#d79921\", \"blue\": \"#458588\", \"magenta\": \"#b16286\", \"cyan\": \"#689d6a\", \"white\": \"#f2e5bc\", \"brightBlack\": \"#1d2021\", \"brightRed\": \"#fb4934\", \"brightGreen\": \"#b8bb26\", \"brightYellow\": \"#fabd2f\", \"brightBlue\": \"#83a598\", \"brightMagenta\": \"#d3869b\", \"brightCyan\": \"#8ec07c\", \"brightWhite\": \"#f9f5d7\", \"background\": \"#282828\", \"foreground\": \"#ecdbb2\", \"selection\": \"#413e3d\", \"cursor\": \"#928374\" }\n\nHide\nType@0 'npm start --src=examples/Todo.tsx'\nEnter\nSleep 400ms\n\nShow\nSleep 2s\n\nHide\nCtrl+c\nType@0 'clear; npm start --src=examples/Visualizer.tsx'\nEnter\nSleep 400ms\n\nShow\nSleep 1s\n\nHide\nCtrl+c\nType@0 'clear; npm start --src=examples/Speed.tsx'\nEnter\nSleep 400ms\n\nShow\nSleep 2s\n\nHide\nCtrl+c\nType@0 'clear; npm start --src=examples/Pong.tsx'\nEnter\nSleep 400ms\n\nShow\nSleep 250ms\nType kkk\nSleep 500ms\nType@500ms jj */\n"
  },
  {
    "path": "mediacreators/exampleAnimate.tsx",
    "content": "/* @vhs 80x3\nHide\nType@0 npm_start\nEnter\nSleep 40ms\n\nShow\nSleep 2s */\nimport ReactCurse, { Text, useAnimation, useInput } from '..'\n\nconst App = () => {\n  useInput()\n  const { interpolate, interpolateColor } = useAnimation(1000)\n\n  return <Text width={interpolate(0, 80)} background={interpolateColor('#282828', '#d79921')} />\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/exampleHello.tsx",
    "content": "/* @vhs 80x3@1 */\nimport ReactCurse, { Text, useInput } from '..'\n\nconst App = ({ text }: { text: string }) => {\n  useInput()\n\n  return <Text color=\"Red\">{text}</Text>\n}\n\nReactCurse.render(<App text=\"hello world\" />)\n"
  },
  {
    "path": "mediacreators/exampleInput.tsx",
    "content": "/* @vhs 80x3x10\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 250ms\nType@250ms kkjkkkjjjj\nSleep 250ms */\nimport ReactCurse, { Text, useInput } from '..'\nimport { useState } from 'react'\n\nconst App = () => {\n  const [counter, setCounter] = useState(0)\n\n  useInput(\n    input => {\n      if (input === 'k') setCounter(counter + 1)\n      if (input === 'j') setCounter(counter - 1)\n      if (input === 'q') ReactCurse.exit()\n    },\n    [counter]\n  )\n\n  return (\n    <Text>\n      counter: <Text bold>{counter.toString()}</Text>\n    </Text>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "mediacreators/logo.tsx",
    "content": "/* @vhs 48x7\nSet Theme { \"green\": \"#98971a\", \"background\": \"#ffffff\", \"foreground\": \"#ecdbb2\" }\n\nHide\nType@0 npm_start\nEnter\nSleep 40ms\n\nShow\nSleep 10s */\nimport ReactCurse, { Bar, Text, useAnimation, useInput } from '..'\n\nconst splitLine = (line: string) => {\n  const chunks: Record<string, any> = {}\n  let chunksAt = 0\n  line.split('').forEach((value, x: number) => {\n    if (value !== ' ') {\n      chunksAt = x + 1\n      return\n    }\n    if (chunks[chunksAt] === undefined) chunks[chunksAt] = ['']\n    chunks[chunksAt] += value\n  })\n  return Object.entries(chunks)\n}\n\nconst mask = `\n### ### ### ### ###     ### # # ### ### ###\n# # #   # # #    #      #   # # # # #   #\n##  ##  ### #    #  ### #   # # ##  ### ##\n# # #   # # #    #      #   # # # #   # #\n# # ### # # ###  #      ### ### # # ### ###\n`.trim()\n\nconst w = Math.max(...mask.split('\\n').map(i => i.length))\n\n// prettier-ignore\nconst lines = [\n  [[w + 32, w], [w * 3 + 32, w * 2]],\n  [[w + 8, w],  [w * 3 + 8, w * 2]],\n  [[w + 16, w], [w * 3 + 16, w * 2]],\n  [[w, w],      [w * 3, w * 2]],\n  [[w + 24, w], [w * 3 + 24, w * 2]],\n]\n\nexport const Logo = ({ ...props }) => {\n  useInput(input => input === '\\x10\\x0d' && ReactCurse.exit())\n\n  const l = 10000\n  const { ms, interpolate } = useAnimation(l)\n\n  return (\n    <Text y={1} x={2} height={5} {...props}>\n      <Text y={0} x={0}>\n        <Text color=\"Green\">{mask}</Text>\n      </Text>\n\n      {[\n        [0, l - 1250],\n        [l - 1200, l - 600],\n        [l - 550, l - 500]\n      ].find(([from, to]) => ms >= from && ms < to) && (\n        <Text y={0} x={0} width={w}>\n          {lines.map((line, key) => (\n            <Text key={key} block>\n              {line.map(([x, width], key) => (\n                <Bar key={key} type=\"horizontal\" x={interpolate(x, x - w * 4, 0, 2000)} width={width} />\n              ))}\n            </Text>\n          ))}\n        </Text>\n      )}\n\n      <Text y={0} x={0} width={w}>\n        {mask.split('\\n').map((line, key) => {\n          return (\n            <Text key={key} block>\n              {splitLine(line).map(([x, str], key) => (\n                <Text key={key} x={parseInt(x)}>\n                  {str as string}\n                </Text>\n              ))}\n              <Text x={line.length}>{' '.repeat(w - line.length)}</Text>\n            </Text>\n          )\n        })}\n      </Text>\n    </Text>\n  )\n}\n\n// ReactCurse.render(<Logo />)\n"
  },
  {
    "path": "mediacreators/useAnimation.tsx",
    "content": "/* @vhs 80x3\nHide\nType@0 npm_start\nEnter\nSleep 40ms\n\nShow\nSleep 2s */\nimport ReactCurse, { Text, useAnimation, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  const { ms, interpolate, interpolateColor } = useAnimation(1000, 4)\n  const rounded = Math.floor(ms / 250) * 250\n  const color = interpolateColor('#000', '#0f8', 0, 1000, rounded)\n  return (\n    <>\n      <Text block>ms: {Math.floor(ms / 250) * 250}</Text>\n      <Text block>interpolate: {Math.round(interpolate(0, 80, 0, 1000, rounded))}</Text>\n      <Text>\n        interpolateColor: <Text color={color}>{color}</Text>\n      </Text>\n    </>\n  )\n}\n\nReactCurse.render(<App />)\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"react-curse\",\n  \"version\": \"1.0.23\",\n  \"description\": \"Fastest terminal UI for react (TUI, CLI, curses-like)\",\n  \"keywords\": [\n    \"ansi\",\n    \"ascii\",\n    \"blessed\",\n    \"cli\",\n    \"console\",\n    \"cursed\",\n    \"curses\",\n    \"ncurses\",\n    \"gui\",\n    \"ncurses\",\n    \"ranger\",\n    \"react\",\n    \"renderer\",\n    \"term\",\n    \"terminal\",\n    \"tmux\",\n    \"tui\",\n    \"unicode\",\n    \"vim\",\n    \"xterm\"\n  ],\n  \"author\": {\n    \"name\": \"Oleksandr Vasyliev\",\n    \"email\": \"infely@gmail.com\",\n    \"url\": \"https://github.com/infely\"\n  },\n  \"repository\": \"infely/react-curse\",\n  \"homepage\": \"https://github.com/infely/react-curse\",\n  \"main\": \"index.ts\",\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"bun run --watch examples/Example.tsx\",\n    \"build\": \"NODE_ENV=production bun build --minify --target=node ${npm_config_src:=examples/Example.tsx} > .dist/index.js\",\n    \"lint\": \"tsc --noEmit\",\n    \"esbuild:start\": \"npx esbuild ${npm_config_src:=examples/Example.tsx} --outfile=.dist/index.js --bundle --platform=node --format=esm --external:'./node_modules/*' --sourcemap && node --enable-source-maps .dist\",\n    \"npm\": \"npx esbuild index.ts --outdir=.npm --bundle --platform=node --format=esm --packages=external\",\n    \"postnpm\": \"tsc --emitDeclarationOnly --declaration --jsx react-jsx --target esnext --esModuleInterop --moduleResolution node index.ts --outdir .npm && bin/postnpm.js\",\n    \"create\": \"npx esbuild create/Create.tsx --outfile=.create/index.js --bundle --platform=node --define:'process.env.NODE_ENV=\\\"production\\\"' --minify --tree-shaking=true\",\n    \"postcreate\": \"bin/postcreate.js\",\n    \"esbuild:dist\": \"npx esbuild ${npm_config_src:=examples/Example.tsx} --outfile=.dist/index.cjs --bundle --platform=node --define:'process.env.NODE_ENV=\\\"production\\\"' --minify --tree-shaking=true\",\n    \"logger\": \"bin/logger.js\"\n  },\n  \"dependencies\": {\n    \"react\": \"^19.1.1\",\n    \"react-reconciler\": \"^0.32.0\"\n  },\n  \"devDependencies\": {\n    \"esbuild\": \"^0.25.9\",\n    \"@eslint/js\": \"^9.34.0\",\n    \"@trivago/prettier-plugin-sort-imports\": \"^5.2.2\",\n    \"@types/node\": \"^24.3.0\",\n    \"@types/react\": \"^19.1.11\",\n    \"@types/react-reconciler\": \"^0.32.0\",\n    \"eslint\": \"^9.34.0\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"globals\": \"^16.3.0\",\n    \"prettier\": \"^3.6.2\",\n    \"typescript\": \"^5.9.2\",\n    \"typescript-eslint\": \"^8.40.0\"\n  }\n}\n"
  },
  {
    "path": "prettier.config.js",
    "content": "import sortImports from '@trivago/prettier-plugin-sort-imports'\n\nexport default {\n  arrowParens: 'avoid',\n  printWidth: 120,\n  semi: false,\n  singleQuote: true,\n  trailingComma: 'none',\n  plugins: [sortImports]\n}\n"
  },
  {
    "path": "readme.md",
    "content": "# react-curse\n\n<div align=\"center\">\n  <br>\n  <img width=\"690\" src=\"media/logo.gif\"><br>\n  <br>\n</div>\n\nFastest terminal UI for react (TUI, CLI, curses-like)\n\n- It is fast, intuitive and easy to use\n- It draws only changed characters\n- It uses a small amount of SSH traffic\n\nSee it in action:\n\n![](media/demo.gif)\n\nStill here? Let's go deeper:\n\n- It has fancy components that are ready to use or can be tree-shaked from your final bundle\n- It supports keyboard and mouse\n- It works in fullscreen and inline modes\n- It has cool hooks like animation with trail\n- It is solely dependent on react\n- It can generate an all-in-one bundle around 100 kb\n\nYou can easily build full-scale terminal UI applications like:\n\n## Apps that use it\n\n- [mngr](https://github.com/infely/mngr) - Database manager supports mongodb, mysql/mariadb, postgresql, sqlite and json-server\n- [nfi](https://github.com/infely/nfi) - Simple nerd fonts icons cheat sheet that allows you to quickly find and copy glyph to clipboard\n- [cosmo](https://github.com/turutupa/cosmo) - A tool for visualizing graphs on terminal. And it allows you to pan around and search by id/value nodes, and in the near future edit/remove/add nodes too\n\n## Installation\n\nJust run `npm init react-curse` answer a few questions and you are ready to go\n\n## Examples\n\n#### Hello world\n\n```jsx\nimport ReactCurse, { Text } from 'react-curse'\n\nconst App = ({ text }) => {\n  return <Text color=\"Red\">{text}</Text>\n}\n\nReactCurse.render(<App text=\"hello world\" />)\n```\n\n![](media/exampleHello.png)\n\n#### How to handle input\n\n```jsx\nimport { useState } from 'react'\nimport ReactCurse, { Text, useInput, exit } from 'react-curse'\n\nconst App = () => {\n  const [counter, setCounter] = useState(0)\n\n  useInput(\n    input => {\n      if (input === 'k') setCounter(counter + 1)\n      if (input === 'j') setCounter(counter - 1)\n      if (input === 'q') exit()\n    },\n    [counter]\n  )\n\n  return (\n    <Text>\n      counter: <Text bold>{counter}</Text>\n    </Text>\n  )\n}\n\nReactCurse.render(<App />)\n```\n\n![](media/exampleInput.gif)\n\n#### How to animate\n\n```jsx\nimport ReactCurse, { useAnimation } from 'react-curse'\n\nconst App = () => {\n  const { interpolate, interpolateColor } = useAnimation(1000)\n\n  return <Text width={interpolate(0, 80)} background={interpolateColor('#282828', '#d79921')} />\n}\n\nReactCurse.render(<App />)\n```\n\n![](media/exampleAnimate.gif)\n\n## Contents\n\n- [Components](#components)\n  - [`<Text>`](#text)\n  - [`<Input>`](#input)\n  - [`<Banner>`](#banner)\n  - [`<Bar>`](#bar)\n  - [`<Block>`](#block)\n  - [`<Canvas>`](#canvas), [`<Point>`](#point), [`<Line>`](#line)\n  - [`<Frame>`](#frame)\n  - [`<List>`](#list)\n  - [`<ListTable>`](#listtable)\n  - [`<Scrollbar>`](#Scrollbar)\n  - [`<Separator>`](#separator)\n  - [`<Spinner>`](#spinner)\n  - [`<View>`](#view)\n- [Hooks](#hooks)\n  - [`useAnimation`](#useanimation), [`useTrail`](#usetrail), [`<Trail>`](#trail)\n  - [`useChildrenSize`](#usechildrensize)\n  - [`useClipboard`](#useclipboard)\n  - [`useInput`](#useinput)\n  - [`useMouse`](#usemouse)\n  - [`useSize`](#usesize)\n  - [`useWordWrap`](#useWordWrap)\n- [API](#api)\n  - [`render`](#render)\n  - [￼`inline`￼](#inline)\n  - [`bell`](#bell)\n  - [`exit`](#exit)\n\n## Components\n\n### `<Text>`\n\nBase component\\\nThe only component required to do anything\\\nEvery other component uses this one to draw\n\n##### y?, x?: `number` | `string`\n\nPosition from top left corner relative to parent\\\nContent will be cropped by parent\\\nSee `absolute` to avoid this behavior\\\nExample: `32, '100%', '100%-8'`\n\n##### height?, width?: `number` | `string`\n\nSize of block, will be cropped by parent\\\nSee `absolute` to avoid this behavior\n\n##### absolute?: `boolean`\n\nMakes position and size ignoring parent container\n\n##### background?, color?: `number` | `string`\n\nBackground and foreground color\\\nExample: `31, 'Red', '#f04020', '#f42'`\n\n##### clear?: `boolean`\n\nClears block before drawing content\\\n`height` and `width`\n\n##### block?: `boolean`\n\nMoves cursor to a new line after its content relative to parent\n\n##### bold?, dim?, italic?, underline?, blinking?, inverse?, strikethrough?: `boolean`\n\nText modifiers\n\n#### Examples\n\n```jsx\n<Text color=\"Red\" block>hello world</Text>\n<Text color=\"Green\" bold block>hello world</Text>\n<Text color=\"BrightBlue\" underline block>hello world</Text>\n<Text y={0} x=\"50%\">\n  <Text color={128} italic block>hello world</Text>\n  <Text x=\"100%-11\" color=\"#1ff\" strikethrough block>hello world</Text>\n  <Text x=\"50%-5\" color=\"#e94691\" inverse>hello world</Text>\n</Text>\n```\n\n![](media/Text.png)\n\n### `<Input>`\n\nText input component with cursor movement and text scroll support\\\nIf its height is more than 1, then it switches to multiline, like textarea\\\nMost terminal shortcuts are supported\n\n##### focus?: `boolean` = `true`\n\nMakes it active\n\n##### type?: `'text'` | `'password'` | `'hidden'` = `‘text'`\n\n##### initialValue?: `string`\n\n##### cursorBackground?: `number` | `string`\n\n##### onCancel?: `() => void`\n\n##### onChange?: `(string) => void`\n\n##### onSubmit?: `(string) => void`\n\n#### Examples\n\n```jsx\n<Input background=\"#404040\" height={1} width={8} />\n```\n\n![](media/Input-1.gif)\n\n![](media/Input-2.gif)\n\n### `<Banner>`\n\nDisplays big text\n\n##### y?, x?: `number` | `string`\n\n##### background?, color?: `number` | `string`\n\n##### children: `string`\n\n#### Examples\n\n```jsx\n<Banner>{new Date().toTimeString().substring(0, 8)}</Banner>\n```\n\n![](media/Banner.png)\n\n### `<Bar>`\n\nDisplays vertical or horizontal bar with 1/8 character resolution\n\n##### type: `'vertical'` | `'horizontal'`\n\n##### y & height, x & width: `number`\n\n#### Examples\n\n```jsx\n<>\n  {[...Array(24)].map((_, index) => (\n    <Bar key={index} type=\"vertical\" x={index * 2} height={(index + 1) / 8} />\n  ))}\n</>\n```\n\n![](media/Bar-1.png)\n\nCompare to `<Text>`\n\n![](media/Bar-2.gif)\n\n### `<Block>`\n\nAligns content\n\n##### width?: `number`\n\n##### align?: `'left'` | `'center'` | `'right'` = `'left'`\n\n#### Examples\n\n```jsx\n<Block>left</Block>\n<Block align=\"center\">center</Block>\n<Block align=\"right\">right</Block>\n```\n\n![](media/Block.png)\n\n### `<Canvas>`\n\nCreate a canvas for drawing with one these modes\n\n##### mode: `{ h: 1, w: 1 }` | `{ h: 2, w: 1 }` | `{ h: 2, w: 2 }` | `{ h: 4, w: 2 }`\n\nPixels per character\n\n##### height, width: `number`\n\nSize in pixels\n\n##### children: (`Point` | `Line`)`[]`\n\n#### `<Point>`\n\nDraws a point at the coordinates\n\n##### y, x: `number`\n\n##### color?: `number` | `string`\n\n#### `<Line>`\n\nDraws a line using coordinates\n\n##### y, x, dy, dx: `number`\n\n##### color?: `number` | `string`\n\n#### Examples\n\n```jsx\n<Canvas width={80} height={6}>\n  <Point x={1} y={1} color=\"Yellow\" />\n  <Line x={0} y={5} dx={79} dy={0} />\n</Canvas>\n```\n\n![](media/Canvas-1.png)\n\nBraille's font demo (`{ h: 4, w: 2 }`)\n\n![](media/Canvas-2.png)\n\n### `<Frame>`\n\nDraws frame around its content\n\n##### children: `string`\n\n##### type?: `'single'` | `'double'` | `'rounded'` = `'single'`\n\n##### height?, width?: `number`\n\n#### Examples\n\n```jsx\n<Frame type=\"single\" color=\"Red\">single border type</Frame>\n<Frame type=\"double\" color=\"Green\" y={0}>double border type</Frame>\n<Frame type=\"rounded\" color=\"Blue\" y={0}>rounded border type</Frame>\n```\n\n![](media/Frame.png)\n\n### `<List>`\n\nCreates a list with navigation support\\\nVim shortcuts are supported\n\n##### focus?: `boolean`\n\n##### initialPos?: { y: `number` }\n\n##### data?: `any[]`\n\n##### renderItem?: `(object) => JSX.Element`\n\n##### height?, width?: `number`\n\n##### scrollbar?: `boolean`\n\n##### scrollbarBackground?: `boolean`\n\n##### scrollbarColor?: `boolean`\n\n##### vi?: `boolean` = `true`\n\n##### pass?: `any`\n\n##### onChange?: `(object) => void`\n\n##### onSubmit?: `(object) => void`\n\n#### Examples\n\n```jsx\nconst items = [...Array(8)].map((_, index) => ({ id: index + 1, title: `Task ${index + 1}` }))\nreturn (\n  <List\n    data={items}\n    renderItem={({ item, selected }) => <Text color={selected ? 'Green' : undefined}>{item.title}</Text>}\n  />\n)\n```\n\n![](media/List.gif)\n\n### `<ListTable>`: `<List>`\n\nCreates a table with navigation support\\\nVim shortcuts are supported\n\n##### mode?: `'cell'` | `'row'` = `'cell'`\n\n##### head?: `any[]`\n\n##### renderHead?: `(object) => JSX.Element`\n\n##### data?: `any[][]`\n\n#### Examples\n\n```jsx\nconst head = ['id', 'title']\nconst items = [...Array(8)].map((_, index) => [index + 1, `Task ${index + 1}`])\nreturn (\n  <ListTable\n    head={head}\n    renderHead={({ item }) =>\n      item.map((i, key) => (\n        <Text key={key} width={8}>\n          {i}\n        </Text>\n      ))\n    }\n    data={items}\n    renderItem={({ item, x, y, index }) =>\n      item.map((text, key) => (\n        <Text key={key} color={y === index && x === key ? 'Green' : undefined} width={8}>\n          {text}\n        </Text>\n      ))\n    }\n  />\n)\n```\n\n![](media/ListTable.gif)\n\n### `<Scrollbar>`\n\nDraws a scrollbar with 1/8 character resolution\n\n##### type?: `'vertical'` | `'horizontal'` = `'vertical'`\n\n##### offset: `number`\n\n##### limit: `number`\n\n##### length: `number`\n\n##### background?, color?: `number` | `string`\n\n#### Examples\n\n```jsx\n<Scrollbar type=\"horizontal\" offset={10} limit={80} length={160} />\n```\n\n![](media/Scrollbar.png)\n\n### `<Separator>`\n\nDraws a vertical or horizontal line\n\n##### type: `'vertical'` | `'horizontal'`\n\n##### height, width: `number`\n\n#### Examples\n\n```jsx\n<Separator type=\"vertical\" height={3} />\n<Separator type=\"horizontal\" y={1} x={1} width={79} />\n```\n\n![](media/Separator.png)\n\n### `<Spinner>`\n\nDraws an animated spinner\n\n##### children?: `string`\n\n#### Examples\n\n```jsx\n<Spinner block />\n<Spinner color=\"BrightGreen\">-\\|/</Spinner>\n```\n\n![](media/Spinner.gif)\n\n### `<View>`\n\nCreates a scrollable viewport\\\nVim shortcuts are supported\n\n##### focus?: `boolean`\n\n##### height?: `number`\n\n##### scrollbar?: `boolean`\n\n##### vi?: `boolean` = `true`\n\n##### children: `any`\n\n#### Examples\n\n```jsx\n<View>{JSON.stringify(json, null, 2)}</View>\n```\n\n![](media/View.gif)\n\n## hooks\n\n### `useAnimation`\n\n##### (time: `number`, fps?: `'number'` = `60`) => `object`\n\nCreates a timer for a specified duration\\\nThat gives you time and interpolation functions each frame of animation\n\n#### return\n\n##### ms: `number`\n\n##### interpolate: (from: `number`, to: `number`, delay?: `number`)\n\n##### interpolateColor: (from: `string`, to: `string`: delay?: `number`)\n\n#### Examples\n\n```jsx\nconst { ms } = useAnimation(1000, 4)\nreturn ms // 0, 250, 500, 750, 1000\n```\n\n```jsx\nconst { interpolate } = useAnimation(1000, 4)\nreturn interpolate(0, 80) // 0, 20, 40, 60, 80\n```\n\n```jsx\nconst { interpolateColor } = useAnimation(1000, 4)\nreturn interpolateColor('#000', '#0f8') // #000, #042, #084, #0c6, #0f8\n```\n\n![](media/useAnimation.gif)\n\n#### `<Trail>`\n\nMutate array of items to show one by one with latency\n\n##### delay: `number`\n\n##### children: `JSX.Element[]`\n\n#### Examples\n\n```jsx\nconst items = [...Array(8)].map((_, index) => ({ id: index + 1, title: `Task ${index + 1}` }))\nreturn (\n  <Trail delay={100}>\n    {items.map(({ id, title }) => (\n      <Text key={id} block>\n        {title}\n      </Text>\n    ))}\n  </Trail>\n)\n```\n\n![](media/Trail.gif)\n\n#### `useTrail`\n\n##### (delay: `number`, items: `JSX.Element[]`, key?: `string` = `'key'`) => `JSX.Element[]`\n\nSame as `<Trail>` but hook\\\nYou can pass it to `data` property of `<List>` component for example\n\n#### Examples\n\n```jsx\n<List data={useTrail(items)} />\n```\n\n### `useChildrenSize`\n\n##### (value: `string`) => `object`\n\nGives you content size\n\n#### return\n\n##### height, width: `number`\n\n#### Examples\n\n```jsx\nuseChildrenSize('1\\n22\\n333') // { height: 3, width: 3 }\n```\n\n### `useClipboard`\n\n#### () => `array`\n\nAllows you to work with the system clipboard\n\n#### return\n\n##### getClipboard: `() => string`\n\n##### setClipboard: `(value: string) => void`\n\n#### Examples\n\n```jsx\nconst { getClipboard, setClipboard } = useClipboard()\nconst string = getClipboard()\nsetClipboard(string.toUpperCase()) // copied\n```\n\n### `useInput`\n\n##### (callback: `(string) => void`, dependencies: `any[]`) => `void`\n\nAllows you to handle keyboard input\n\n#### Examples\n\n```jsx\nset[(counter, setCounter)] = useState(0)\n\nuseInput(\n  input => {\n    if (input === 'k') setCounter(counter + 1)\n    if (input === 'j') setCounter(counter - 1)\n  },\n  [counter]\n)\n```\n\n### `useMouse`\n\n##### (callback: `(object) => void`, dependencies: `any[]`)\n\nAllows you to handle mouse input\n\n#### Examples\n\n```jsx\nset[(counter, setCounter)] = useState(0)\n\nuseMouse(\n  event => {\n    if (event.type === 'wheelup') setCounter(counter + 1)\n    if (event.type === 'wheeldown') setCounter(counter - 1)\n  },\n  [counter]\n)\n```\n\n### `useSize`\n\n##### () => `object`\n\nGives you terminal size\\\nUpdates when size is changing\n\n#### return\n\n##### height, width: `number`\n\n#### Examples\n\n```jsx\nuseSize() // { height: 24, width: 80 }\n```\n\n### `useWordWrap`\n\n##### (text: `string`, width?: `number`) => `object`\n\nGives your text a word wrap\n\n#### return\n\n##### height, width: `number`\n\n#### Examples\n\n```jsx\nuseWordWrap('hello world', 5) // hello\\nworld\n```\n\n## API\n\n### `render` (children: `JSX.Element`) => `void`\n\nRenders your fullscreen application to `stdout`\n\n### `inline` (children: `JSX.Element`) => `void`\n\nRenders your inline application to `stdout`\n\n### `bell`\n\n#### () => `void`\n\nMakes a terminal bell\n\n```jsx\nbell() // ding\n```\n\n### `exit`\n\n##### (code: `number` = `0`) => `void`\n\nAllows you to exit from an application that waits for user input or has timers\n\n#### Examples\n\n```jsx\nuseInput(input => {\n  if (input === 'q') exit()\n})\n```\n"
  },
  {
    "path": "reconciler.ts",
    "content": "import { type TextProps } from './components/Text'\nimport Reconciler from 'react-reconciler'\n\nlet currentUpdatePriority = 0\n\nexport class TextElement {\n  props: TextProps\n  parent: TextElement | null\n  children: TextElement[]\n\n  constructor(props: object = {}) {\n    this.props = props\n    this.parent = null\n    this.children = []\n  }\n\n  terminate() {\n    this.children = []\n  }\n\n  appendChild(child: any) {\n    this.children = [...this.children, child]\n  }\n\n  commitUpdate(nextProps: any) {\n    this.props = nextProps\n  }\n\n  insertBefore(child: any, beforeChild: any) {\n    const index = this.children.indexOf(beforeChild)\n    if (index !== -1) this.children.splice(index, 0, child)\n  }\n\n  removeChild(child: any) {\n    const index = this.children.indexOf(child)\n    if (index !== -1) this.children.splice(index, 1)\n  }\n}\n\nexport class TextInstance {\n  value: string\n\n  constructor(value: string) {\n    this.value = value\n  }\n\n  commitTextUpdate(value: string) {\n    this.value = value\n  }\n\n  toString() {\n    return this.value\n  }\n}\n\nexport default (resetAfterCommit: () => void) => {\n  // prettier-ignore\n  const reconciler = Reconciler({\n    supportsMutation: true,\n    appendChild(parentInstance: any, child: any) { parentInstance.appendChild(child) },\n    appendChildToContainer(container: any, child: any) { container.appendChild(child) },\n    appendInitialChild(parentInstance: any, child: any) { parentInstance.appendChild(child) },\n    clearContainer() {},\n    commitTextUpdate(textInstance: any, _oldText: any, newText: any) { textInstance.commitTextUpdate(newText) },\n    commitUpdate(instance: any,  _type: any, _prevProps: any, nextProps: any) { instance.commitUpdate(nextProps) },\n    createInstance(type: any, props: any) { if (type === 'text') { return new TextElement(props) } else { throw new Error('must be <Text>') } },\n    createTextInstance(text: string) { return new TextInstance(text) },\n    detachDeletedInstance() {},\n    finalizeInitialChildren() { return false },\n    getChildHostContext() { return {} },\n    getPublicInstance(instance: any) { return instance },\n    getRootHostContext(rootContainer: any) { return rootContainer },\n    insertBefore(parentInstance: any, child: any, beforeChild: any) { parentInstance.insertBefore(child, beforeChild) },\n    insertInContainerBefore(container: any, child: any, beforeChild: any) { container.insertBefore(child, beforeChild) },\n    prepareForCommit() { return null },\n    // @ts-expect-error any\n    prepareUpdate() { return true },\n    removeChild(parentInstance: any, child: any) { parentInstance.removeChild(child) },\n    removeChildFromContainer(container: any, child: any) { container.removeChild(child) },\n    resetAfterCommit() { resetAfterCommit() },\n    shouldSetTextContent() { return false },\n\n    setCurrentUpdatePriority(newPriority: number) { currentUpdatePriority = newPriority },\n    getCurrentUpdatePriority() { return currentUpdatePriority },\n    resolveUpdatePriority() { return currentUpdatePriority !== 0 ? currentUpdatePriority : 16 },\n    maySuspendCommit() { return false },\n  })\n\n  return reconciler\n}\n"
  },
  {
    "path": "renderer.ts",
    "content": "import Input from './input'\nimport Reconciler, { TextElement } from './reconciler'\nimport Screen from './screen'\nimport Term from './term'\nimport { spawnSync, type SpawnSyncOptions, type SpawnSyncReturns } from 'child_process'\nimport { type ReactElement } from 'react'\n\nclass Renderer {\n  container: TextElement\n  screen: Screen\n  input: Input\n  term: Term\n  reconciler: ReturnType<typeof Reconciler>\n  callback?: (value: any) => void\n  throttleAt = 0\n  throttleTimeout?: NodeJS.Timeout\n\n  constructor() {\n    this.container = new TextElement()\n    this.screen = new Screen()\n    this.input = new Input()\n    this.reconciler = Reconciler(this.throttle)\n    this.term = new Term() // TODO:\n  }\n\n  render(reactElement: ReactElement, options = { fullscreen: true, print: false }) {\n    this.term = new Term()\n    this.term.init(options.fullscreen, options.print).then(() => {\n      this.reconciler.updateContainer(\n        reactElement,\n        this.reconciler.createContainer(this.container, 0, null, false, null, '', () => {}, null)\n      )\n    })\n  }\n\n  inline(reactElement: ReactElement, options = { fullscreen: false, print: false }) {\n    this.render(reactElement, options)\n  }\n\n  prompt<T>(reactElement: ReactElement, options = { fullscreen: false, print: false }): Promise<T> {\n    this.render(reactElement, options)\n\n    return new Promise(resolve => {\n      this.callback = resolve\n    })\n  }\n\n  print(reactElement: ReactElement, options = { fullscreen: false, print: true }) {\n    this.render(reactElement, options)\n\n    return new Promise(resolve => {\n      this.callback = resolve\n    })\n  }\n\n  frame(reactElement: ReactElement, options = { fullscreen: false, print: true }) {\n    this.render(reactElement, options)\n\n    return new Promise(resolve => {\n      this.callback = (value: any) => {\n        process.stdout.write(value)\n        resolve(value)\n      }\n    })\n  }\n\n  terminate(value: any) {\n    this.container.terminate()\n    this.input.terminate()\n    if (this.term) {\n      this.term.terminate()\n      // process.stdout.write(this.term.terminate())\n    }\n    this.callback?.(value)\n  }\n\n  spawnSync(\n    command: string,\n    args: ReadonlyArray<string>,\n    options: SpawnSyncOptions\n  ): SpawnSyncReturns<string | Buffer> {\n    const res = spawnSync(command, args, options)\n    this.term?.reinit()\n    this.term?.render(this.screen.buffer)\n    return res\n  }\n\n  bell() {\n    process.stdout.write('\\x07')\n  }\n\n  exit(code: number | any = 0) {\n    if (typeof code === 'number') process.exit(code)\n    this.term?.setResult(code)\n  }\n\n  private throttle = () => {\n    const at = Date.now()\n    const nextAt = Math.max(0, 1000 / 60 - (at - this.throttleAt))\n    clearTimeout(this.throttleTimeout)\n    this.throttleTimeout = setTimeout(() => {\n      this.throttleAt = at\n      this.screen.render(this.container.children)\n      this.term?.render(this.screen.buffer)\n      this.input.render()\n    }, nextAt)\n  }\n}\n\nexport default new Renderer()\n"
  },
  {
    "path": "screen.ts",
    "content": "import { type TextProps } from './components/Text'\nimport { type TextElement } from './reconciler'\nimport { type ReactElement } from 'react'\n\nexport type Color =\n  | number\n  | string\n  | 'Black'\n  | 'Red'\n  | 'Green'\n  | 'Yellow'\n  | 'Blue'\n  | 'Magenta'\n  | 'Cyan'\n  | 'White'\n  | 'BrightBlack'\n  | 'BrightRed'\n  | 'BrightGreen'\n  | 'BrightYellow'\n  | 'BrightBlue'\n  | 'BrightMagenta'\n  | 'BrightCyan'\n  | 'BrightWhite'\n\nexport interface Modifier {\n  background?: Color\n  color?: Color\n  clear?: boolean\n  bold?: boolean\n  dim?: boolean\n  italic?: boolean\n  underline?: boolean\n  blinking?: boolean\n  inverse?: boolean\n  strikethrough?: boolean\n}\n\nexport type Char = [string, Modifier]\n\ninterface Bounds {\n  x: number\n  y: number\n  x1: number\n  y1: number\n  x2: number\n  y2: number\n}\n\nclass Screen {\n  buffer: Char[][]\n  cursor = { x: 0, y: 0 }\n  size = { x1: 0, y1: 0, x2: 0, y2: 0 }\n\n  constructor() {\n    this.buffer = this.generateBuffer()\n  }\n\n  generateBuffer() {\n    this.size = { x1: 0, y1: 0, x2: process.stdout.columns, y2: process.stdout.rows }\n    return [...Array(this.size.y2)].map(() => [...Array(this.size.x2)].map(() => [' ', {}] as Char))\n  }\n\n  clearBuffer() {\n    this.buffer = this.generateBuffer()\n    this.cursor = { x: 0, y: 0 }\n  }\n\n  render(elements: TextElement[]) {\n    this.clearBuffer()\n    this.renderElement(elements, { ...this.cursor, ...this.size })\n  }\n\n  stringAt(value: string, limit: number) {\n    const percent = parseFloat(value)\n    let diff = ''\n\n    const index = value.search(/%[+-]\\d+$/)\n    if (index !== -1) diff = value.substring(index + 1)\n    if (!value.endsWith('%' + diff) || isNaN(percent)) throw new Error('must be percent')\n\n    return Math.round((limit / 100) * percent) + parseInt(diff || '0')\n  }\n\n  renderElement(element: ReactElement | ReactElement[] | any, prevBounds: Bounds, prevProps: TextProps = {}) {\n    if (Array.isArray(element)) return element.forEach(i => this.renderElement(i, prevBounds, prevProps))\n\n    const { children, ...props } = element.props ?? { children: element }\n\n    if (typeof props.x === 'string')\n      props.x = this.stringAt(props.x, props.absolute ? this.buffer[0].length : prevBounds.x2 - prevBounds.x)\n    if (typeof props.y === 'string')\n      props.y = this.stringAt(props.y, props.absolute ? this.buffer.length : prevBounds.y2 - prevBounds.y)\n    if (typeof props.width === 'string')\n      props.width = this.stringAt(props.width, props.absolute ? this.buffer[0].length : prevBounds.x2 - prevBounds.x)\n    if (typeof props.height === 'string')\n      props.height = this.stringAt(props.height, props.absolute ? this.buffer.length : prevBounds.y2 - prevBounds.y)\n    if (props.width !== undefined && isNaN(props.width)) props.width = 0\n    if (props.height !== undefined && isNaN(props.height)) props.height = 0\n    const x = props.x !== undefined ? (props.absolute ? 0 : prevBounds.x) + props.x : this.cursor.x\n    const y = props.y !== undefined ? (props.absolute ? 0 : prevBounds.y) + props.y : this.cursor.y\n    const x1 =\n      props.x !== undefined\n        ? props.absolute\n          ? props.x\n          : Math.max(prevBounds.x, prevBounds.x + props.x)\n        : prevBounds.x1\n    const y1 =\n      props.y !== undefined\n        ? props.absolute\n          ? props.y\n          : Math.max(prevBounds.y, prevBounds.y + props.y)\n        : prevBounds.y1\n    const x2 =\n      props.width !== undefined\n        ? Math.min(props.absolute ? this.buffer[0].length : prevBounds.x2, props.width + x)\n        : props.absolute\n          ? this.buffer[0].length\n          : prevBounds.x2\n    const y2 =\n      props.height !== undefined\n        ? Math.min(props.absolute ? this.buffer.length : prevBounds.y2, props.height + y)\n        : props.absolute\n          ? this.buffer.length\n          : prevBounds.y2\n    const bounds = { x, y, x1, y1, x2, y2 }\n    this.cursor.x = bounds.x\n    this.cursor.y = bounds.y\n\n    const modifiers = Object.fromEntries(\n      ['color', 'background', 'bold', 'dim', 'italic', 'underline', 'blinking', 'inverse', 'strikethrough']\n        .map(i => [i, props[i] ?? (prevProps as any)[i]])\n        .filter(i => i[1])\n    )\n    if ((props.background || props.clear) && (props.width || props.height))\n      this.fill(bounds, props.absolute ? bounds : prevBounds, modifiers)\n\n    if (Array.isArray(children) || children?.props) {\n      this.renderElement(element.children, bounds, modifiers)\n    } else if (typeof children === 'number' || children) {\n      const text = children.toString()\n      if (text.includes('\\n')) {\n        const lines = children.toString().split('\\n')\n        lines.forEach((line: string, index: number) => {\n          this.renderElement(line, bounds, modifiers)\n          if (index < lines.length - 1) this.carret(prevBounds)\n        })\n      } else {\n        this.cursor.x = this.put(text, bounds, modifiers)\n      }\n    }\n\n    if (props.block) this.carret(prevBounds)\n    if (props.width || props.height) {\n      this.cursor.x = props.block ? prevBounds.x : bounds.x2\n      this.cursor.y = props.block ? bounds.y2 : prevBounds.y\n    }\n  }\n\n  fill(bounds: Bounds, prevBounds: Bounds, modifiers: TextProps) {\n    for (let y = bounds.y; y < bounds.y2; y++) {\n      if (y < Math.max(0, prevBounds.y1) || y >= Math.min(prevBounds.y2, this.buffer.length)) continue\n      for (let x = bounds.x; x < bounds.x2; x++) {\n        if (x < Math.max(0, prevBounds.x1) || x >= Math.min(prevBounds.x2, this.buffer[y].length)) continue\n\n        this.buffer[y][x] = [' ', modifiers]\n      }\n    }\n  }\n\n  put(text: string, bounds: Bounds, modifiers: TextProps) {\n    const { x, y } = bounds\n\n    let i: number\n    for (i = 0; i < text.length; i++) {\n      if (y < Math.max(0, bounds.y1) || y >= Math.min(this.buffer.length, bounds.y2)) break\n      if (x + i < Math.max(0, bounds.x1) || x + i >= Math.min(this.buffer[y].length, bounds.x2)) continue\n\n      this.buffer[y][x + i] = [text[i], modifiers]\n    }\n\n    return x + i\n  }\n\n  carret(bounds: Bounds) {\n    this.cursor.x = bounds.x ?? 0\n    this.cursor.y++\n  }\n}\n\nexport default Screen\n"
  },
  {
    "path": "term.ts",
    "content": "import Renderer from './renderer'\nimport { type Char, type Color, type Modifier } from './screen'\n\nconst ESC = '\\x1B'\n\nclass Term {\n  fullscreen = true\n  print = false\n  isResized = false\n  isMouseEnabled = false\n  prevBuffer: Char[][] | undefined\n  prevModifier: Modifier = {}\n  nextWritePrefix = ''\n  size = { width: process.stdout.columns, height: process.stdout.rows }\n  offset = { x: 0, y: 0 }\n  cursor = { x: 0, y: 0 }\n  maxCursor = { x: 0, y: 0 }\n  result: any\n\n  async init(fullscreen: boolean, print: boolean) {\n    this.fullscreen = fullscreen\n    this.print = print\n\n    process.stdout.on('resize', () => {\n      this.isResized = true\n\n      // this.offset.y += process.stdout.rows - this.size.height\n      this.size = { width: process.stdout.columns, height: process.stdout.rows }\n    })\n\n    process.on('exit', this.onExit)\n\n    if (fullscreen) {\n      this.append(`${ESC}[?1049h`) // enables the alternative buffer\n      this.append(`${ESC}c`) // clear screen\n    } else {\n      const cursor = await this.termGetCursor()\n      this.offset = cursor\n    }\n    this.append(`${ESC}[?25l`) // make cursor invisible\n  }\n\n  reinit() {\n    this.prevModifier = {}\n    this.prevBuffer = undefined\n    this.append(`${ESC}[?1049h${ESC}c${ESC}[?25l`) // enables the alternative buffer, clear screen, make cursor invisible\n  }\n\n  onExit = (code: number) => {\n    if (code !== 0) return\n\n    process.stdout.write(this.terminate())\n    process.exit(0)\n  }\n\n  terminate() {\n    process.off('exit', this.onExit)\n\n    const sequence: string[] = []\n    if (this.fullscreen) {\n      sequence.push(`${ESC}[?1049l`) // disables the alternative buffer\n    } else {\n      const y = this.maxCursor.y - this.cursor.y\n      if (y > 0) sequence.push(`${ESC}[${y}B`) // moves cursor down\n      const x = this.maxCursor.x - this.cursor.x + 1\n      if (x > 0) sequence.push(`${ESC}[${x}C`) // moves cursor right\n      sequence.push(`\\n`)\n    }\n    sequence.push(`${ESC}[?25h`) // make cursor visible\n    if (this.isMouseEnabled) sequence.push(`${ESC}[?1000l`) // disable mouse\n    return sequence.join('')\n  }\n\n  append(value: string) {\n    this.nextWritePrefix += value\n  }\n\n  setResult(result: any) {\n    this.result = result\n  }\n\n  enableMouse() {\n    this.append(`${ESC}[?1000h${ESC}[?1005h`) // enable mouse\n    this.isMouseEnabled = true\n  }\n\n  async termGetCursor(): Promise<{ x: number; y: number }> {\n    process.stdin.setRawMode(true)\n    process.stdout.write('\\x1b[6n')\n    return await new Promise(resolve => {\n      process.stdin.once('data', data => {\n        const [x, y] = data\n          .toString()\n          .slice(2, -1)\n          .split(';')\n          .reverse()\n          .map(i => parseInt(i) - 1)\n        resolve({ x, y })\n        // process.stdin.unref()\n        // process.stdin.setRawMode(false)\n      })\n    })\n  }\n\n  parseHexColor(color: string) {\n    if (!color.match(/^([\\da-f]{6})|([\\da-f]{3})$/i)) return\n\n    return (\n      color.length === 4\n        ? color\n            .substring(1, 4)\n            .split('')\n            .map(i => i + i)\n        : (color.substring(1, 7).match(/.{2}/g) as any)\n    ).map((i: string) => parseInt(i, 16))\n  }\n\n  parseColor(color: Color | string | number, offset = 0) {\n    if (typeof color === 'number') {\n      if (color < 0 || color > 255) throw new Error('color not found')\n      return `${38 + offset};5;${color}`\n    }\n\n    if (color.startsWith('#')) {\n      const [r, g, b] = this.parseHexColor(color)\n      return `${38 + offset};2;${r};${g};${b}`\n    }\n\n    const names = {\n      black: 30,\n      red: 31,\n      green: 32,\n      yellow: 33,\n      blue: 34,\n      magenta: 35,\n      cyan: 36,\n      white: 37,\n      brightblack: 90,\n      brightred: 91,\n      brightgreen: 92,\n      brightyellow: 93,\n      brightblue: 94,\n      brightmagenta: 95,\n      brightcyan: 96,\n      brightwhite: 97\n    }\n    const colorFromName = (names as any)[color.toLowerCase()]\n    if (colorFromName === undefined) throw new Error('color not found')\n    return colorFromName + offset\n  }\n\n  createModifierSequence(modifier: Modifier) {\n    if (JSON.stringify(modifier) === '{}') return '0'\n\n    const { prevModifier } = this\n\n    const sequence: (number | string)[] = []\n\n    if (modifier.color !== prevModifier.color) sequence.push(modifier.color ? this.parseColor(modifier.color) : 39)\n    if (modifier.background !== prevModifier.background)\n      sequence.push(modifier.background ? this.parseColor(modifier.background, 10) : 49)\n    if (modifier.bold !== prevModifier.bold) sequence.push(modifier.bold ? 1 : modifier.dim ? '22;2' : 22)\n    if (modifier.dim !== prevModifier.dim) sequence.push(modifier.dim ? 2 : modifier.bold ? '22;1' : 22)\n    if (modifier.italic !== prevModifier.italic) sequence.push(modifier.italic ? 3 : 23)\n    if (modifier.underline !== prevModifier.underline) sequence.push(modifier.underline ? 4 : 24)\n    if (modifier.blinking !== prevModifier.blinking) sequence.push(modifier.blinking ? 5 : 25)\n    if (modifier.inverse !== prevModifier.inverse) sequence.push(modifier.inverse ? 7 : 27)\n    if (modifier.strikethrough !== prevModifier.strikethrough) sequence.push(modifier.strikethrough ? 9 : 29)\n\n    return sequence.join(';')\n  }\n\n  isIcon(char: string) {\n    const code = char.charCodeAt(0)\n    return (code >= 9211 && code <= 9214) || [9829, 9889, 11096].includes(code) || (code >= 57344 && code <= 64838)\n  }\n\n  render(buffer: Char[][]) {\n    let full = false\n    let result = ''\n\n    if (this.isResized) {\n      if (this.fullscreen) {\n        result += `${ESC}[H` // moves cursor to home position\n        this.cursor = { x: 0, y: 0 }\n        full = true\n      }\n      this.isResized = false\n    }\n\n    for (let y = 0; y < buffer.length; y++) {\n      const line = buffer[y]\n      const prevLine = this.prevBuffer?.[y]\n      let includesEmoji = false\n      let includesIcon = false\n\n      const diffLine = full\n        ? line\n        : line\n            .map((i: Char, x: number) => {\n              const [prevChar, prevModifier] = prevLine && prevLine[x] ? prevLine[x] : [' ', {}]\n              const [char, modifier] = i\n              return prevChar !== char || JSON.stringify(prevModifier) !== JSON.stringify(modifier) ? i : null\n            })\n            .filter(i => i !== undefined)\n\n      const chunks: Record<string, [any, any]> = {}\n      let chunksAt = 0\n      diffLine.forEach((value, x: number) => {\n        if (value === null) {\n          chunksAt = x + 1\n          return\n        }\n\n        const [char, modifier] = value\n        if (chunks[chunksAt] === undefined) chunks[chunksAt] = ['', '']\n        if (JSON.stringify(modifier) !== JSON.stringify(this.prevModifier)) {\n          chunks[chunksAt][1] += `\\x1b[${this.createModifierSequence(modifier)}m`\n          this.prevModifier = modifier\n        }\n        chunks[chunksAt][0] += char\n        chunks[chunksAt][1] += char\n      })\n\n      Object.entries(chunks).map(([index, value]) => {\n        const [str, strWithModifiers] = value as [string, string]\n        const x = parseInt(index)\n        if (/\\p{Emoji}/u.test(str)) includesEmoji = true\n        if (!includesIcon && str.split('').find((i: string) => this.isIcon(i))) includesIcon = true\n\n        if (x === 0 && y === this.cursor.y + 1) {\n          if (!this.fullscreen && y > this.maxCursor.y) {\n            this.offset.y -= 1\n          }\n\n          result += '\\n'\n        } else {\n          if (!this.fullscreen && y > this.cursor.y && y > this.maxCursor.y) {\n            const diff = y - this.maxCursor.y\n            result += '\\n'.repeat(diff)\n            this.cursor = { y: this.cursor.y + diff, x: 0 }\n\n            const rows = this.offset.y + y - (process.stdout.rows - 1)\n            if (rows > 0) this.offset.y -= rows\n          }\n\n          if (y !== this.cursor.y && x !== this.cursor.x) {\n            result += `${ESC}[${y + 1 + this.offset.y};${x + 1}H` // move cursor to position\n          } else if (y > this.cursor.y) {\n            const diff = y - this.cursor.y\n            result += `${ESC}[${diff > 1 ? diff : ''}B` // move cursor down\n          } else if (y < this.cursor.y) {\n            const diff = this.cursor.y - y\n            result += `${ESC}[${diff > 1 ? diff : ''}A` // move cursor up\n          } else if (x > this.cursor.x) {\n            if (includesEmoji || includesIcon) {\n              result += `${ESC}[G${ESC}[${x > 1 ? x : ''}C` // move cursor to column, move cursor right\n            } else {\n              const diff = x - this.cursor.x\n              result += `${ESC}[${diff > 1 ? diff : ''}C` // move cursor right\n            }\n          } else if (x < this.cursor.x) {\n            if (includesEmoji) {\n              result += `${ESC}[G${ESC}[${x > 1 ? x : ''}C` // move cursor to start, move cursor right\n            } else {\n              const diff = this.cursor.x - x\n              result += `${ESC}[${diff > 1 ? diff : ''}D` // move cursor left\n            }\n          }\n        }\n        result += strWithModifiers\n\n        this.cursor = { x: x + str.length, y }\n      })\n      // if (this.cursor.x > buffer[y].length - 1) {\n      //   this.cursor = { x: 0, y: 0 }\n      //   result += `${ESC}[H` // moves cursor to home position\n      // }\n      if (this.cursor.x > this.maxCursor.x) this.maxCursor.x = this.cursor.x\n      if (this.cursor.y > this.maxCursor.y) this.maxCursor.y = this.cursor.y\n    }\n    this.prevBuffer = buffer\n\n    if (this.nextWritePrefix) {\n      result = this.nextWritePrefix + result\n      this.nextWritePrefix = ''\n    }\n\n    if (this.result !== undefined || this.print) {\n      result += this.terminate()\n    }\n\n    if (result) {\n      if (this.print) return Renderer.terminate(result)\n\n      process.stdout.write(result)\n      // log(/* Date.now(), */ result)\n    }\n\n    if (this.result !== undefined) {\n      Renderer.terminate(this.result)\n    }\n  }\n}\n\nexport default Term\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"allowJs\": false,\n    \"checkJs\": false,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"jsx\": \"react-jsx\",\n    \"noEmit\": true\n  },\n  \"exclude\": [\"node_modules\", \".create\", \".dist\", \".npm\", \"create/template\"]\n}\n"
  },
  {
    "path": "utils/chunk.ts",
    "content": "export default function chunk(arr: any, size: number, cache: any[] = []) {\n  const tmp = [...arr]\n  while (tmp.length) cache.push(tmp.splice(0, size))\n  return cache\n}\n"
  },
  {
    "path": "utils/log.ts",
    "content": "import { createConnection } from 'net'\nimport { tmpdir } from 'os'\nimport { join } from 'path'\nimport { inspect } from 'util'\n\nlet socket: any\nlet once = false\n\nconst connect = () => {\n  return new Promise((resolve, reject) => {\n    socket = createConnection(join(tmpdir(), 'node-log.sock'))\n      .on('connect', function () {\n        socket.write('\\0')\n        resolve(socket)\n      })\n      .on('error', () => {\n        reject(undefined)\n      })\n  })\n}\n\nconst toString = (data: any[]) => data.map(i => inspect(i, undefined, null, true)).join(' ')\n\nexport default async function log(...rest: any) {\n  if (once && socket === undefined) return\n  try {\n    once = true\n    if (socket === undefined) socket = await connect()\n    socket.write(toString(rest) + '\\n')\n  } catch {\n    socket = undefined\n    // console.log(...rest)\n  }\n}\n"
  }
]