Full Code of infely/react-curse for AI

master f4dc11e3085e cached
79 files
122.1 KB
39.2k tokens
128 symbols
1 requests
Download .txt
Repository: infely/react-curse
Branch: master
Commit: f4dc11e3085e
Files: 79
Total size: 122.1 KB

Directory structure:
gitextract_vg983zb_/

├── .gitignore
├── bin/
│   ├── font.js
│   ├── logger.js
│   ├── postcreate.js
│   ├── postdist.js
│   └── postnpm.js
├── components/
│   ├── Banner.tsx
│   ├── Bar.tsx
│   ├── Block.tsx
│   ├── Canvas.tsx
│   ├── Frame.tsx
│   ├── Input.tsx
│   ├── List.tsx
│   ├── ListTable.tsx
│   ├── Scrollbar.tsx
│   ├── Separator.tsx
│   ├── Spinner.tsx
│   ├── Text.tsx
│   └── View.tsx
├── create/
│   ├── Create.tsx
│   ├── readme.md
│   └── template/
│       ├── App.tsx
│       ├── package.json
│       └── tsconfig.json
├── eslint.config.js
├── examples/
│   ├── Banner.tsx
│   ├── Canvas.tsx
│   ├── Chat.tsx
│   ├── Example.tsx
│   ├── Inline.tsx
│   ├── Pong.tsx
│   ├── Prompt.tsx
│   ├── Speed.tsx
│   ├── Todo.tsx
│   └── Visualizer.tsx
├── hooks/
│   ├── useAnimation.ts
│   ├── useBell.ts
│   ├── useChildrenSize.ts
│   ├── useClipboard.ts
│   ├── useExit.ts
│   ├── useInput.ts
│   ├── useMouse.ts
│   ├── useSize.ts
│   └── useWordWrap.ts
├── index.ts
├── input.ts
├── mediacreators/
│   ├── Banner.tsx
│   ├── Bar-1.tsx
│   ├── Bar-2.tsx
│   ├── Block.tsx
│   ├── Canvas-1.tsx
│   ├── Canvas-2.tsx
│   ├── Frame.tsx
│   ├── Input-1.tsx
│   ├── Input-2.tsx
│   ├── List.tsx
│   ├── ListTable.tsx
│   ├── Scrollbar.tsx
│   ├── Separator.tsx
│   ├── Spinner.tsx
│   ├── Text.tsx
│   ├── Trail.tsx
│   ├── View.tsx
│   ├── demo.tsx
│   ├── exampleAnimate.tsx
│   ├── exampleHello.tsx
│   ├── exampleInput.tsx
│   ├── logo.tsx
│   └── useAnimation.tsx
├── package.json
├── prettier.config.js
├── readme.md
├── reconciler.ts
├── renderer.ts
├── screen.ts
├── term.ts
├── tsconfig.json
└── utils/
    ├── chunk.ts
    └── log.ts

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

================================================
FILE: .gitignore
================================================
.DS_Store
.create/
.dist/
.npm/
node_modules/


================================================
FILE: bin/font.js
================================================
#!/usr/bin/env node

// prettier-ignore
const letters = [
  [ //  !
    0b00000100,
    0b00000100,
    0b00000100,
    0b00000000,
    0b00000100,
    0b00000000
  ], [ // "#
    0b10101010,
    0b10101110,
    0b00001010,
    0b00001110,
    0b00001010,
    0b00000000
  ], [ // $%
    0b11101010,
    0b10000010,
    0b11100100,
    0b00101000,
    0b11101010,
    0b01000000
  ], [ // &'
    0b01100100,
    0b10000100,
    0b01000000,
    0b10100000,
    0b11100000,
    0b00000000
  ], [ // ()
    0b00101000,
    0b01000100,
    0b01000100,
    0b01000100,
    0b00101000,
    0b00000000
  ], [ // *+
    0b00000000,
    0b01000100,
    0b11101110,
    0b01000100,
    0b10100000,
    0b00000000
  ], [ // ,-
    0b00000000,
    0b00000000,
    0b00001110,
    0b00000000,
    0b01000000,
    0b10000000
  ], [ // ./
    0b00000010,
    0b00000100,
    0b00000100,
    0b00000100,
    0b01001000,
    0b00000000
  ], [ // 01
    0b11100100,
    0b10101100,
    0b10100100,
    0b10100100,
    0b11101110,
    0b00000000
  ], [ // 23
    0b11101110,
    0b00100010,
    0b11101110,
    0b10000010,
    0b11101110,
    0b00000000
  ], [ // 45
    0b10101110,
    0b10101000,
    0b11101110,
    0b00100010,
    0b00101110,
    0b00000000
  ], [ // 67
    0b11101110,
    0b10000010,
    0b11100010,
    0b10100010,
    0b11100010,
    0b00000000
  ], [ // 89
    0b11101110,
    0b10101010,
    0b11101110,
    0b10100010,
    0b11101110,
    0b00000000
  ], [ // :;
    0b00000000,
    0b01000100,
    0b00000000,
    0b00000000,
    0b01000100,
    0b00001000
  ], [ // <=
    0b00100000,
    0b01001110,
    0b10000000,
    0b01001110,
    0b00100000,
    0b00000000
  ], [ // >?
    0b10001110,
    0b01000010,
    0b00100100,
    0b01000000,
    0b10000100,
    0b00000000
  ], [ // @A
    0b01001110,
    0b10101010,
    0b10001110,
    0b11101010,
    0b01001010,
    0b00000000
  ], [ // BC
    0b11101110,
    0b10101000,
    0b11001000,
    0b10101000,
    0b11101110,
    0b00000000
  ], [ // DE
    0b11001110,
    0b10101000,
    0b10101100,
    0b10101000,
    0b11001110,
    0b00000000
  ], [ // FG
    0b11101110,
    0b10001000,
    0b11001000,
    0b10001010,
    0b10001110,
    0b00000000
  ], [ // HI
    0b10101110,
    0b10100100,
    0b11100100,
    0b10100100,
    0b10101110,
    0b00000000
  ], [ // JK
    0b11101010,
    0b00101010,
    0b00101100,
    0b10101010,
    0b11101010,
    0b00000000
  ], [ // LM
    0b10001010,
    0b10001110,
    0b10001010,
    0b10001010,
    0b11101010,
    0b00000000
  ], [ // NO
    0b11001110,
    0b10101010,
    0b10101010,
    0b10101010,
    0b10101110,
    0b00000000
  ], [ // PQ
    0b11101110,
    0b10101010,
    0b11101010,
    0b10001010,
    0b10001110,
    0b00000010
  ], [ // RS
    0b11101110,
    0b10101000,
    0b11001110,
    0b10100010,
    0b10101110,
    0b00000000
  ], [ // TU
    0b11101010,
    0b01001010,
    0b01001010,
    0b01001010,
    0b01001110,
    0b00000000
  ], [ // VW
    0b10101010,
    0b10101010,
    0b10101010,
    0b10101110,
    0b01001010,
    0b00000000
  ], [ // XY
    0b10101010,
    0b10101010,
    0b01000100,
    0b10100100,
    0b10100100,
    0b00000000
  ], [ // Z[
    0b11100110,
    0b00100100,
    0b01000100,
    0b10000100,
    0b11100110,
    0b00000000
  ], [ // \]
    0b10001100,
    0b01000100,
    0b01000100,
    0b01000100,
    0b00101100,
    0b00000000
  ], [ // ^_
    0b01000000,
    0b10100000,
    0b00000000,
    0b00000000,
    0b00001110,
    0b00000000
  ], [ // `{
    0b10000110,
    0b01000100,
    0b00001000,
    0b00000100,
    0b00000110,
    0b00000000
  ], [ // |}
    0b01001100,
    0b01000100,
    0b01000010,
    0b01000100,
    0b01001100,
    0b00000000
  ], [ // ~
    0b01010000,
    0b10100000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000
  ]
]

console.log(Buffer.from(letters.flat()).toString('base64'))


================================================
FILE: bin/logger.js
================================================
#!/usr/bin/env node
import { rmSync } from 'node:fs'
import { createServer } from 'node:net'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import process from 'node:process'

const filename = join(tmpdir(), 'node-log.sock')

try {
  rmSync(filename)
} catch {
  //
}

createServer(stream => {
  stream.on('data', data => {
    data = data.toString()
    if (data === '\0') return process.stdout.write('\x1bc')

    process.stdout.write(data)
  })
}).listen(filename)


================================================
FILE: bin/postcreate.js
================================================
#!/usr/bin/env node
import { execSync } from 'node:child_process'
import { readFileSync, writeFileSync, chmodSync } from 'node:fs'

const makeJs = () => {
  const file = `.create/index.js`
  writeFileSync(file, `#!/usr/bin/env node\n${readFileSync(file, 'utf8')}`)
  chmodSync(file, '755')
}

const makeJson = () => {
  const json = JSON.parse(readFileSync('package.json', 'utf8'))

  const keys = ['name', 'version', 'description', 'keywords', 'author', 'repository', 'homepage', 'license']
  const jsonNew = {
    ...Object.fromEntries(Object.entries(json).filter(([key]) => keys.includes(key))),
    ...{
      name: 'create-react-curse',
      description: 'Create React-Curse app',
      keywords: [...json.keywords, 'react-curse'],
      bin: 'index.js'
    }
  }
  writeFileSync('.create/package.json', JSON.stringify(jsonNew, null, 2))
}

makeJs()
makeJson()
execSync('cp -r create/{template,README.md} .create')


================================================
FILE: bin/postdist.js
================================================
#!/usr/bin/env node
import { readFileSync, writeFileSync, chmodSync, unlinkSync } from 'node:fs'
import { argv } from 'node:process'

let [, , arg] = argv
if (arg === undefined) {
  const json = JSON.parse(readFileSync('package.json', 'utf8'))
  arg = json.main.replace(/\.[jt]sx?$/, '')
}

const src = '.dist/index.cjs'
const dest = `.dist/${arg}.cjs`
writeFileSync(dest, `#!/usr/bin/env node\n${readFileSync(src, 'utf8')}`)
unlinkSync(src)
chmodSync(dest, '755')


================================================
FILE: bin/postnpm.js
================================================
#!/usr/bin/env node
import { readFileSync, writeFileSync } from 'node:fs'

const makeJson = () => {
  const json = JSON.parse(readFileSync('package.json', 'utf8'))

  const keys = [
    'name',
    'version',
    'description',
    'keywords',
    'author',
    'repository',
    'homepage',
    'main',
    'license',
    'type',
    'dependencies'
  ]
  const jsonNew = {
    ...Object.fromEntries(Object.entries(json).filter(([key]) => keys.includes(key))),
    ...{
      main: 'index.js'
    }
  }
  writeFileSync('.npm/package.json', JSON.stringify(jsonNew, null, 2))
}

const makeReadme = () => {
  const data = readFileSync('README.md', 'utf8')

  const dataNew = data
    .split('\n')
    .map(i => i.replace('media/', 'https://raw.githubusercontent.com/infely/react-curse/HEAD/media/'))
    .join('\n')
  writeFileSync('.npm/README.md', dataNew)
}

makeJson()
makeReadme()


================================================
FILE: components/Banner.tsx
================================================
import chunk from '../utils/chunk'
import Text, { type TextProps } from './Text'
import { useMemo } from 'react'

const FONT =
  'BAQEAAQAqq4KDgoA6oLkKOpAZIRAoOAAKERERCgAAETuRKAAAAAOAECAAgQEBEgA5KykpO4A7iLugu4ArqjuIi4A7oLiouIA7qruou4AAEQAAEQIIE6ATiAAjkIkQIQATqqO6koA7qjIqO4AzqisqM4A7ojIio4ArqTkpK4A6iosquoAio6KiuoAzqqqqq4A7qrqio4C7qjOoq4A6kpKSk4AqqqqrkoAqqpEpKQA5iREhOYAjERERCwAQKAAAA4AhkQIBAYATERCREwAUKAAAAAA'

const letters = chunk(Buffer.from(FONT, 'base64'), 6)

const Letter = ({ children }: { children: string }) => {
  const text = useMemo(() => {
    let code = children.toUpperCase().charCodeAt(0)
    if (code >= 123 && code <= 126) code -= 26
    const font = letters[Math.floor((code - 32) / 2)]
    if (!font) return

    const bits = code % 2 === 0 ? 4 : 0
    return chunk(font, 2)
      .map(([top, bot]) => {
        return [3, 2, 1, 0]
          .map(i => {
            const b = Math.pow(2, i + bits)
            const code = 0 | (top & b && 0x04) | (bot & b && 0x08)
            return code ? String.fromCharCode(0x257c + code) : ' '
          })
          .join('')
      })
      .join('\n')
  }, [children])

  return <>{text}</>
}

export interface BannerProps extends TextProps {
  children: string
}

export default function Banner({ children, ...props }: BannerProps) {
  if (children === undefined || children === null) return null

  const lines = children.toString().split('\n')
  const length = Math.max(...lines.map((i: string) => i.length))

  return (
    <Text {...props} height={lines.length * 3} width={length * 4}>
      {lines.map((line: string, key: number) => (
        <Text key={key} x={0} y={key * 3}>
          {line.split('').map((char: string, key: number) => (
            <Text key={key} x={key * 4} y={0}>
              <Letter>{char}</Letter>
            </Text>
          ))}
        </Text>
      ))}
    </Text>
  )
}


================================================
FILE: components/Bar.tsx
================================================
import Text, { TextProps } from './Text'

const getSize = (offset: number, size: number) => {
  offset = Math.round(offset * 8)
  size = Math.round(size * 8)
  if (offset < 0) {
    size += offset
    offset = 0
  }
  size = Math.max(0, size)

  return [offset, size]
}

const getSections = (offset: number, size: number) => [
  size >= 8 || ((offset + size) % 8 === 0 && size > 0 && size < 8),
  size >= 8,
  (size >= 8 || (offset % 8 === 0 && size < 8)) && (offset + size) % 8 !== 0
]

const Vertical = (y: number, height: number, props: object) => {
  const [offset, size] = getSize(y, height)
  const sections = getSections(offset, size)

  const char = (value: number) => {
    if (value > 7) return String.fromCharCode(0x2588)
    return String.fromCharCode(0x2588 - Math.min(8, value))
  }

  return (
    <Text {...props} y={Math.floor(offset / 8)}>
      {sections[0] && <Text block>{char(offset % 8)}</Text>}
      {sections[1] &&
        [...Array(Math.floor(((offset % 8) + size) / 8) - 1)].map((_, key) => (
          <Text key={key} block>
            {char(8)}
          </Text>
        ))}
      {sections[2] && <Text inverse>{char((offset + size) % 8)}</Text>}
    </Text>
  )
}

const Horizontal = (x: number, width: number, props: object) => {
  const [offset, size] = getSize(x, width)
  const sections = getSections(offset, size)

  const char = (value: number) => {
    if (value <= 0) return ' '
    return String.fromCharCode(0x2590 - Math.min(8, value))
  }

  return (
    <Text {...props} x={Math.floor(offset / 8)}>
      {sections[0] && <Text inverse>{char(offset % 8)}</Text>}
      {sections[1] && <Text>{char(8).repeat(Math.floor(((offset % 8) + size) / 8) - 1)}</Text>}
      {sections[2] && <Text>{char((offset + size) % 8)}</Text>}
    </Text>
  )
}

export interface BarProps extends TextProps {
  type: 'vertical' | 'horizontal'
  y?: number
  x?: number
  height?: number
  width?: number
}

export default function Bar({ type = 'vertical', y, x, height, width, ...props }: BarProps) {
  if (type === 'vertical') return Vertical(y || 0, height || 0, { x, width, ...props })
  if (type === 'horizontal') return Horizontal(x || 0, width || 0, { y, height, ...props })

  return null
}


================================================
FILE: components/Block.tsx
================================================
import useSize from '../hooks/useSize'
import Text, { TextProps } from './Text'

export interface BlockProps extends TextProps {
  width?: number | undefined
  align?: 'left' | 'center' | 'right'
  children: any
}

export default function Block({ width = undefined, align = 'left', children, ...props }: BlockProps) {
  const handle = (line: any, key: any = undefined) => {
    if (typeof line === 'object') return line
    if (line === '\n') return

    let x: number | string = 0
    switch (align) {
      case 'center':
        width ??= useSize().width
        x = Math.round(width / 2 - line.length / 2)
        break
      case 'right':
        x = `100%-${line.length}`
        break
    }
    return (
      <Text key={key} x={x} {...props} block>
        {line}
      </Text>
    )
  }

  if (Array.isArray(children)) return children.map(handle)
  return handle(children)
}


================================================
FILE: components/Canvas.tsx
================================================
import { Color } from '../screen'
import chunk from '../utils/chunk'
import Text, { TextProps } from './Text'
import { Children, useEffect, useMemo, useRef } from 'react'

class CanvasClass {
  // prettier-ignore
  MODES = {
    '1x1': { map: [[0x1]], table: [0x20, 0x88] },
    '1x2': { map: [[0x1], [0x2]], table: [0x20, 0x80, 0x84, 0x88] },
    '2x2': { map: [[0x1, 0x4], [0x2, 0x8]], table: [0x20, 0x98, 0x96, 0x8c, 0x9d, 0x80, 0x9e, 0x9b, 0x97, 0x9a, 0x84, 0x99, 0x90, 0x9c, 0x9f, 0x88] },
    '2x4': { map: [[0x1, 0x8], [0x2, 0x10], [0x4, 0x20], [0x40, 0x80]] }
  }

  mode: { w: number; h: number }
  multicolor: boolean
  w: number
  h: number
  buffer: Buffer
  colors: Color[]

  constructor(width: number, height: number, mode = { w: 1, h: 2 }) {
    this.mode = mode
    this.multicolor = mode.w === 1 && mode.h === 2
    this.w = Math.ceil(width / this.mode.w) * this.mode.w
    this.h = Math.ceil(height / this.mode.h) * this.mode.h

    const size = ((this.w / this.mode.w) * this.h) / this.mode.h
    this.buffer = Buffer.alloc(size)
    this.colors = [...Array(size * (this.multicolor ? 2 : 1))]
  }

  clear() {
    this.buffer.fill(0)
    this.colors.fill(0)
  }

  set(x: number, y: number, color: Color) {
    if (x < 0 || x >= this.w || y < 0 || y >= this.h) return
    const index = (this.w / this.mode.w) * Math.floor(y / this.mode.h) + Math.floor(x / this.mode.w)
    this.buffer[index] |= (this.MODES as any)[`${this.mode.w}x${this.mode.h}`].map[y % this.mode.h][x % this.mode.w]

    if (color) this.colors[this.multicolor ? this.w * y + x : index] = color
  }

  line(x0: number, y0: number, x1: number, y1: number, color: Color) {
    const dx = x1 - x0
    const dy = y1 - y0
    const adx = Math.abs(dx)
    const ady = Math.abs(dy)
    let eps = 0
    const sx = dx > 0 ? 1 : -1
    const sy = dy > 0 ? 1 : -1
    if (adx > ady) {
      for (let x = x0, y = y0; sx < 0 ? x >= x1 : x <= x1; x += sx) {
        this.set(x, y, color)
        eps += ady
        if (eps << 1 >= adx) {
          y += sy
          eps -= adx
        }
      }
    } else {
      for (let x = x0, y = y0; sy < 0 ? y >= y1 : y <= y1; y += sy) {
        this.set(x, y, color)
        eps += adx
        if (eps << 1 >= ady) {
          x += sx
          eps -= ady
        }
      }
    }
  }

  render() {
    return [...this.buffer].map((i, index) => {
      const table = (this.MODES as any)[`${this.mode.w}x${this.mode.h}`].table
      let res = String.fromCharCode(table ? (i && 0x2500) + table[i] : 0x2800 + i)

      let colors: Color[] = []
      if (res !== ' ') {
        if (this.multicolor) {
          const y = Math.floor(index / this.w) * this.mode.h
          const x = (index % this.w) * this.mode.w
          const color1 = this.colors[this.w * y + x]
          const color2 = this.colors[this.w * (y + 1) + x]
          if (res === '\u2588' && color1 !== color2) {
            res = '\u2580'
            colors = [color1, color2]
          } else {
            colors = [color1 || color2]
          }
        } else {
          colors = [this.colors[index]]
        }
      }

      return [res, colors]
    })
  }
}

interface Point {
  x: number
  y: number
  color?: Color
}

export const Point = (_props: Point) => <></>

interface Line {
  x: number
  y: number
  dx: number
  dy: number
  color?: Color
}

export const Line = (_props: Line) => <></>

interface CanvasProps extends TextProps {
  mode?: { w: number; h: number }
  width: number
  height: number
  children: any[]
}

export default function Canvas({ mode = { w: 1, h: 2 }, width, height, children, ...props }: CanvasProps) {
  const canvas = useRef(new CanvasClass(width, height, mode))

  useEffect(() => {
    canvas.current = new CanvasClass(width, height, mode)
  }, [width, height, mode])

  const text = useMemo(() => {
    canvas.current.clear()

    Children.forEach(children, i => {
      if (i.type === Point) {
        const { x, y, color } = i.props
        canvas.current.set(x, y, color)
      } else if (i.type === Line) {
        const { x, y, dx, dy, color } = i.props
        canvas.current.line(x, y, dx, dy, color)
      }
    })

    return canvas.current.render()
  }, [children])

  return (
    <Text {...props}>
      {chunk(text, canvas.current.w / canvas.current.mode.w).map((line: any, y) => (
        <Text key={y} x={0} y={y}>
          {line.map(
            ([char, [color, background]]: any, x: number) =>
              char !== ' ' && (
                <Text
                  key={x}
                  x={x}
                  y={0}
                  color={color ? color : undefined}
                  background={background ? background : undefined}
                >
                  {char}
                </Text>
              )
          )}
        </Text>
      ))}
    </Text>
  )
}


================================================
FILE: components/Frame.tsx
================================================
import useChildrenSize from '../hooks/useChildrenSize'
import Text, { TextProps } from './Text'

const FRAMES = {
  single: '┌─┐│└┘',
  double: '╔═╗║╚╝',
  rounded: '╭─╮│╰╯'
} as const

export interface FrameProps extends TextProps {
  type?: 'single' | 'double' | 'rounded'
  height?: number
  width?: number
  children: React.ReactNode
}

export default function Frame({ type = 'single', height: _height, width: _width, children, ...props }: FrameProps) {
  const frames = FRAMES[type]

  const size = _height === undefined || _width === undefined ? useChildrenSize(children) : undefined
  const height = _height ?? size!.height
  const width = _width ?? size!.width

  const { color } = props

  return (
    <Text {...props}>
      <Text color={color} block>
        {frames[0]}
        {frames[1].repeat(width)}
        {frames[2]}
      </Text>
      {[...Array(height)].map((_, key) => (
        <Text key={key} block>
          <Text color={color}>{frames[3]}</Text>
          {' '.repeat(width)}
          <Text color={color}>{frames[3]}</Text>
        </Text>
      ))}
      <Text y={1} x={1} block>
        {children}
      </Text>
      <Text y={height + 1} color={color}>
        {frames[4]}
        {frames[1].repeat(width)}
        {frames[5]}
      </Text>
    </Text>
  )
}


================================================
FILE: components/Input.tsx
================================================
import useInput from '../hooks/useInput'
import Renderer from '../renderer'
import { Color } from '../screen'
import Text, { TextProps } from './Text'
import { useEffect, useMemo, useRef, useState } from 'react'

const mutate = (value: string, pos: number, str: string, multiline: boolean): [string, number, string | null] => {
  const edit = (value: string, pos: number, callback: (string: string) => string) => {
    const left = callback(value.substring(0, pos))
    const right = value.substring(pos)
    return [left, right].join('')
  }

  const arr = Renderer.input.parse(str) // str.split('')
  for (const input of arr) {
    switch (input) {
      case '\x01': // C-a
      case '\x1b\x5b\x31\x7e': {
        // home
        if (pos > 0) pos = 0
        break
      }
      case '\x05': // C-e
      case '\x1b\x5b\x34\x7e': {
        // end
        if (pos < value.length) pos = value.length
        break
      }
      case '\x02': // C-b
      case '\x1b\x5b\x44': {
        // left
        if (pos > 0) pos -= 1
        break
      }
      case '\x06': // C-f
      case '\x1b\x5b\x43': {
        // right
        if (pos < value.length) pos += 1
        break
      }
      case '\x1b': {
        // esc
        return [value, pos, 'cancel']
      }
      case '\x04': // C-d
      case '\x0d': {
        // cr
        if (input === '\x0d' && multiline) {
          value = edit(value, pos, i => i + '\n')
          pos += 1
          break
        }
        return [value, pos, 'submit']
      }
      case '\x08': // C-h
      case '\x7f': {
        // backspace
        if (pos < 1) break
        value = edit(value, pos, i => i.substring(0, i.length - 1))
        pos -= 1
        break
      }
      case '\x15': {
        // C-u
        if (pos < 1) break
        value = edit(value, pos, () => '')
        pos = 0
        break
      }
      case '\x0b': {
        // C-k
        if (pos > value.length - 1) break
        value = value.substring(0, pos)
        break
      }
      case '\x1b\x62': // M-b
      case '\x17': {
        // C-w
        if (pos < 1) break
        const index = value.substring(0, pos).trimEnd().lastIndexOf(' ')
        if (input === '\x17') value = edit(value, pos, i => (index !== -1 ? i.substring(0, index + 1) : ''))
        pos = Math.max(0, index + 1)
        break
      }
      case '\x1b\x66': {
        // M-f
        if (pos > value.length - 1) break
        const nextWordIndex = value.substring(pos).match(/\s(\w)/)?.index ?? -1
        pos = nextWordIndex === -1 ? value.length : pos + nextWordIndex + 1
        break
      }
      case '\x1b\x64': {
        // M-d
        const nextEndIndex = value.substring(pos).match(/\w(\b)/)?.index ?? -1
        value = value.substring(0, pos) + (nextEndIndex !== -1 ? value.substring(pos + nextEndIndex + 1) : '')
        break
      }
      case '\x1b\x5b\x41': {
        // up
        if (!multiline) break

        const currentLine = value.substring(0, pos).lastIndexOf('\n')
        if (currentLine === -1) break

        const targetLine = value.substring(0, currentLine).lastIndexOf('\n')
        pos = targetLine + Math.min(pos - currentLine, currentLine - targetLine)
        break
      }
      case '\x1b\x5b\x42': {
        // down
        if (!multiline) break

        let targetLine_ = value.substring(pos).indexOf('\n')
        if (targetLine_ === -1) break

        targetLine_ += pos + 1
        let nextLine = value.substring(targetLine_).indexOf('\n')
        nextLine = (nextLine !== -1 ? targetLine_ + nextLine : value.length) + 1
        const currentLine_ = value.substring(0, pos).lastIndexOf('\n')
        pos = targetLine_ + Math.min(pos - currentLine_ - 1, nextLine - targetLine_ - 1)
        break
      }
      default: {
        if (input.charCodeAt(0) < 32) break
        value = edit(value, pos, i => i + input)
        pos += 1
      }
    }
  }
  return [value, pos, null]
}

interface InputProps extends TextProps {
  focus?: boolean
  type?: 'text' | 'password' | 'hidden'
  initialValue?: string
  cursorBackground?: Color
  onCancel?: () => void
  onChange?: (_: any) => void
  onSubmit?: (_: any) => void
}

export default function Input({
  focus = true,
  type = 'text',
  initialValue = '',
  cursorBackground = undefined,
  onCancel = () => {},
  onChange = (_: string) => {},
  onSubmit = (_: string) => {},
  width = undefined,
  height = undefined,
  ...props
}: InputProps) {
  const [value, setValue] = useState(initialValue)
  const [pos, setPos] = useState(initialValue.length)
  const offset = useRef({ y: 0, x: 0 })

  const multiline = useMemo(() => {
    return typeof height === 'number' && height > 1
  }, [height])

  useInput(
    (_, raw) => {
      if (raw === undefined) return
      if (!focus) return

      const [valueNew, posNew, action] = mutate(value, pos, raw(), multiline)
      switch (action) {
        case 'cancel':
          onCancel()
          break
        case 'submit':
          onSubmit(valueNew)
          setValue('')
          setPos(0)
          break
        default:
          setValue(valueNew)
          setPos(posNew)
      }
    },
    [focus, value, pos, onCancel, onSubmit]
  )

  useEffect(() => {
    onChange(value)
  }, [value])

  if (type === 'hidden') return null

  const text = useMemo(() => {
    if (type === 'password') return '*'.repeat(value.length)
    return value
  }, [value, type])

  const { y: yo, x: xo } = useMemo(() => {
    if (typeof width !== 'number') return offset.current

    let posLine = pos
    let valueLine = value
    if (multiline && typeof height === 'number') {
      const line = value.substring(0, pos).split('\n').length - 1
      if (offset.current.y < line - height + 1) offset.current.y = line - height + 1
      if (offset.current.y > line) offset.current.y = line

      const currentLine = value.substring(0, pos).lastIndexOf('\n')
      posLine = pos - (currentLine !== -1 ? currentLine + 1 : 0)
      const nextLine = value.substring(pos).indexOf('\n')
      valueLine = value.substring(currentLine + 1, nextLine !== -1 ? pos + nextLine : value.length)
    }

    if (!multiline && offset.current.x + valueLine.length + 1 > width)
      offset.current.x = Math.max(0, valueLine.length - width + 1)
    if (offset.current.x < posLine - width + 1) offset.current.x = posLine - width + 1
    if (offset.current.x > posLine) offset.current.x = posLine

    return offset.current
  }, [value, pos, width])

  return (
    <Text height={height} width={width} {...props}>
      <Text y={-yo} x={-xo}>
        {text.substring(0, pos)}
        {focus && (
          <>
            <Text inverse={cursorBackground === undefined} background={cursorBackground}>
              {(text[pos] !== '\n' && text[pos]) || ' '}
            </Text>
            {text[pos] === '\n' && '\n'}
          </>
        )}
        {text.length > pos && text.substring(pos + (focus ? 1 : 0))}
      </Text>
    </Text>
  )
}


================================================
FILE: components/List.tsx
================================================
import useInput from '../hooks/useInput'
import useSize from '../hooks/useSize'
import { Color } from '../screen'
import Scrollbar from './Scrollbar'
import Text from './Text'
import { useEffect, useMemo, useState } from 'react'

export const getYO = (offset: number, limit: number, y: number) => {
  if (offset <= y - limit) return y - limit + 1
  if (offset > y) return y
  return offset
}

export const inputHandler =
  (
    vi: boolean,
    pos: ListPos,
    setPos: (_: any) => void,
    height: number,
    dataLength: number,
    onChange: (_: any) => void
  ) =>
  (input: string) => {
    let y: undefined | number
    let yo: undefined | number
    if (((vi && input === 'k') || input === '\x1b\x5b\x41') /* up */ && pos.y > 0) y = pos.y - 1
    if (((vi && input === 'j') || input === '\x1b\x5b\x42') /* down */ && pos.y < dataLength - 1) y = pos.y + 1
    if (((vi && input === '\x02') /* C-b */ || input === '\x1b\x5b\x35\x7e') /* pageup */ && pos.y > 0)
      y = Math.max(0, pos.y - height)
    if (((vi && input === '\x06') /* C-f */ || input === '\x1b\x5b\x36\x7e') /* pagedown */ && pos.y < dataLength - 1)
      y = Math.min(dataLength - 1, pos.y + height)
    if (vi && input === '\x15' /* C-u */ && pos.y > 0) y = Math.max(0, pos.y - Math.floor(height / 2))
    if (vi && input === '\x04' /* C-d */ && pos.y < dataLength - 1)
      y = Math.min(dataLength - 1, pos.y + Math.floor(height / 2))
    if (((vi && input === 'g') || input === '\x1b\x5b\x31\x7e') /* home */ && pos.y > 0) y = 0
    if (((vi && input === 'G') || input === '\x1b\x5b\x34\x7e') /* end */ && pos.y < dataLength - 1) y = dataLength - 1
    if (y !== undefined) yo = getYO(pos.yo, height, y)

    if (vi && input === 'H') y = pos.yo
    if (vi && input === 'M') y = pos.yo + Math.floor(height / 2)
    if (vi && input === 'L') y = pos.yo + height - 1

    if (y !== undefined) {
      let newPos = { ...pos, y }
      if (yo !== undefined) newPos = { ...newPos, yo }
      setPos(newPos)
      onChange(newPos)
    }
  }

export interface ListPos {
  y: number
  x: number
  yo: number
  xo: number
  x1: number
  x2: number
  xm?: number
}

export interface ListBase {
  focus?: boolean
  initialPos?: ListPos
  height?: number
  width?: number
  renderItem?: (_: any) => any
  scrollbar?: boolean
  scrollbarBackground?: Color
  scrollbarColor?: Color
  vi?: boolean
  pass?: any
  onChange?: (pos: ListPos) => void
  onSubmit?: (pos: ListPos) => void
}

interface ListProps extends ListBase {
  data?: any[]
}

export default function List({
  focus = true,
  initialPos = { y: 0, x: 0, yo: 0, xo: 0, x1: 0, x2: 0 },
  data = [''],
  renderItem = (_: any) => <Text></Text>,
  height: _height = undefined,
  width: _width = undefined,
  scrollbar = undefined,
  scrollbarBackground = undefined,
  scrollbarColor = undefined,
  vi = true,
  pass = undefined,
  onChange = (_: ListPos) => {},
  onSubmit = (_: ListPos) => {}
}: ListProps) {
  const size = _height === undefined || _width === undefined ? useSize() : undefined
  const height = _height ?? size!.height
  const width = _width ?? size!.width

  const [pos, setPos] = useState<ListPos>({ ...{ y: 0, x: 0, yo: 0, xo: 0, x1: 0, x2: 0 }, ...initialPos })

  const isScrollbarRequired = useMemo(() => {
    return scrollbar === undefined ? data.length > height : scrollbar
  }, [scrollbar, data.length, height])

  useEffect(() => {
    let newPos: undefined | ListPos
    let { y } = initialPos
    if (y > 0 && y >= data.length) {
      y = data.length - 1
      onChange({ ...pos, y })
    }
    if (y !== pos.y) {
      y = Math.max(0, y)
      newPos = { ...(newPos || pos), y, yo: getYO(pos.yo, height - 1, y) }
    }
    if (newPos) {
      setPos(newPos)
      onChange(newPos)
    }
  }, [initialPos.y])

  useEffect(() => {
    if (pos.y > 0 && pos.y > data.length - 1) {
      const y = Math.max(0, data.length - 1)
      const newPos = { ...pos, y, yo: getYO(pos.yo, data.length, y) }
      setPos(newPos)
      onChange(newPos)
    }
  }, [data])

  useInput(
    (input: string) => {
      if (!focus) return

      inputHandler(vi, pos, setPos, height, data.length, onChange)(input)

      if (input === '\x0d' /* cr */) onSubmit(pos)
    },
    [focus, vi, pos, setPos, height, data, onChange, onSubmit]
  )

  return (
    <Text width={width} height={height}>
      {data
        .filter((_: any, index: number) => index >= pos.yo && index < height + pos.yo)
        .map((row: any, index: number) => (
          <Text key={index} height={1} block>
            {renderItem({
              focus,
              item: row,
              selected: index + pos.yo === pos.y,
              pass
            })}
          </Text>
        ))}
      {isScrollbarRequired && (
        <Text y={0} x="100%-1">
          <Scrollbar
            offset={pos.yo}
            limit={height}
            length={data.length}
            background={scrollbarBackground}
            color={scrollbarColor}
          />
        </Text>
      )}
    </Text>
  )
}


================================================
FILE: components/ListTable.tsx
================================================
import useInput from '../hooks/useInput'
import useSize from '../hooks/useSize'
import { getYO, inputHandler, ListBase, ListPos } from './List'
import Scrollbar from './Scrollbar'
import Text from './Text'
import { useEffect, useMemo, useState } from 'react'

const getX = (index: number, widths: number[]) => {
  const [x1, x2] = widths.reduce((acc, i, k) => [acc[0] + (k < index ? i : 0), acc[1] + (k <= index ? i : 0)], [0, 0])
  return { x1, x2 }
}

const getXO = (offsetX: number, limit: number, x1: number, x2: number) => {
  if (x1 <= offsetX) return x1
  if (x2 >= offsetX + limit) return x2 - limit + 1
  return offsetX
}

interface ListTableProps extends ListBase {
  mode?: 'cell' | 'row'
  head?: any[]
  renderHead?: (_: any) => any
  data?: any[][]
}

export default function List({
  mode = 'cell',
  focus = true,
  initialPos = { y: 0, x: 0, yo: 0, xo: 0, x1: 0, x2: 0 },
  height: _height = undefined,
  width: _width = undefined,
  head = [''],
  renderHead = (_: any) => <Text></Text>,
  data = [['']],
  renderItem = (_: any) => <Text></Text>,
  scrollbar = undefined,
  scrollbarBackground = undefined,
  scrollbarColor = undefined,
  vi = true,
  pass = undefined,
  onChange = (_pos: ListPos) => {},
  onSubmit = (_pos: ListPos) => {}
}: ListTableProps) {
  const size = _height === undefined || _width === undefined ? useSize() : undefined
  const height = _height ?? size!.height
  const width = _width ?? size!.width

  const [pos, setPos] = useState<ListPos>({ ...{ y: 0, x: 0, yo: 0, xo: 0, x1: 0, x2: 0 }, ...initialPos })

  const isScrollbarRequired = useMemo(() => {
    return scrollbar === undefined ? data.length > height - 1 : scrollbar
  }, [scrollbar, data.length, height])

  const widths = useMemo(() => {
    const widths = data
      .reduce(
        (acc: number[], row: string[]) => {
          row.forEach((i, k) => (acc[k] = Math.max(acc[k], (i?.toString() || 'null').length)))
          return acc
        },
        head.map((i: any) => i.toString().length)
      )
      .map((i, index) => i + (index <= head.length - 2 ? 2 : 0))

    const sum = widths.reduce((acc, i) => acc + i, 0)
    if (sum >= width - 1) return widths.map(i => Math.min(32, i))

    // const left = width - sum - 2
    // if (sum > 0) widths[widths.length - 1] += left

    return widths
  }, [data, head, width])

  const isCropped = useMemo(() => {
    const sum = widths.reduce((acc, i) => acc + i, 0)
    return sum - pos.xo >= width + (isScrollbarRequired ? -1 : 0)
  }, [widths, pos.xo, width, isScrollbarRequired])

  const dataFiltered = useMemo(() => {
    return data.filter((_: any, index: number) => index >= pos.yo && index < height + pos.yo)
  }, [data, pos.yo, height])

  useEffect(() => {
    let newPos: undefined | ListPos
    let { y, x } = initialPos
    if (y > 0 && y >= data.length) {
      y = data.length - 1
      onChange({ ...pos, y })
    }
    if (y !== pos.y) {
      y = Math.max(0, y)
      newPos = { ...(newPos || pos), y, yo: getYO(pos.yo, height - 1, y) }
    }
    if (initialPos.xm) {
      let acc = 0
      x = widths.map(i => (acc += i)).findIndex(i => i >= Math.min(acc, (initialPos.xm ?? 0) + pos.xo))
    }
    if (x > 0 && x >= head.length) {
      x = head.length - 1
      onChange({ ...pos, x })
    }
    if (x !== pos.x) {
      const { x1, x2 } = getX(x, widths)
      newPos = { ...(newPos || pos), x, xo: getXO(pos.xo, width + (isScrollbarRequired ? -1 : 0), x1, x2) }
    }
    if (newPos) {
      setPos(newPos)
      onChange(newPos)
    }
  }, [initialPos.y, initialPos.x, initialPos.xm])

  useEffect(() => {
    if (pos.y > 0 && head.length > 0 && pos.y > data.length - 1) {
      const y = Math.max(0, data.length - 1)
      const newPos = { ...pos, y, yo: getYO(pos.yo, data.length, y) }
      setPos(newPos)
      onChange(newPos)
    }
    if (pos.x > 0 && head.length > 0 && pos.x > head.length - 1) {
      const newPos = { ...pos, x: head.length - 1 }
      setPos(newPos)
      onChange(newPos)
    }
  }, [head, data])

  useInput(
    (input: string) => {
      if (!focus) return

      inputHandler(vi, pos, setPos, height - 1, data.length, onChange)(input)

      let x: undefined | number
      switch (mode) {
        case 'cell':
          if (((vi && input === 'h') || input === '\x1b\x5b\x44') /* left */ && pos.x > 0) x = pos.x - 1
          if (((vi && input === 'l') || input === '\x1b\x5b\x43') /* right */ && pos.x < head.length - 1) x = pos.x + 1
          if (vi && input === '^' && pos.x > 0) x = 0
          if (vi && input === '$' && pos.x < head.length - 1) x = head.length - 1
          if (x !== undefined) {
            const { x1, x2 } = getX(x, widths)
            const xo = getXO(pos.xo, width + (isScrollbarRequired ? -1 : 0), x1, x2)
            const newPos = { ...pos, x, xo, x1, x2 }
            setPos(newPos)
            onChange(newPos)
          }
          break
        case 'row':
          if (((vi && input === 'h') || input === '\x1b\x5b\x44') /* left */ && pos.x > 0) x = pos.x - 1
          if (((vi && input === 'l') || input === '\x1b\x5b\x43') /* right */ && pos.x < head.length - 1) x = pos.x + 1
          if (x !== undefined) {
            const { x1, x2 } = getX(x, widths)
            const newPos = { ...pos, x, xo: x1, x1, x2 }
            setPos(newPos)
            onChange(newPos)
          }
          break
      }

      if (input === '\x0d' /* cr */) onSubmit({ ...pos, ...getX(pos.x, widths) })
    },
    [focus, vi, pos, width, height, head, data, widths, isScrollbarRequired, setPos, onChange, onSubmit]
  )

  return (
    <Text width={width} height={height}>
      <Text x={-pos.xo} height={1}>
        {renderHead({
          focus,
          item: head,
          widths,
          pass
        })}
      </Text>
      <Text y={1} x={-pos.xo}>
        {dataFiltered.map((item: any, index: number) => (
          <Text key={index} height={1} block>
            {renderItem({
              mode,
              focus,
              item,
              y: pos.y,
              x: pos.x,
              widths,
              index: index + pos.yo,
              pass
            })}
          </Text>
        ))}
      </Text>
      {isCropped && (
        <Text y={0} x="100%-1" dim>
          ~
        </Text>
      )}
      {isScrollbarRequired && (
        <Text y={1} x="100%-1">
          <Scrollbar
            offset={pos.yo}
            limit={height - 1}
            length={data.length}
            background={scrollbarBackground}
            color={scrollbarColor}
          />
        </Text>
      )}
    </Text>
  )
}


================================================
FILE: components/Scrollbar.tsx
================================================
import { Color } from '../screen'
import Bar from './Bar'
import Text from './Text'

export interface ScrollbarProps {
  type?: 'vertical' | 'horizontal'
  offset: number
  limit: number
  length: number
  background?: Color
  color?: Color
}

export default function Scrollbar({ type = 'vertical', offset, limit, length, background, color }: ScrollbarProps) {
  length ||= limit
  offset = (limit / length) * offset
  let size = limit / (length / limit)
  if (size < 1) {
    offset *= (length - limit / size) / (length - limit)
    size = 1
  }

  return (
    <Text background={background} height={type === 'vertical' ? limit : 1} width={type === 'horizontal' ? limit : 1}>
      <Bar
        type={type}
        y={type === 'vertical' ? offset : undefined}
        x={type === 'horizontal' ? offset : undefined}
        height={type === 'vertical' ? size : undefined}
        width={type === 'horizontal' ? size : undefined}
        color={color}
      />
    </Text>
  )
}


================================================
FILE: components/Separator.tsx
================================================
import useSize from '../hooks/useSize'
import Text, { TextProps } from './Text'

export interface SeparatorProps extends TextProps {
  type?: 'vertical' | 'horizontal'
  height?: number
  width?: number
}

export default function Separator({ type = 'vertical', height: _height, width: _width, ...props }: SeparatorProps) {
  const size = _height === undefined || _width === undefined ? useSize() : undefined
  const height = _height ?? size!.height
  const width = _width ?? size!.width

  if (type === 'vertical' && height < 1) return null
  if (type === 'horizontal' && width < 1) return null

  return (
    <Text height={type === 'horizontal' ? 1 : undefined} width={type === 'vertical' ? 1 : undefined} {...props}>
      {type === 'vertical' &&
        [...Array(height)].map((_, key) => (
          <Text key={key} block>
            │
          </Text>
        ))}
      {type === 'horizontal' && '─'.repeat(width)}
    </Text>
  )
}


================================================
FILE: components/Spinner.tsx
================================================
import useAnimation from '../hooks/useAnimation'
import Text, { TextProps } from './Text'

export interface SpinnerProps extends TextProps {
  children?: string
}

export default function Spinner({ children, ...props }: SpinnerProps) {
  const frames = children ? children.split('') : ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
  const { ms, interpolate } = useAnimation(Infinity)
  const frame = Math.floor(interpolate(0, frames.length, 0, 500, ms % 500))
  const color = 255 - Math.abs(Math.floor(interpolate(-16, 16, 0, 1500, ms % 1500)))

  return (
    <Text color={color} {...props}>
      {frames[frame]}
    </Text>
  )
}


================================================
FILE: components/Text.tsx
================================================
import { Modifier } from '../screen'

export interface TextProps extends Modifier {
  readonly absolute?: boolean
  readonly x?: number | string
  readonly y?: number | string
  readonly width?: number | string
  readonly height?: number | string
  readonly block?: boolean
  readonly children?: React.ReactNode
}

export default function Text({ children, ...props }: TextProps) {
  // @ts-ignore
  return <text {...props}>{children}</text>
}


================================================
FILE: components/View.tsx
================================================
import useChildrenSize from '../hooks/useChildrenSize'
import useInput from '../hooks/useInput'
import useSize from '../hooks/useSize'
import Scrollbar from './Scrollbar'
import Text, { TextProps } from './Text'
import { useState } from 'react'

export interface ViewProps extends TextProps {
  focus?: boolean
  height?: number
  scrollbar?: boolean
  vi?: boolean
}

export default function View({ focus = true, height: _height, scrollbar, vi = true, children, ...props }: ViewProps) {
  const height = _height ?? useSize().height
  const [yo, setYo] = useState(0)
  const { height: length } = useChildrenSize(children)

  useInput(
    (input: string) => {
      if (!focus) return

      if (((vi && input === 'k') || input === '\x1b\x5b\x41') /* up */ && yo > 0) setYo(yo - 1)
      if (((vi && input === 'j') || input === '\x1b\x5b\x42') /* down */ && yo < length - height) setYo(yo + 1)
      if (((vi && input === '\x02') /* C-b */ || input === '\x1b\x5b\x35\x7e') /* pageup */ && yo > 0)
        setYo(Math.max(0, yo - height))
      if (((vi && input === '\x06') /* C-f */ || input === '\x1b\x5b\x36\x7e') /* pagedown */ && yo < length - height)
        setYo(Math.min(length - height, yo + height))
      if (vi && input === '\x15' /* C-u */ && yo > 0) setYo(Math.max(0, yo - Math.floor(height / 2)))
      if (vi && input === '\x04' /* C-d */ && yo < length - height)
        setYo(Math.min(length - height, yo + Math.floor(height / 2)))
      if (((vi && input === 'g') || input === '\x1b\x5b\x31\x7e') /* home */ && yo > 0) setYo(0)
      if (((vi && input === 'G') || input === '\x1b\x5b\x34\x7e') /* end */ && yo < length - height)
        setYo(length - height)
    },
    [focus, yo, length, height]
  )

  const isScrollbarRequired = scrollbar === undefined ? length > height : scrollbar

  return (
    <Text height={height} {...props}>
      <Text y={-yo}>{children}</Text>
      {isScrollbarRequired && (
        <Text y={0} x="100%-1">
          <Scrollbar offset={yo} limit={height} length={length} />
        </Text>
      )}
    </Text>
  )
}


================================================
FILE: create/Create.tsx
================================================
import ReactCurse, { Input, Spinner, Text, useAnimation, useInput } from '..'
import { Logo } from '../mediacreators/logo'
import { exec } from 'child_process'
import { copyFileSync, existsSync, mkdirSync, readdirSync } from 'fs'
import { join } from 'path'
import { cwd } from 'process'
import { useState } from 'react'

const pwd = cwd()

const install = (value: string) => {
  value ??= ''
  // console.log({ value })
  // if (!value) return

  const dest = join(pwd, value)
  if (!existsSync(dest)) mkdirSync(dest)

  if (existsSync(join(dest, 'package.json'))) return
  const src = join(__dirname, 'template')
  const files = readdirSync(src)
  for (const file of files) {
    copyFileSync(join(src, file), join(dest, file))
  }

  const cp = exec(`cd ${dest} && npm i`)
  return new Promise(resolve => {
    cp.on('exit', () => {
      resolve(true)
    })
  })
}

// const Logo = ({ text }: { text: string }) => {
//   const { interpolate } = useAnimation(1000)
//   const w = Math.floor(interpolate(0, 22))
//
//   return (
//     <Text block>
//       <Text y={0} dim block>
//         {text.replace(/[^\s]/g, '#')}
//       </Text>
//       <Text y={0} color="BrightGreen">
//         {text.substring(0, w)}
//       </Text>
//     </Text>
//   )
// }

const App = () => {
  const [focus, setFocus] = useState(1)
  const [value, setValue] = useState(null)

  const onSubmit = async (value: string) => {
    setFocus(2)

    const res = await install(value)
    setFocus(res ? 3 : -1)

    setTimeout(ReactCurse.exit, 1000 / 60)
  }

  useInput()

  return (
    <>
      <Text block />
      {/* <Logo text="Welcome to ReactCurse!" /> */}
      <Logo block />
      <Text block />
      <Text block>
        {focus === 1 && <Text>? </Text>}
        {focus !== 1 && <Text color="Green">{'✔ '}</Text>}
        <Text dim>Where would you like to create your app</Text>
        <Text> {pwd}/</Text>
        {focus === 1 && <Input onChange={setValue} onSubmit={onSubmit} color="Green" />}
        {focus !== 1 && <Text>{value}</Text>}
      </Text>
      {focus >= 2 && (
        <Text block>
          {focus === 2 && (
            <>
              <Spinner /> Installing...
            </>
          )}
          {focus > 2 && (
            <>
              <Text color="Green">{'✔ '}</Text>
              <Text dim>Done</Text>
            </>
          )}
        </Text>
      )}
      {focus === 3 && (
        <>
          <Text block />
          {value && (
            <Text block>
              <Text dim>{'# '}</Text>cd {value}
            </Text>
          )}
          <Text block>
            <Text dim>{'# '}</Text>npm start
          </Text>
          <Text block />
          <Text color="Green">Enjoy!</Text>
        </>
      )}
      {focus === -1 && <Text color="Red">Canceled</Text>}
    </>
  )
}

ReactCurse.inline(<App />)


================================================
FILE: create/readme.md
================================================
# create-react-curse

Generate a [react-curse](https://www.npmjs.com/package/react-curse) app


================================================
FILE: create/template/App.tsx
================================================
import { useState } from 'react'
import ReactCurse, { Text, useInput } from 'react-curse'

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

  useInput(
    input => {
      if (input === 'q') ReactCurse.exit()
      else setCounter(counter + 1)
    },
    [counter]
  )

  return (
    <Text>
      <Text block>
        Counter: <Text color="Green">{counter.toString()}</Text>
      </Text>
      <Text dim block>
        Press q to exit or any key to increment the counter
      </Text>
      <Text>
        Edit <Text inverse>App.tsx</Text>
      </Text>
    </Text>
  )
}

ReactCurse.render(<App />)


================================================
FILE: create/template/package.json
================================================
{
  "name": "react-curse-app",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
  "scripts": {
    "start": "npx esbuild App.tsx --outfile=.dist/index.js --bundle --platform=node --format=esm --external:'./node_modules/*' --sourcemap && node --enable-source-maps .dist",
    "dist": "npx esbuild App.tsx --outfile=.dist/index.cjs --bundle --platform=node --define:'process.env.NODE_ENV=\"production\"' --minify --tree-shaking=true"
  },
  "dependencies": {
    "react-curse": "^1.0.0"
  },
  "devDependencies": {
    "@types/node": "^18.11.18",
    "@types/react": "^18.0.27"
  }
}


================================================
FILE: create/template/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ESNext",
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "allowJs": false,
    "checkJs": false,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "jsx": "react-jsx",
    "noEmit": true
  },
  "exclude": ["node_modules"]
}


================================================
FILE: eslint.config.js
================================================
import js from '@eslint/js'
import pluginReact from 'eslint-plugin-react'
import { defineConfig } from 'eslint/config'
import globals from 'globals'
import tseslint from 'typescript-eslint'

export default defineConfig([
  {
    files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
    plugins: { js },
    extends: ['js/recommended'],
    languageOptions: { globals: globals.browser }
  },
  tseslint.configs.recommended,
  pluginReact.configs.flat.recommended,
  {
    rules: {
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
      '@typescript-eslint/no-explicit-any': 'off',

      'react/react-in-jsx-scope': 'off'
    }
  }
])


================================================
FILE: examples/Banner.tsx
================================================
import ReactCurse, { Text, useInput } from '..'
import Banner from '../components/Banner'
import { useEffect, useState } from 'react'

const lorem = [...Array(3)]
  .map((_, offset) => [...Array(32)].map((_, index) => String.fromCharCode((offset + 1) * 32 + index)).join(''))
  .join('\n')

const getTime = () => new Date().toTimeString().substring(0, 8)

const Clock = (props: any) => {
  const [time, setTime] = useState(getTime())

  useEffect(() => {
    const interval = setInterval(() => setTime(getTime()), 1000)

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

  useInput((input: string) => {
    if (input === 'q') ReactCurse.exit()
  })

  return <Banner {...props}>{time}</Banner>
}

ReactCurse.render(
  <>
    <Clock color="Red" x="50%-15" block />
    <Text>
      <Text x={2} width={6} dim>
        0x20{'\n'.repeat(3)}0x40{'\n'.repeat(3)}0x60
      </Text>
      <Banner color="Blue">{lorem}</Banner>
    </Text>
  </>
)


================================================
FILE: examples/Canvas.tsx
================================================
import ReactCurse, { Text, useInput, useSize } from '..'
import Canvas, { Line } from '../components/Canvas'
import { useEffect, useMemo, useState } from 'react'

const CELL = 4
const COLORS = ['Red', 'Green', 'Blue']
const DATA = [...Array(COLORS.length)].map(() => {
  let prev = 0
  return [...Array(1024)].map(() => {
    prev += Math.round(Math.random() * 2 - 1)
    prev = Math.max(0, Math.min(4, prev))
    return prev
  })
})

const Graph = ({ mode, play }: any) => {
  const { width } = useSize()

  const h = useMemo(() => CELL * mode.h, [mode])
  const w = useMemo(() => CELL * mode.w, [mode])
  const c = useMemo(() => h * 4, [h])
  const [lines, setLines] = useState<any[]>([])
  const [offset, setOffset] = useState(0)

  useEffect(() => {
    const interval = setInterval(() => {
      if (!play) return

      setOffset(offset + 1)
    }, 1000 / 60)

    return () => clearInterval(interval)
  }, [offset, play])

  useEffect(() => {
    setLines(
      DATA.flatMap((line, colorIndex) => {
        return line
          .map((i, index) => {
            if (index - (offset * mode.w) / w > (width * mode.w) / w) return
            return {
              x: index * w - offset * mode.w,
              y: c - (line[index - 1] || 0) * h,
              dx: index * w + w - offset * mode.w,
              dy: c - i * h,
              color: COLORS[colorIndex]
            }
          })
          .filter(i => i)
      })
    )
  }, [mode, offset])

  return (
    <Canvas width={width * mode.w} height={c + 1} mode={mode}>
      {lines.map((props, key) => (
        <Line key={key} {...props} />
      ))}
    </Canvas>
  )
}

const App = () => {
  const [mode, setMode] = useState({ w: 1, h: 2 })
  const [play, setPlay] = useState(true)

  useInput((input: string) => {
    if (input === '\x10\x0d') ReactCurse.exit()
    if (input === 'q') ReactCurse.exit()

    if (input === '1') setMode({ w: 1, h: 1 })
    if (input === '2') setMode({ w: 1, h: 2 })
    if (input === '3') setMode({ w: 2, h: 2 })
    if (input === '4') setMode({ w: 2, h: 4 })

    if (input === ' ') setPlay(play => !play)
  })

  return (
    <>
      <Text x={3} block>
        {COLORS.map((color, key) => (
          <Text key={key} color={color}>
            Line {key + 1}{' '}
          </Text>
        ))}
      </Text>
      <Text>
        <Text width={3} dim>
          {[...Array(5)].map((_, key) => (
            <Text key={key} x={1} y={CELL * 4 - key * CELL}>
              {key.toString()}
            </Text>
          ))}
        </Text>
        <Graph mode={mode} play={play} />
      </Text>
    </>
  )
}

ReactCurse.render(<App />)


================================================
FILE: examples/Chat.tsx
================================================
import ReactCurse, { Banner, Input, Separator, Text, Trail, useAnimation, useWordWrap } from '..'
import { useState } from 'react'

const Message = ({ color: toColor, children }: { color: string; children: string }) => {
  const { interpolateColor } = useAnimation(1000)
  const color = interpolateColor('#888888', toColor)

  return <Text color={color}>{useWordWrap(children)}</Text>
}

const App = () => {
  const [messages, setMessages] = useState<{ role: 'system' | 'user'; message: string }[]>([
    { role: 'system', message: 'Hello' },
    { role: 'system', message: 'Type anything' },
    { role: 'system', message: 'And press enter' }
  ])

  const submitHandler = (message: string) => {
    if (message) setMessages([...messages, { role: 'user', message }])
  }

  return (
    <>
      <Banner block>CHAT</Banner>
      <Text height="100%-5" block>
        <Trail delay={250}>
          {messages.map((i, key) => (
            <Text key={key} color={i.role === 'system' ? 'Red' : 'Blue'} block>
              <Text>{`${i.role}:`.padEnd(7, ' ')}</Text>{' '}
              <Message color={i.role === 'system' ? '#e02020' : '#2020e0'}>{i.message}</Message>
            </Text>
          ))}
        </Trail>
      </Text>
      <Separator type="horizontal" dim block />
      <Text>
        <Text dim>#</Text> <Input initialValue="Hi there" width="100%" color="Blue" onSubmit={submitHandler} />
      </Text>
    </>
  )
}

ReactCurse.render(<App />)


================================================
FILE: examples/Example.tsx
================================================
import ReactCurse, { Text, useInput } from '..'
import Frame from '../components/Frame'
import { useState } from 'react'

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

  useInput(
    input => {
      if (input === 'q') ReactCurse.exit()

      if (input === 'k') setCounter(counter + 1)
      if (input === 'j') setCounter(counter - 1)
    },
    [counter]
  )

  return (
    <>
      <Frame type="rounded" block>
        <Text italic>Counter:</Text>{' '}
        <Text color="red" underline>
          {counter}
        </Text>
      </Frame>
      <Text dim>j,k - change counter, q - quit</Text>
    </>
  )
}

ReactCurse.render(<App />)


================================================
FILE: examples/Inline.tsx
================================================
import ReactCurse, { Text } from '..'

const App = () => {
  return (
    <>
      <Text block>Line 1</Text>
      <Text block>Line 2</Text>
      <Text block>Line 3</Text>
    </>
  )
}

ReactCurse.inline(<App />)


================================================
FILE: examples/Pong.tsx
================================================
import ReactCurse, { Banner, Canvas, Point, Line, useSize, useInput } from '..'
import { useEffect, useState } from 'react'

const Game = () => {
  const { width, height } = useSize()

  const [scores, setScores] = useState([0, 0])
  const [y, setY] = useState(height - 4)
  const [ball, setBall] = useState({ x: Math.floor(width / 2), y: height, dx: 1, dy: 1 })

  useEffect(() => {
    const interval = setInterval(() => {
      setBall(({ x, y, dx, dy }) => {
        x += dx
        y += dy
        if (x <= 1 || x >= width - 2) {
          dx = -dx
          scores[0]++
          setScores(scores)
        }
        if (y <= 0 || y >= height * 2 - 1) {
          dy = -dy
          scores[1]++
          setScores(scores)
        }
        return { x, y, dx, dy }
      })
    }, 1000 / 60)

    return () => clearInterval(interval)
  }, [scores])

  useInput((input: string) => {
    if (input === '\x10\x0d') ReactCurse.exit()
    if (input === 'q') ReactCurse.exit()

    if (input === 'k') setY(y => y - 1)
    if (input === 'j') setY(y => y + 1)
  })

  return (
    <>
      <Banner y={0} x="50%-8">
        {scores.join('   ')}
      </Banner>
      <Canvas y={0} x={0} width={width} height={height * 2}>
        <Line x={1} y={y} dx={1} dy={y + 4} />
        <Point x={ball.x} y={ball.y} />
        <Line x={width - 2} y={y} dx={width - 2} dy={y + 4} />
      </Canvas>
    </>
  )
}

ReactCurse.render(<Game />)


================================================
FILE: examples/Prompt.tsx
================================================
import ReactCurse, { Block, Frame, Input, Text, useAnimation, useInput } from '..'
import { useState } from 'react'

const InputText = ({ text, type, color }: { text: string; type: any; color: any }) => {
  const [focus, setFocus] = useState(true)
  const [value, setValue] = useState('')

  const onSubmit = (input: string) => {
    setFocus(false)
    setValue(input)
    ReactCurse.exit(input)
  }

  return (
    <>
      <Text color={color} underline>
        {text}
      </Text>
      <Text>: </Text>
      {focus && <Input type={type} onSubmit={onSubmit} color={color} />}
      {!focus && <Text bold>{value}</Text>}
    </>
  )
}

const InputList = ({ items, color }: { items: string[]; color: any }) => {
  const [, setFocus] = useState(true)
  const [selected, setSelected] = useState(0)

  useInput(
    (input: string) => {
      if (input === 'q') ReactCurse.exit()

      if (input === 'k') setSelected(i => Math.max(0, i - 1))
      if (input === 'j') setSelected(i => Math.min(items.length - 1, i + 1))
      if (input === '\x0d') {
        ReactCurse.exit(items[selected])
        setFocus(false)
      }
    },
    [selected]
  )

  return (
    <>
      <Text block>
        <Text color={color}>Please select an option</Text>:
      </Text>
      {items.map((i, key) => (
        <Text key={key} block>
          <Text color={color}>{selected === key ? '>' : ' '}</Text> {i}
        </Text>
      ))}
    </>
  )
}

const Spinner = ({ text }: { text: string }) => {
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] // ['|', '/', '-', '\\']

  const { ms, interpolate } = useAnimation(Infinity)
  const frame = Math.floor(interpolate(0, frames.length, 0, 500, ms % 500))
  const color = 255 - Math.abs(Math.floor(interpolate(-16, 16, 0, 1500, ms % 1500)))

  return (
    <>
      <Text color={color}>{frames[frame]}</Text> {text}
    </>
  )
}

;(async () => {
  await ReactCurse.frame(
    <Frame type="rounded" width={32} block color="Blue">
      <Block color="Yellow" width={32} align="center">
        hello world
      </Block>
    </Frame>
  )

  const res1 = await ReactCurse.prompt(<InputText text="Question 1" type="text" color="Red" />)
  console.log(`Answer 1: ${res1}`)

  const res2 = await ReactCurse.prompt(<InputText text="Question 2" type="password" color="Green" />)
  console.log(`Answer 2: ${res2}`)

  const res3 = await ReactCurse.prompt(<InputText text="Question 3" type="hidden" color="Blue" />)
  console.log(`Answer 3: ${res3}`)

  const res4 = await ReactCurse.prompt(<InputList items={['Item 1', 'Item 2', 'Item 3', 'Item 4']} color="Yellow" />)
  console.log(`Answer 3: ${res4}`)

  ReactCurse.inline(<Spinner text={JSON.stringify({ res1, res2, res3, res4 })} />)
  await new Promise(resolve => setTimeout(resolve, 500))

  process.exit()
})()


================================================
FILE: examples/Speed.tsx
================================================
import ReactCurse, { Text, useInput } from '..'
import { useEffect, useState } from 'react'

const TEXT = ''
const width = process.stdout.columns
const height = process.stdout.rows

const rand = () => {
  return [...Array(128)].map(() => [
    // 512
    width / 2,
    0,
    Math.floor(Math.random() * 256),
    Math.random() * 2 - 1, // 3 - 1.5
    Math.random() * 1 // 1.5
  ])
}

const App = () => {
  const [texts, setTexts] = useState(rand())

  useEffect(() => {
    const interval = setInterval(() => {
      setTexts(texts =>
        texts
          .map(([x, y, color, dx, dy]) => {
            x += dx
            y += dy
            if (x >= width - 1 - TEXT.length || x <= 0) dx = -dx
            if (y >= height - 1 || y <= 0) {
              dy = -dy
              if (dy < 0) dy *= 0.9
            }
            return [x, y, color, dx, dy]
          })
          .filter(i => Math.round(i[4] * 10))
      )
    }, 1000 / 60)

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

  useInput((input: string) => {
    if (input === 'q') ReactCurse.exit()
  })

  return (
    <>
      {texts.map(([x, y, color], key) => (
        <Text key={key} x={Math.round(x)} y={Math.round(y)} color={color}>
          {TEXT}
        </Text>
      ))}
    </>
  )
}

ReactCurse.render(<App />)


================================================
FILE: examples/Todo.tsx
================================================
import ReactCurse, { Block, Input, List, Text, useInput, useSize } from '..'
import useAnimation, { useTrail } from '../hooks/useAnimation'
import { useCallback, useState } from 'react'

const Task = ({ title, completed, selected }: { title: string; completed: boolean; selected: boolean }) => {
  const { interpolateColor } = useAnimation(250)

  return (
    <Text color={interpolateColor('#282828', selected ? '#b8bb26' : '#ebdbb2')} block>
      {completed ? '' : ''} {title}
    </Text>
  )
}

const Tasks = ({ focus, setFocus }: { focus: boolean; setFocus: (focus: boolean) => void }) => {
  const { height, width } = useSize()
  const [pos, setPos] = useState<{ y: number }>({ y: 0 })
  const [tasks, setTasks] = useState(() =>
    [...Array(32)].map((_, i) => ({ id: i + 1, title: `Task ${i + 1}`, completed: Math.random() >= 0.5 }))
  )

  useInput(
    input => {
      if (focus) return
      if (input === 'D') setTasks(tasks.filter((_, index) => index !== pos.y))
      if (input === ' ') setTasks(tasks.map((i, index) => (index === pos.y ? { ...i, completed: !i.completed } : i)))
      if (input === '\x0d') setFocus(true)
    },
    [pos, tasks]
  )

  const onSubmit = useCallback(
    (title: string) => {
      setFocus(false)
      setTasks(tasks => [...tasks, { id: tasks.length + 1, title, completed: false }])
    },
    [tasks]
  )

  return (
    <>
      <List
        height={height - 3}
        width={width}
        data={useTrail(1000 / 30, tasks, 'id')}
        renderItem={({ item, selected }) => {
          return <Task title={item.title} completed={item.completed} selected={selected} />
        }}
        onChange={setPos}
      />
      <Input absolute y="100%-1" x={0} focus={focus} onSubmit={onSubmit} onCancel={() => setFocus(false)} />
    </>
  )
}

const Fade = ({ children }: { children: React.ReactNode }) => {
  const { interpolateColor } = useAnimation(1000)

  const color = interpolateColor('#3c3836', '#ebdbb2', 500)
  if (color === '#3c3836') return null

  return (
    <Block align="center" color={color}>
      {children}
    </Block>
  )
}

const App = () => {
  const { width } = useSize()
  const [show, setShow] = useState(true)
  const [focus, setFocus] = useState(false)

  const { interpolate, interpolateColor } = useAnimation(500)
  const x = Math.round(interpolate(0, width))
  const background = interpolateColor('#282828', '#3c3836')

  useInput(
    input => {
      if (input === '\x10\x0d') ReactCurse.exit()
      if (focus) return

      if (input === 'q') ReactCurse.exit()
      if (input === 't') setShow(i => !i)
    },
    [focus]
  )

  return (
    <Text>
      <Text height={1} width={x} background={background}>
        <Fade>hello</Fade>
      </Text>
      <Text y={1} x={1}>
        {show && <Tasks focus={focus} setFocus={setFocus} />}
      </Text>
      <Text y="100%-2" x={width - x} height={1} background={background}>
        <Fade>world</Fade>
      </Text>
    </Text>
  )
}

ReactCurse.render(<App />)


================================================
FILE: examples/Visualizer.tsx
================================================
import ReactCurse, { Bar, Text, Trail, useAnimation, useSize } from '..'
import { useMemo } from 'react'

// const App = () => {
//   const { height, width } = useSize()
//
//   const { ms, interpolate } = useAnimation(2000)
//
//   const lines = useMemo(() => {
//     return [...Array(Math.floor(width / 2))].map(_ => ({
//       h: Math.floor(Math.random() * height),
//       t: Math.floor(Math.random() * 250) + 250,
//       c: 255 - Math.floor(Math.random() * 16)
//     }))
//   }, [])
//
//   return (
//     <>
//       {lines.map(({ h, t, c }, key) => {
//         h = ms < 1000 ? interpolate(0, h, 0, t, ms % t) : interpolate(h, 0, 1000, 2000)
//         return (
//           <Text y={0} x={key * 2} key={key}>
//             <Bar type="vertical" y={height - h} height={h} color={c} />
//           </Text>
//         )
//       })}
//     </>
//   )
// }

const Line = ({ h, c }: { h: number; c: number }) => {
  const { height } = useSize()
  const { interpolate } = useAnimation(500)

  h = interpolate(h, 0)

  return <Bar type="vertical" y={height - h} height={h} color={c} />
}

const App2 = () => {
  const { height, width } = useSize()

  const lines = useMemo(() => {
    return [...Array(Math.floor(width / 2))].map(_ => ({
      h: Math.floor(Math.random() * height),
      c: 255 - Math.floor(Math.random() * 16)
    }))
  }, [])

  return (
    <Trail delay={1000 / width}>
      {lines.map((props, key) => (
        <Text y={0} x={key * 2} key={key}>
          <Line {...props} />
        </Text>
      ))}
    </Trail>
  )
}

ReactCurse.render(<App2 />)


================================================
FILE: hooks/useAnimation.ts
================================================
import Renderer from '../renderer'
import { useState, useEffect, useRef } from 'react'

const interpolate = (toLow: number, toHigh: number, fromLow: number, fromHigh: number, value: number) => {
  const res = toLow + ((((toHigh - toLow) / 100) * 100) / (fromHigh - fromLow)) * (value - fromLow)
  return Math.max(Math.min(toLow, toHigh), Math.min(Math.max(toLow, toHigh), res))
}

const interpolateColor = (toLow: string, toHigh: string, fromLow: number, fromHigh: number, value: number) => {
  if (!toLow.startsWith('#')) return toHigh
  if (!toHigh.startsWith('#')) return toHigh

  const toLowColor = Renderer.term.parseHexColor(toLow)
  return (
    '#' +
    Buffer.from(
      Renderer.term.parseHexColor(toHigh).map((i: number, index: number) => {
        return Math.round(interpolate(toLowColor[index], i, fromLow, fromHigh, value))
      })
    ).toString('hex')
  )
}

export const Trail = ({ delay, children }: { delay: number; children: any }): React.JSX.Element => {
  return useTrail(delay, children)
}

export const useTrail = (delay: number, children: any[], key: string = 'key'): any => {
  const ms = useRef(0)
  const timeout = useRef<NodeJS.Timeout>(undefined)
  const [keys, setKeys] = useState<any[]>([])

  useEffect(() => {
    const keysNew = children.map((i: any) => i[key])
    const keyOld = keys.find(key => !keysNew.includes(key))
    if (keyOld) {
      setKeys(keys.filter(i => i !== keyOld))
      return
    }

    const keyNew = keysNew.find((i: any) => !keys.includes(i))
    if (!keyNew) return

    const at = Date.now()
    const nextAt = Math.max(0, delay - (at - ms.current))

    clearTimeout(timeout.current)
    timeout.current = setTimeout(() => {
      ms.current = Date.now()
      setKeys([...keys, keyNew])
    }, nextAt)
  }, [children.map((i: any) => i[key]).join('\n'), keys])

  return children.filter((i: any) => keys.includes(i[key]))
}

interface useAnimation {
  ms: number
  interpolate: (toLow: number, toHigh: number, fromLow?: number, fromHigh?: number, value?: number) => number
  interpolateColor: (toLow: string, toHigh: string, fromLow?: number, fromHigh?: number, value?: number) => string
}

export default (time = Infinity, fps = 60): useAnimation => {
  if (time <= 0 || fps <= 0) return { ms: 0, interpolate: i => i, interpolateColor: i => i }

  const at = useRef(Date.now())
  const interval = useRef<NodeJS.Timeout>(undefined)
  const [ms, setMs] = useState(0)

  useEffect(() => {
    const frameMs = 1000 / Math.min(60, fps)

    interval.current = setInterval(() => {
      const msNew = Date.now() - at.current
      if (msNew >= time) clearInterval(interval.current)
      setMs(Math.min(msNew, time))
    }, frameMs)

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

  return {
    ms,
    interpolate: (toLow, toHigh, fromLow = 0, fromHigh = time, value = ms) => {
      return interpolate(toLow, toHigh, fromLow, fromHigh, value)
    },
    interpolateColor: (toLow, toHigh, fromLow = 0, fromHigh = time, value = ms) => {
      return interpolateColor(toLow, toHigh, fromLow, fromHigh, value)
    }
  }
}


================================================
FILE: hooks/useBell.ts
================================================
/**
 * @deprecated
 */
export default () => {
  process.stdout.write('\x07')
}


================================================
FILE: hooks/useChildrenSize.ts
================================================
import { type ReactElement, useEffect, useState } from 'react'

const render = (element: ReactElement | ReactElement[] | any): any => {
  if (Array.isArray(element)) return element.map(i => render(i)).join('')

  const { children } = ((element as ReactElement).props as any) ?? { children: element }
  if (Array.isArray(children) || children?.props) return render(children)

  return children.toString()
}

const getSize = (children: ReactElement | ReactElement[] | any) => {
  const string = render(children).split('\n')
  const width = string.reduce((acc: number, i: string) => Math.max(acc, i.length), 0)
  const height = string.length

  return { width, height }
}

export default (children: ReactElement | ReactElement[] | any) => {
  const [size, setSize] = useState(getSize(children))

  useEffect(() => {
    setSize(getSize(children))
  }, [children])

  return size
}


================================================
FILE: hooks/useClipboard.ts
================================================
import { spawnSync } from 'child_process'

export default (): [() => string, (input: string) => string] => {
  const getClipboard = () => {
    switch (process.platform) {
      case 'darwin':
        return spawnSync('pbpaste', [], { encoding: 'utf8' }).stdout
    }

    return ''
  }

  const setClipboard = (input: any) => {
    if (typeof input !== 'string') input = input.toString()

    switch (process.platform) {
      case 'darwin':
        spawnSync('pbcopy', [], { input })
        break
      default:
        input = ''
    }

    return input
  }

  return [getClipboard, setClipboard]
}


================================================
FILE: hooks/useExit.ts
================================================
import Renderer from '../renderer'
import process from 'process'

/**
 * @deprecated
 */
export default (code: number | any = 0) => {
  if (typeof code === 'number') process.exit(code)

  Renderer.term.setResult(code)
}


================================================
FILE: hooks/useInput.ts
================================================
import Renderer from '../renderer'
import { useEffect } from 'react'

export default (callback: (input: string, raw: () => string) => void = () => {}, deps: React.DependencyList = []) => {
  useEffect(() => {
    if (!process.stdin.isRaw) process.stdin.setRawMode?.(true)
  }, [])

  useEffect(() => {
    const handler = (input: string, raw: () => string) => {
      if (input === '\x03') process.exit()
      if (input.startsWith('\x1b\x5b\x4d')) return

      callback(input, raw)
    }

    Renderer.input.on(handler)
    return () => {
      Renderer.input.off(handler)
    }
  }, deps)
}


================================================
FILE: hooks/useMouse.ts
================================================
import Renderer from '../renderer'
import { type DependencyList, useEffect } from 'react'

interface Event {
  type: 'mousedown' | 'mouseup' | 'wheeldown' | 'wheelup'
  x: number
  y: number
}

export default (callback: (event: Event) => void, deps: DependencyList = []) => {
  useEffect(() => {
    if (!process.stdin.isRaw) process.stdin.setRawMode?.(true)
    Renderer.term.enableMouse()
  }, [])

  useEffect(() => {
    const handler = (input: string) => {
      if (!input.startsWith('\x1b\x5b\x4d')) return

      const b = input.charCodeAt(3)
      const type = (1 << 6) & b ? (1 & b ? 'wheelup' : 'wheeldown') : (3 & b) === 3 ? 'mouseup' : 'mousedown'

      const x = input.charCodeAt(4) - 0o41
      const y = input.charCodeAt(5) - 0o41

      callback({ type, x, y })
    }

    Renderer.input.on(handler)
    return () => {
      Renderer.input.off(handler)
    }
  }, deps)
}


================================================
FILE: hooks/useSize.ts
================================================
import { useEffect, useState } from 'react'

const subscribers = new Set<(size: { width: number; height: number }) => void>()

const getSize = () => {
  const { columns: width, rows: height } = process.stdout
  return { width, height }
}

process.stdout.on('resize', () => {
  const size = getSize()
  subscribers.forEach((_, fn) => fn(size))
})

export default () => {
  const [size, setSize] = useState(getSize())

  useEffect(() => {
    subscribers.add(setSize)

    return () => {
      subscribers.delete(setSize)
    }
  }, [])

  return size
}


================================================
FILE: hooks/useWordWrap.ts
================================================
import useSize from './useSize'

export default (text: string, _width: number | undefined = undefined) => {
  const width = _width ?? useSize().width

  return text
    .split('\n')
    .map((line: string) => {
      if (line.length <= width) return line

      return line
        .split(' ')
        .reduce(
          (acc, i) => {
            if (acc[acc.length - 1].length + i.length > width) acc.push('')
            acc[acc.length - 1] += `${i} `
            return acc
          },
          ['']
        )
        .map(i => i.trimEnd())
        .join('\n')
    })
    .join('\n')
}


================================================
FILE: index.ts
================================================
export { default } from './renderer'
export { default as Banner } from './components/Banner'
export { default as Bar } from './components/Bar'
export { default as Block } from './components/Block'
export { default as Canvas, Point, Line } from './components/Canvas'
export { default as Frame } from './components/Frame'
export { default as Input } from './components/Input'
export { default as List, type ListPos } from './components/List'
export { default as ListTable } from './components/ListTable'
export { default as Scrollbar } from './components/Scrollbar'
export { default as Separator } from './components/Separator'
export { default as Spinner } from './components/Spinner'
export { default as Text } from './components/Text'
export { default as View } from './components/View'
export { default as useAnimation, useTrail, Trail } from './hooks/useAnimation'
export { default as useBell } from './hooks/useBell'
export { default as useChildrenSize } from './hooks/useChildrenSize'
export { default as useClipboard } from './hooks/useClipboard'
export { default as useExit } from './hooks/useExit'
export { default as useInput } from './hooks/useInput'
export { default as useMouse } from './hooks/useMouse'
export { default as useSize } from './hooks/useSize'
export { default as useWordWrap } from './hooks/useWordWrap'
export { default as log } from './utils/log'


================================================
FILE: input.ts
================================================
import EventEmitter from 'events'

export default class Input {
  ee: EventEmitter
  queue: string[] = []

  constructor() {
    this.ee = new EventEmitter()
  }

  terminate() {
    this.ee.removeAllListeners()
  }

  private onData = (key: Buffer) => {
    const raw = key.toString()
    const chunks = this.parse(raw)

    if (chunks.length > 1) this.queue = chunks.slice(1)
    this.ee.emit('data', chunks[0], () => {
      this.queue = []
      return raw
    })
  }

  parse(input: string) {
    const chars = input.split('')

    let res: any
    const chunks: string[] = []
    while ((res = chars.shift())) {
      if (['\x10', '\x1b'].includes(res)) {
        // length >= 2, example: M-a (1b 61)
        res += chars.shift() || ''

        if (res.endsWith('\x5b')) {
          // length >= 3, example: arrowup (1b 5b 41)
          res += chars.shift() || ''

          if (res.endsWith('\x31') || res.endsWith('\x34') || res.endsWith('\x35') || res.endsWith('\x36')) {
            // length >= 4, example: pageup (1b 5b 35 7e, 1b 5b 36 7e)
            res += chars.shift() || ''
          } else if (res.endsWith('\x4d')) {
            // length >= 4, example: mousedown (1b 5b 4d 20 21 21, 1b 5b 4d 20 c3 80 21)
            res += chars.shift() || ''
            res += chars.shift() || ''
            res += chars.shift() || ''
          }
        }
      }

      chunks.push(res)
    }

    return chunks
  }
  on(callback: (input: string, raw: () => string) => void) {
    if (this.ee.listenerCount('data') === 0) process.stdin.on('data', this.onData)
    this.ee.on('data', callback)
  }

  off(callback: (input: string, raw: () => string) => void) {
    this.ee.off('data', callback)
    if (this.ee.listenerCount('data') === 0) process.stdin.off('data', this.onData)
  }

  render() {
    const chunk = this.queue.shift()
    if (chunk) setTimeout(() => this.ee.emit('data', chunk), 0)
  }
}


================================================
FILE: mediacreators/Banner.tsx
================================================
/* @vhs 80x3@1 */
import ReactCurse, { Banner, useInput } from '..'

const App = () => {
  useInput()

  return <Banner>{'12:34:56' /* new Date().toTimeString().substring(0, 8) */}</Banner>
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/Bar-1.tsx
================================================
/* @vhs 80x3@1 */
import ReactCurse, { Bar, useInput } from '..'

const App = () => {
  useInput()

  return (
    <>
      {[...Array(24)].map((_, index) => (
        <Bar key={index} type="vertical" x={index * 2} height={(index + 1) / 8} />
      ))}
    </>
  )
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/Bar-2.tsx
================================================
/* @vhs 80x3
Hide
Type@0 npm_start
Enter
Sleep 40ms

Show
Sleep 2s */
import ReactCurse, { Bar, Text, useAnimation, useInput } from '..'

const App = () => {
  useInput()
  const { interpolate } = useAnimation(2000)

  return (
    <>
      <Text y={0} x={0}>
        {'<Bar>'} <Bar type="horizontal" x={interpolate(7, 14)} width={interpolate(1, 16)} />
      </Text>
      <Text y={2} x={0}>
        {'<Text>'}{' '}
        <Text inverse x={Math.round(interpolate(7, 14))} width={Math.round(interpolate(1, 16))}>
          {' '.repeat(22)}
        </Text>
      </Text>
    </>
  )
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/Block.tsx
================================================
/* @vhs 80x3@1 */
import ReactCurse, { Block } from '..'

const App = () => {
  setTimeout(() => {}, 1000)

  return (
    <>
      <Block>left</Block>
      <Block align="center">center</Block>
      <Block align="right">right</Block>
    </>
  )
}

process.stdout.write('\x1bc')
ReactCurse.inline(<App />)


================================================
FILE: mediacreators/Canvas-1.tsx
================================================
/* @vhs 80x3@1 */
import ReactCurse, { Canvas, Point, Line, useInput } from '..'

const App = () => {
  useInput()

  return (
    <Canvas width={80} height={6}>
      <Point x={1} y={1} color="BrightGreen" />
      <Line x={0} y={5} dx={79} dy={0} />
    </Canvas>
  )
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/Canvas-2.tsx
================================================
/* @vhs 80x3@1
Set FontFamily "Apple Symbols"
Set FontSize 21

Hide
Type@0 npm_start
Enter
Sleep 250ms

Show
Sleep 250ms */
import ReactCurse, { Canvas, Point, Line, useInput } from '..'

const App = () => {
  useInput()

  return (
    <Canvas width={160} height={12} mode={{ h: 4, w: 2 }}>
      <Point x={1} y={1} color="BrightGreen" />
      <Line x={0} y={11} dx={159} dy={0} />
    </Canvas>
  )
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/Frame.tsx
================================================
/* @vhs 80x3@1 */
import ReactCurse, { Frame, useInput } from '..'

const App = () => {
  useInput()

  return (
    <>
      <Frame type="single" color="Red">
        single border type
      </Frame>
      <Frame type="double" color="Green" y={0}>
        double border type
      </Frame>
      <Frame type="rounded" color="Blue" y={0}>
        rounded border type
      </Frame>
    </>
  )
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/Input-1.tsx
================================================
/* @vhs 80x3
Hide
Type@0 npm_start
Enter
Sleep 250ms

Show
Sleep 250ms
Type hello world
Left 11
Sleep 1s */
import ReactCurse, { Input } from '..'

const App = () => {
  return <Input background="White" height={1} width={8} />
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/Input-2.tsx
================================================
/* @vhs 80x3
Hide
Type@0 npm_start
Enter
Sleep 250ms

Show
Sleep 250ms
Type hello
Enter
Type world
Enter
Enter
Up@200ms 4
Down@200ms 4
Sleep 1s */
import ReactCurse, { Input } from '..'

const App = () => {
  return <Input background="White" height={3} width={16} />
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/List.tsx
================================================
/* @vhs 80x8
Hide
Type@0 npm_start
Enter
Sleep 250ms

Show
Sleep 500ms
Type@100ms jjj
Sleep 500ms
Type@100ms kkk
Sleep 1000ms */
import ReactCurse, { List, Text } from '..'

const App = () => {
  const items = [...Array(8)].map((_, index) => ({ id: index + 1, title: `Task ${index + 1}` }))
  return (
    <List
      data={items}
      renderItem={({ item, selected }) => <Text color={selected ? 'BrightGreen' : undefined}>{item.title}</Text>}
    />
  )
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/ListTable.tsx
================================================
/* @vhs 80x8
Hide
Type@0 npm_start
Enter
Sleep 250ms

Show
Sleep 500ms
Type@200ms jjl
Sleep 500ms
Type@200ms kkh
Sleep 1000ms */
import ReactCurse, { ListTable, Text } from '..'

const App = () => {
  const head = ['id', 'title']
  const items = [...Array(8)].map((_, index) => [index + 1, `Task ${index + 1}`])
  return (
    <ListTable
      head={head}
      renderHead={({ item }) =>
        item.map((i: string, key: string) => (
          <Text key={key} width={8}>
            {i}
          </Text>
        ))
      }
      data={items}
      renderItem={({ item, x, y, index }) =>
        item.map((text: string, key: string) => (
          <Text key={key} color={y === index && x === key ? 'BrightGreen' : undefined} width={8}>
            {text}
          </Text>
        ))
      }
    />
  )
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/Scrollbar.tsx
================================================
/* @vhs 80x3@1 */
import ReactCurse, { Scrollbar, useInput } from '..'

const App = () => {
  useInput()

  return <Scrollbar type="horizontal" offset={10} limit={80} length={160} background={254} />
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/Separator.tsx
================================================
/* @vhs 80x3@1 */
import ReactCurse, { Separator, useInput } from '..'

const App = () => {
  useInput()

  return (
    <>
      <Separator type="vertical" height={3} />
      <Separator type="horizontal" y={1} x={1} width={79} />
    </>
  )
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/Spinner.tsx
================================================
/* @vhs 80x3
Hide
Type@0 npm_start
Enter
Sleep 40ms

Show
Sleep 1.5s */
import ReactCurse, { Spinner, useInput } from '..'

const App = () => {
  useInput()

  return (
    <>
      <Spinner block />
      <Spinner color="BrightGreen">-\|/</Spinner>
    </>
  )
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/Text.tsx
================================================
/* @vhs 80x3@1 */
import ReactCurse, { Text, useInput } from '..'

const App = () => {
  useInput()

  return (
    <>
      <Text color="Red" block>
        hello world
      </Text>
      <Text color="Green" bold block>
        hello world
      </Text>
      <Text color="BrightBlue" underline block>
        hello world
      </Text>
      <Text y={0} x="50%">
        <Text color={128} italic block>
          hello world
        </Text>
        <Text x="100%-11" color="#1ff" strikethrough block>
          hello world
        </Text>
        <Text x="50%-5" color="#e94691" inverse>
          hello world
        </Text>
      </Text>
    </>
  )
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/Trail.tsx
================================================
/* @vhs 80x8
Hide
Type@0 npm_start
Enter
Sleep 40ms

Show
Sleep 2s */
import ReactCurse, { Text, Trail, useInput } from '..'

const App = () => {
  useInput()

  const items = [...Array(8)].map((_, index) => ({ id: index + 1, title: `Task ${index + 1}` }))
  return (
    <Trail delay={100}>
      {items.map(({ id, title }) => (
        <Text key={id} block>
          {title}
        </Text>
      ))}
    </Trail>
  )
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/View.tsx
================================================
/* @vhs 80x8
Hide
Type@0 npm_start
Enter
Sleep 250ms

Show
Sleep 750ms
Type@20ms jjjjjjjjjj
Sleep 250ms
Type@20ms kkkkkkkkkk */
import ReactCurse, { View } from '..'
import json from '../package.json'

const App = () => {
  return <View>{JSON.stringify(json, null, 2)}</View>
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/demo.tsx
================================================
/* @vhs 80x16
Set 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" }

Hide
Type@0 'npm start --src=examples/Todo.tsx'
Enter
Sleep 400ms

Show
Sleep 2s

Hide
Ctrl+c
Type@0 'clear; npm start --src=examples/Visualizer.tsx'
Enter
Sleep 400ms

Show
Sleep 1s

Hide
Ctrl+c
Type@0 'clear; npm start --src=examples/Speed.tsx'
Enter
Sleep 400ms

Show
Sleep 2s

Hide
Ctrl+c
Type@0 'clear; npm start --src=examples/Pong.tsx'
Enter
Sleep 400ms

Show
Sleep 250ms
Type kkk
Sleep 500ms
Type@500ms jj */


================================================
FILE: mediacreators/exampleAnimate.tsx
================================================
/* @vhs 80x3
Hide
Type@0 npm_start
Enter
Sleep 40ms

Show
Sleep 2s */
import ReactCurse, { Text, useAnimation, useInput } from '..'

const App = () => {
  useInput()
  const { interpolate, interpolateColor } = useAnimation(1000)

  return <Text width={interpolate(0, 80)} background={interpolateColor('#282828', '#d79921')} />
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/exampleHello.tsx
================================================
/* @vhs 80x3@1 */
import ReactCurse, { Text, useInput } from '..'

const App = ({ text }: { text: string }) => {
  useInput()

  return <Text color="Red">{text}</Text>
}

ReactCurse.render(<App text="hello world" />)


================================================
FILE: mediacreators/exampleInput.tsx
================================================
/* @vhs 80x3x10
Hide
Type@0 npm_start
Enter
Sleep 250ms

Show
Sleep 250ms
Type@250ms kkjkkkjjjj
Sleep 250ms */
import ReactCurse, { Text, useInput } from '..'
import { useState } from 'react'

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

  useInput(
    input => {
      if (input === 'k') setCounter(counter + 1)
      if (input === 'j') setCounter(counter - 1)
      if (input === 'q') ReactCurse.exit()
    },
    [counter]
  )

  return (
    <Text>
      counter: <Text bold>{counter.toString()}</Text>
    </Text>
  )
}

ReactCurse.render(<App />)


================================================
FILE: mediacreators/logo.tsx
================================================
/* @vhs 48x7
Set Theme { "green": "#98971a", "background": "#ffffff", "foreground": "#ecdbb2" }

Hide
Type@0 npm_start
Enter
Sleep 40ms

Show
Sleep 10s */
import ReactCurse, { Bar, Text, useAnimation, useInput } from '..'

const splitLine = (line: string) => {
  const chunks: Record<string, any> = {}
  let chunksAt = 0
  line.split('').forEach((value, x: number) => {
    if (value !== ' ') {
      chunksAt = x + 1
      return
    }
    if (chunks[chunksAt] === undefined) chunks[chunksAt] = ['']
    chunks[chunksAt] += value
  })
  return Object.entries(chunks)
}

const mask = `
### ### ### ### ###     ### # # ### ### ###
# # #   # # #    #      #   # # # # #   #
##  ##  ### #    #  ### #   # # ##  ### ##
# # #   # # #    #      #   # # # #   # #
# # ### # # ###  #      ### ### # # ### ###
`.trim()

const w = Math.max(...mask.split('\n').map(i => i.length))

// prettier-ignore
const lines = [
  [[w + 32, w], [w * 3 + 32, w * 2]],
  [[w + 8, w],  [w * 3 + 8, w * 2]],
  [[w + 16, w], [w * 3 + 16, w * 2]],
  [[w, w],      [w * 3, w * 2]],
  [[w + 24, w], [w * 3 + 24, w * 2]],
]

export const Logo = ({ ...props }) => {
  useInput(input => input === '\x10\x0d' && ReactCurse.exit())

  const l = 10000
  const { ms, interpolate } = useAnimation(l)

  return (
    <Text y={1} x={2} height={5} {...props}>
      <Text y={0} x={0}>
        <Text color="Green">{mask}</Text>
      </Text>

      {[
        [0, l - 1250],
        [l - 1200, l - 600],
        [l - 550, l - 500]
      ].find(([from, to]) => ms >= from && ms < to) && (
        <Text y={0} x={0} width={w}>
          {lines.map((line, key) => (
            <Text key={key} block>
              {line.map(([x, width], key) => (
                <Bar key={key} type="horizontal" x={interpolate(x, x - w * 4, 0, 2000)} width={width} />
              ))}
            </Text>
          ))}
        </Text>
      )}

      <Text y={0} x={0} width={w}>
        {mask.split('\n').map((line, key) => {
          return (
            <Text key={key} block>
              {splitLine(line).map(([x, str], key) => (
                <Text key={key} x={parseInt(x)}>
                  {str as string}
                </Text>
              ))}
              <Text x={line.length}>{' '.repeat(w - line.length)}</Text>
            </Text>
          )
        })}
      </Text>
    </Text>
  )
}

// ReactCurse.render(<Logo />)


================================================
FILE: mediacreators/useAnimation.tsx
================================================
/* @vhs 80x3
Hide
Type@0 npm_start
Enter
Sleep 40ms

Show
Sleep 2s */
import ReactCurse, { Text, useAnimation, useInput } from '..'

const App = () => {
  useInput()

  const { ms, interpolate, interpolateColor } = useAnimation(1000, 4)
  const rounded = Math.floor(ms / 250) * 250
  const color = interpolateColor('#000', '#0f8', 0, 1000, rounded)
  return (
    <>
      <Text block>ms: {Math.floor(ms / 250) * 250}</Text>
      <Text block>interpolate: {Math.round(interpolate(0, 80, 0, 1000, rounded))}</Text>
      <Text>
        interpolateColor: <Text color={color}>{color}</Text>
      </Text>
    </>
  )
}

ReactCurse.render(<App />)


================================================
FILE: package.json
================================================
{
  "name": "react-curse",
  "version": "1.0.23",
  "description": "Fastest terminal UI for react (TUI, CLI, curses-like)",
  "keywords": [
    "ansi",
    "ascii",
    "blessed",
    "cli",
    "console",
    "cursed",
    "curses",
    "ncurses",
    "gui",
    "ncurses",
    "ranger",
    "react",
    "renderer",
    "term",
    "terminal",
    "tmux",
    "tui",
    "unicode",
    "vim",
    "xterm"
  ],
  "author": {
    "name": "Oleksandr Vasyliev",
    "email": "infely@gmail.com",
    "url": "https://github.com/infely"
  },
  "repository": "infely/react-curse",
  "homepage": "https://github.com/infely/react-curse",
  "main": "index.ts",
  "license": "MIT",
  "type": "module",
  "scripts": {
    "dev": "bun run --watch examples/Example.tsx",
    "build": "NODE_ENV=production bun build --minify --target=node ${npm_config_src:=examples/Example.tsx} > .dist/index.js",
    "lint": "tsc --noEmit",
    "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",
    "npm": "npx esbuild index.ts --outdir=.npm --bundle --platform=node --format=esm --packages=external",
    "postnpm": "tsc --emitDeclarationOnly --declaration --jsx react-jsx --target esnext --esModuleInterop --moduleResolution node index.ts --outdir .npm && bin/postnpm.js",
    "create": "npx esbuild create/Create.tsx --outfile=.create/index.js --bundle --platform=node --define:'process.env.NODE_ENV=\"production\"' --minify --tree-shaking=true",
    "postcreate": "bin/postcreate.js",
    "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",
    "logger": "bin/logger.js"
  },
  "dependencies": {
    "react": "^19.1.1",
    "react-reconciler": "^0.32.0"
  },
  "devDependencies": {
    "esbuild": "^0.25.9",
    "@eslint/js": "^9.34.0",
    "@trivago/prettier-plugin-sort-imports": "^5.2.2",
    "@types/node": "^24.3.0",
    "@types/react": "^19.1.11",
    "@types/react-reconciler": "^0.32.0",
    "eslint": "^9.34.0",
    "eslint-plugin-react": "^7.37.5",
    "globals": "^16.3.0",
    "prettier": "^3.6.2",
    "typescript": "^5.9.2",
    "typescript-eslint": "^8.40.0"
  }
}


================================================
FILE: prettier.config.js
================================================
import sortImports from '@trivago/prettier-plugin-sort-imports'

export default {
  arrowParens: 'avoid',
  printWidth: 120,
  semi: false,
  singleQuote: true,
  trailingComma: 'none',
  plugins: [sortImports]
}


================================================
FILE: readme.md
================================================
# react-curse

<div align="center">
  <br>
  <img width="690" src="media/logo.gif"><br>
  <br>
</div>

Fastest terminal UI for react (TUI, CLI, curses-like)

- It is fast, intuitive and easy to use
- It draws only changed characters
- It uses a small amount of SSH traffic

See it in action:

![](media/demo.gif)

Still here? Let's go deeper:

- It has fancy components that are ready to use or can be tree-shaked from your final bundle
- It supports keyboard and mouse
- It works in fullscreen and inline modes
- It has cool hooks like animation with trail
- It is solely dependent on react
- It can generate an all-in-one bundle around 100 kb

You can easily build full-scale terminal UI applications like:

## Apps that use it

- [mngr](https://github.com/infely/mngr) - Database manager supports mongodb, mysql/mariadb, postgresql, sqlite and json-server
- [nfi](https://github.com/infely/nfi) - Simple nerd fonts icons cheat sheet that allows you to quickly find and copy glyph to clipboard
- [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

## Installation

Just run `npm init react-curse` answer a few questions and you are ready to go

## Examples

#### Hello world

```jsx
import ReactCurse, { Text } from 'react-curse'

const App = ({ text }) => {
  return <Text color="Red">{text}</Text>
}

ReactCurse.render(<App text="hello world" />)
```

![](media/exampleHello.png)

#### How to handle input

```jsx
import { useState } from 'react'
import ReactCurse, { Text, useInput, exit } from 'react-curse'

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

  useInput(
    input => {
      if (input === 'k') setCounter(counter + 1)
      if (input === 'j') setCounter(counter - 1)
      if (input === 'q') exit()
    },
    [counter]
  )

  return (
    <Text>
      counter: <Text bold>{counter}</Text>
    </Text>
  )
}

ReactCurse.render(<App />)
```

![](media/exampleInput.gif)

#### How to animate

```jsx
import ReactCurse, { useAnimation } from 'react-curse'

const App = () => {
  const { interpolate, interpolateColor } = useAnimation(1000)

  return <Text width={interpolate(0, 80)} background={interpolateColor('#282828', '#d79921')} />
}

ReactCurse.render(<App />)
```

![](media/exampleAnimate.gif)

## Contents

- [Components](#components)
  - [`<Text>`](#text)
  - [`<Input>`](#input)
  - [`<Banner>`](#banner)
  - [`<Bar>`](#bar)
  - [`<Block>`](#block)
  - [`<Canvas>`](#canvas), [`<Point>`](#point), [`<Line>`](#line)
  - [`<Frame>`](#frame)
  - [`<List>`](#list)
  - [`<ListTable>`](#listtable)
  - [`<Scrollbar>`](#Scrollbar)
  - [`<Separator>`](#separator)
  - [`<Spinner>`](#spinner)
  - [`<View>`](#view)
- [Hooks](#hooks)
  - [`useAnimation`](#useanimation), [`useTrail`](#usetrail), [`<Trail>`](#trail)
  - [`useChildrenSize`](#usechildrensize)
  - [`useClipboard`](#useclipboard)
  - [`useInput`](#useinput)
  - [`useMouse`](#usemouse)
  - [`useSize`](#usesize)
  - [`useWordWrap`](#useWordWrap)
- [API](#api)
  - [`render`](#render)
  - [`inline`](#inline)
  - [`bell`](#bell)
  - [`exit`](#exit)

## Components

### `<Text>`

Base component\
The only component required to do anything\
Every other component uses this one to draw

##### y?, x?: `number` | `string`

Position from top left corner relative to parent\
Content will be cropped by parent\
See `absolute` to avoid this behavior\
Example: `32, '100%', '100%-8'`

##### height?, width?: `number` | `string`

Size of block, will be cropped by parent\
See `absolute` to avoid this behavior

##### absolute?: `boolean`

Makes position and size ignoring parent container

##### background?, color?: `number` | `string`

Background and foreground color\
Example: `31, 'Red', '#f04020', '#f42'`

##### clear?: `boolean`

Clears block before drawing content\
`height` and `width`

##### block?: `boolean`

Moves cursor to a new line after its content relative to parent

##### bold?, dim?, italic?, underline?, blinking?, inverse?, strikethrough?: `boolean`

Text modifiers

#### Examples

```jsx
<Text color="Red" block>hello world</Text>
<Text color="Green" bold block>hello world</Text>
<Text color="BrightBlue" underline block>hello world</Text>
<Text y={0} x="50%">
  <Text color={128} italic block>hello world</Text>
  <Text x="100%-11" color="#1ff" strikethrough block>hello world</Text>
  <Text x="50%-5" color="#e94691" inverse>hello world</Text>
</Text>
```

![](media/Text.png)

### `<Input>`

Text input component with cursor movement and text scroll support\
If its height is more than 1, then it switches to multiline, like textarea\
Most terminal shortcuts are supported

##### focus?: `boolean` = `true`

Makes it active

##### type?: `'text'` | `'password'` | `'hidden'` = `‘text'`

##### initialValue?: `string`

##### cursorBackground?: `number` | `string`

##### onCancel?: `() => void`

##### onChange?: `(string) => void`

##### onSubmit?: `(string) => void`

#### Examples

```jsx
<Input background="#404040" height={1} width={8} />
```

![](media/Input-1.gif)

![](media/Input-2.gif)

### `<Banner>`

Displays big text

##### y?, x?: `number` | `string`

##### background?, color?: `number` | `string`

##### children: `string`

#### Examples

```jsx
<Banner>{new Date().toTimeString().substring(0, 8)}</Banner>
```

![](media/Banner.png)

### `<Bar>`

Displays vertical or horizontal bar with 1/8 character resolution

##### type: `'vertical'` | `'horizontal'`

##### y & height, x & width: `number`

#### Examples

```jsx
<>
  {[...Array(24)].map((_, index) => (
    <Bar key={index} type="vertical" x={index * 2} height={(index + 1) / 8} />
  ))}
</>
```

![](media/Bar-1.png)

Compare to `<Text>`

![](media/Bar-2.gif)

### `<Block>`

Aligns content

##### width?: `number`

##### align?: `'left'` | `'center'` | `'right'` = `'left'`

#### Examples

```jsx
<Block>left</Block>
<Block align="center">center</Block>
<Block align="right">right</Block>
```

![](media/Block.png)

### `<Canvas>`

Create a canvas for drawing with one these modes

##### mode: `{ h: 1, w: 1 }` | `{ h: 2, w: 1 }` | `{ h: 2, w: 2 }` | `{ h: 4, w: 2 }`

Pixels per character

##### height, width: `number`

Size in pixels

##### children: (`Point` | `Line`)`[]`

#### `<Point>`

Draws a point at the coordinates

##### y, x: `number`

##### color?: `number` | `string`

#### `<Line>`

Draws a line using coordinates

##### y, x, dy, dx: `number`

##### color?: `number` | `string`

#### Examples

```jsx
<Canvas width={80} height={6}>
  <Point x={1} y={1} color="Yellow" />
  <Line x={0} y={5} dx={79} dy={0} />
</Canvas>
```

![](media/Canvas-1.png)

Braille's font demo (`{ h: 4, w: 2 }`)

![](media/Canvas-2.png)

### `<Frame>`

Draws frame around its content

##### children: `string`

##### type?: `'single'` | `'double'` | `'rounded'` = `'single'`

##### height?, width?: `number`

#### Examples

```jsx
<Frame type="single" color="Red">single border type</Frame>
<Frame type="double" color="Green" y={0}>double border type</Frame>
<Frame type="rounded" color="Blue" y={0}>rounded border type</Frame>
```

![](media/Frame.png)

### `<List>`

Creates a list with navigation support\
Vim shortcuts are supported

##### focus?: `boolean`

##### initialPos?: { y: `number` }

##### data?: `any[]`

##### renderItem?: `(object) => JSX.Element`

##### height?, width?: `number`

##### scrollbar?: `boolean`

##### scrollbarBackground?: `boolean`

##### scrollbarColor?: `boolean`

##### vi?: `boolean` = `true`

##### pass?: `any`

##### onChange?: `(object) => void`

##### onSubmit?: `(object) => void`

#### Examples

```jsx
const items = [...Array(8)].map((_, index) => ({ id: index + 1, title: `Task ${index + 1}` }))
return (
  <List
    data={items}
    renderItem={({ item, selected }) => <Text color={selected ? 'Green' : undefined}>{item.title}</Text>}
  />
)
```

![](media/List.gif)

### `<ListTable>`: `<List>`

Creates a table with navigation support\
Vim shortcuts are supported

##### mode?: `'cell'` | `'row'` = `'cell'`

##### head?: `any[]`

##### renderHead?: `(object) => JSX.Element`

##### data?: `any[][]`

#### Examples

```jsx
const head = ['id', 'title']
const items = [...Array(8)].map((_, index) => [index + 1, `Task ${index + 1}`])
return (
  <ListTable
    head={head}
    renderHead={({ item }) =>
      item.map((i, key) => (
        <Text key={key} width={8}>
          {i}
        </Text>
      ))
    }
    data={items}
    renderItem={({ item, x, y, index }) =>
      item.map((text, key) => (
        <Text key={key} color={y === index && x === key ? 'Green' : undefined} width={8}>
          {text}
        </Text>
      ))
    }
  />
)
```

![](media/ListTable.gif)

### `<Scrollbar>`

Draws a scrollbar with 1/8 character resolution

##### type?: `'vertical'` | `'horizontal'` = `'vertical'`

##### offset: `number`

##### limit: `number`

##### length: `number`

##### background?, color?: `number` | `string`

#### Examples

```jsx
<Scrollbar type="horizontal" offset={10} limit={80} length={160} />
```

![](media/Scrollbar.png)

### `<Separator>`

Draws a vertical or horizontal line

##### type: `'vertical'` | `'horizontal'`

##### height, width: `number`

#### Examples

```jsx
<Separator type="vertical" height={3} />
<Separator type="horizontal" y={1} x={1} width={79} />
```

![](media/Separator.png)

### `<Spinner>`

Draws an animated spinner

##### children?: `string`

#### Examples

```jsx
<Spinner block />
<Spinner color="BrightGreen">-\|/</Spinner>
```

![](media/Spinner.gif)

### `<View>`

Creates a scrollable viewport\
Vim shortcuts are supported

##### focus?: `boolean`

##### height?: `number`

##### scrollbar?: `boolean`

##### vi?: `boolean` = `true`

##### children: `any`

#### Examples

```jsx
<View>{JSON.stringify(json, null, 2)}</View>
```

![](media/View.gif)

## hooks

### `useAnimation`

##### (time: `number`, fps?: `'number'` = `60`) => `object`

Creates a timer for a specified duration\
That gives you time and interpolation functions each frame of animation

#### return

##### ms: `number`

##### interpolate: (from: `number`, to: `number`, delay?: `number`)

##### interpolateColor: (from: `string`, to: `string`: delay?: `number`)

#### Examples

```jsx
const { ms } = useAnimation(1000, 4)
return ms // 0, 250, 500, 750, 1000
```

```jsx
const { interpolate } = useAnimation(1000, 4)
return interpolate(0, 80) // 0, 20, 40, 60, 80
```

```jsx
const { interpolateColor } = useAnimation(1000, 4)
return interpolateColor('#000', '#0f8') // #000, #042, #084, #0c6, #0f8
```

![](media/useAnimation.gif)

#### `<Trail>`

Mutate array of items to show one by one with latency

##### delay: `number`

##### children: `JSX.Element[]`

#### Examples

```jsx
const items = [...Array(8)].map((_, index) => ({ id: index + 1, title: `Task ${index + 1}` }))
return (
  <Trail delay={100}>
    {items.map(({ id, title }) => (
      <Text key={id} block>
        {title}
      </Text>
    ))}
  </Trail>
)
```

![](media/Trail.gif)

#### `useTrail`

##### (delay: `number`, items: `JSX.Element[]`, key?: `string` = `'key'`) => `JSX.Element[]`

Same as `<Trail>` but hook\
You can pass it to `data` property of `<List>` component for example

#### Examples

```jsx
<List data={useTrail(items)} />
```

### `useChildrenSize`

##### (value: `string`) => `object`

Gives you content size

#### return

##### height, width: `number`

#### Examples

```jsx
useChildrenSize('1\n22\n333') // { height: 3, width: 3 }
```

### `useClipboard`

#### () => `array`

Allows you to work with the system clipboard

#### return

##### getClipboard: `() => string`

##### setClipboard: `(value: string) => void`

#### Examples

```jsx
const { getClipboard, setClipboard } = useClipboard()
const string = getClipboard()
setClipboard(string.toUpperCase()) // copied
```

### `useInput`

##### (callback: `(string) => void`, dependencies: `any[]`) => `void`

Allows you to handle keyboard input

#### Examples

```jsx
set[(counter, setCounter)] = useState(0)

useInput(
  input => {
    if (input === 'k') setCounter(counter + 1)
    if (input === 'j') setCounter(counter - 1)
  },
  [counter]
)
```

### `useMouse`

##### (callback: `(object) => void`, dependencies: `any[]`)

Allows you to handle mouse input

#### Examples

```jsx
set[(counter, setCounter)] = useState(0)

useMouse(
  event => {
    if (event.type === 'wheelup') setCounter(counter + 1)
    if (event.type === 'wheeldown') setCounter(counter - 1)
  },
  [counter]
)
```

### `useSize`

##### () => `object`

Gives you terminal size\
Updates when size is changing

#### return

##### height, width: `number`

#### Examples

```jsx
useSize() // { height: 24, width: 80 }
```

### `useWordWrap`

##### (text: `string`, width?: `number`) => `object`

Gives your text a word wrap

#### return

##### height, width: `number`

#### Examples

```jsx
useWordWrap('hello world', 5) // hello\nworld
```

## API

### `render` (children: `JSX.Element`) => `void`

Renders your fullscreen application to `stdout`

### `inline` (children: `JSX.Element`) => `void`

Renders your inline application to `stdout`

### `bell`

#### () => `void`

Makes a terminal bell

```jsx
bell() // ding
```

### `exit`

##### (code: `number` = `0`) => `void`

Allows you to exit from an application that waits for user input or has timers

#### Examples

```jsx
useInput(input => {
  if (input === 'q') exit()
})
```


================================================
FILE: reconciler.ts
================================================
import { type TextProps } from './components/Text'
import Reconciler from 'react-reconciler'

let currentUpdatePriority = 0

export class TextElement {
  props: TextProps
  parent: TextElement | null
  children: TextElement[]

  constructor(props: object = {}) {
    this.props = props
    this.parent = null
    this.children = []
  }

  terminate() {
    this.children = []
  }

  appendChild(child: any) {
    this.children = [...this.children, child]
  }

  commitUpdate(nextProps: any) {
    this.props = nextProps
  }

  insertBefore(child: any, beforeChild: any) {
    const index = this.children.indexOf(beforeChild)
    if (index !== -1) this.children.splice(index, 0, child)
  }

  removeChild(child: any) {
    const index = this.children.indexOf(child)
    if (index !== -1) this.children.splice(index, 1)
  }
}

export class TextInstance {
  value: string

  constructor(value: string) {
    this.value = value
  }

  commitTextUpdate(value: string) {
    this.value = value
  }

  toString() {
    return this.value
  }
}

export default (resetAfterCommit: () => void) => {
  // prettier-ignore
  const reconciler = Reconciler({
    supportsMutation: true,
    appendChild(parentInstance: any, child: any) { parentInstance.appendChild(child) },
    appendChildToContainer(container: any, child: any) { container.appendChild(child) },
    appendInitialChild(parentInstance: any, child: any) { parentInstance.appendChild(child) },
    clearContainer() {},
    commitTextUpdate(textInstance: any, _oldText: any, newText: any) { textInstance.commitTextUpdate(newText) },
    commitUpdate(instance: any,  _type: any, _prevProps: any, nextProps: any) { instance.commitUpdate(nextProps) },
    createInstance(type: any, props: any) { if (type === 'text') { return new TextElement(props) } else { throw new Error('must be <Text>') } },
    createTextInstance(text: string) { return new TextInstance(text) },
    detachDeletedInstance() {},
    finalizeInitialChildren() { return false },
    getChildHostContext() { return {} },
    getPublicInstance(instance: any) { return instance },
    getRootHostContext(rootContainer: any) { return rootContainer },
    insertBefore(parentInstance: any, child: any, beforeChild: any) { parentInstance.insertBefore(child, beforeChild) },
    insertInContainerBefore(container: any, child: any, beforeChild: any) { container.insertBefore(child, beforeChild) },
    prepareForCommit() { return null },
    // @ts-expect-error any
    prepareUpdate() { return true },
    removeChild(parentInstance: any, child: any) { parentInstance.removeChild(child) },
    removeChildFromContainer(container: any, child: any) { container.removeChild(child) },
    resetAfterCommit() { resetAfterCommit() },
    shouldSetTextContent() { return false },

    setCurrentUpdatePriority(newPriority: number) { currentUpdatePriority = newPriority },
    getCurrentUpdatePriority() { return currentUpdatePriority },
    resolveUpdatePriority() { return currentUpdatePriority !== 0 ? currentUpdatePriority : 16 },
    maySuspendCommit() { return false },
  })

  return reconciler
}


================================================
FILE: renderer.ts
================================================
import Input from './input'
import Reconciler, { TextElement } from './reconciler'
import Screen from './screen'
import Term from './term'
import { spawnSync, type SpawnSyncOptions, type SpawnSyncReturns } from 'child_process'
import { type ReactElement } from 'react'

class Renderer {
  container: TextElement
  screen: Screen
  input: Input
  term: Term
  reconciler: ReturnType<typeof Reconciler>
  callback?: (value: any) => void
  throttleAt = 0
  throttleTimeout?: NodeJS.Timeout

  constructor() {
    this.container = new TextElement()
    this.screen = new Screen()
    this.input = new Input()
    this.reconciler = Reconciler(this.throttle)
    this.term = new Term() // TODO:
  }

  render(reactElement: ReactElement, options = { fullscreen: true, print: false }) {
    this.term = new Term()
    this.term.init(options.fullscreen, options.print).then(() => {
      this.reconciler.updateContainer(
        reactElement,
        this.reconciler.createContainer(this.container, 0, null, false, null, '', () => {}, null)
      )
    })
  }

  inline(reactElement: ReactElement, options = { fullscreen: false, print: false }) {
    this.render(reactElement, options)
  }

  prompt<T>(reactElement: ReactElement, options = { fullscreen: false, print: false }): Promise<T> {
    this.render(reactElement, options)

    return new Promise(resolve => {
      this.callback = resolve
    })
  }

  print(reactElement: ReactElement, options = { fullscreen: false, print: true }) {
    this.render(reactElement, options)

    return new Promise(resolve => {
      this.callback = resolve
    })
  }

  frame(reactElement: ReactElement, options = { fullscreen: false, print: true }) {
    this.render(reactElement, options)

    return new Promise(resolve => {
      this.callback = (value: any) => {
        process.stdout.write(value)
        resolve(value)
      }
    })
  }

  terminate(value: any) {
    this.container.terminate()
    this.input.terminate()
    if (this.term) {
      this.term.terminate()
      // process.stdout.write(this.term.terminate())
    }
    this.callback?.(value)
  }

  spawnSync(
    command: string,
    args: ReadonlyArray<string>,
    options: SpawnSyncOptions
  ): SpawnSyncReturns<string | Buffer> {
    const res = spawnSync(command, args, options)
    this.term?.reinit()
    this.term?.render(this.screen.buffer)
    return res
  }

  bell() {
    process.stdout.write('\x07')
  }

  exit(code: number | any = 0) {
    if (typeof code === 'number') process.exit(code)
    this.term?.setResult(code)
  }

  private throttle = () => {
    const at = Date.now()
    const nextAt = Math.max(0, 1000 / 60 - (at - this.throttleAt))
    clearTimeout(this.throttleTimeout)
    this.throttleTimeout = setTimeout(() => {
      this.throttleAt = at
      this.screen.render(this.container.children)
      this.term?.render(this.screen.buffer)
      this.input.render()
    }, nextAt)
  }
}

export default new Renderer()


================================================
FILE: screen.ts
================================================
import { type TextProps } from './components/Text'
import { type TextElement } from './reconciler'
import { type ReactElement } from 'react'

export type Color =
  | number
  | string
  | 'Black'
  | 'Red'
  | 'Green'
  | 'Yellow'
  | 'Blue'
  | 'Magenta'
  | 'Cyan'
  | 'White'
  | 'BrightBlack'
  | 'BrightRed'
  | 'BrightGreen'
  | 'BrightYellow'
  | 'BrightBlue'
  | 'BrightMagenta'
  | 'BrightCyan'
  | 'BrightWhite'

export interface Modifier {
  background?: Color
  color?: Color
  clear?: boolean
  bold?: boolean
  dim?: boolean
  italic?: boolean
  underline?: boolean
  blinking?: boolean
  inverse?: boolean
  strikethrough?: boolean
}

export type Char = [string, Modifier]

interface Bounds {
  x: number
  y: number
  x1: number
  y1: number
  x2: number
  y2: number
}

class Screen {
  buffer: Char[][]
  cursor = { x: 0, y: 0 }
  size = { x1: 0, y1: 0, x2: 0, y2: 0 }

  constructor() {
    this.buffer = this.generateBuffer()
  }

  generateBuffer() {
    this.size = { x1: 0, y1: 0, x2: process.stdout.columns, y2: process.stdout.rows }
    return [...Array(this.size.y2)].map(() => [...Array(this.size.x2)].map(() => [' ', {}] as Char))
  }

  clearBuffer() {
    this.buffer = this.generateBuffer()
    this.cursor = { x: 0, y: 0 }
  }

  render(elements: TextElement[]) {
    this.clearBuffer()
    this.renderElement(elements, { ...this.cursor, ...this.size })
  }

  stringAt(value: string, limit: number) {
    const percent = parseFloat(value)
    let diff = ''

    const index = value.search(/%[+-]\d+$/)
    if (index !== -1) diff = value.substring(index + 1)
    if (!value.endsWith('%' + diff) || isNaN(percent)) throw new Error('must be percent')

    return Math.round((limit / 100) * percent) + parseInt(diff || '0')
  }

  renderElement(element: ReactElement | ReactElement[] | any, prevBounds: Bounds, prevProps: TextProps = {}) {
    if (Array.isArray(element)) return element.forEach(i => this.renderElement(i, prevBounds, prevProps))

    const { children, ...props } = element.props ?? { children: element }

    if (typeof props.x === 'string')
      props.x = this.stringAt(props.x, props.absolute ? this.buffer[0].length : prevBounds.x2 - prevBounds.x)
    if (typeof props.y === 'string')
      props.y = this.stringAt(props.y, props.absolute ? this.buffer.length : prevBounds.y2 - prevBounds.y)
    if (typeof props.width === 'string')
      props.width = this.stringAt(props.width, props.absolute ? this.buffer[0].length : prevBounds.x2 - prevBounds.x)
    if (typeof props.height === 'string')
      props.height = this.stringAt(props.height, props.absolute ? this.buffer.length : prevBounds.y2 - prevBounds.y)
    if (props.width !== undefined && isNaN(props.width)) props.width = 0
    if (props.height !== undefined && isNaN(props.height)) props.height = 0
    const x = props.x !== undefined ? (props.absolute ? 0 : prevBounds.x) + props.x : this.cursor.x
    const y = props.y !== undefined ? (props.absolute ? 0 : prevBounds.y) + props.y : this.cursor.y
    const x1 =
      props.x !== undefined
        ? props.absolute
          ? props.x
          : Math.max(prevBounds.x, prevBounds.x + props.x)
        : prevBounds.x1
    const y1 =
      props.y !== undefined
        ? props.absolute
          ? props.y
          : Math.max(prevBounds.y, prevBounds.y + props.y)
        : prevBounds.y1
    const x2 =
      props.width !== undefined
        ? Math.min(props.absolute ? this.buffer[0].length : prevBounds.x2, props.width + x)
        : props.absolute
          ? this.buffer[0].length
          : prevBounds.x2
    const y2 =
      props.height !== undefined
        ? Math.min(props.absolute ? this.buffer.length : prevBounds.y2, props.height + y)
        : props.absolute
          ? this.buffer.length
          : prevBounds.y2
    const bounds = { x, y, x1, y1, x2, y2 }
    this.cursor.x = bounds.x
    this.cursor.y = bounds.y

    const modifiers = Object.fromEntries(
      ['color', 'background', 'bold', 'dim', 'italic', 'underline', 'blinking', 'inverse', 'strikethrough']
        .map(i => [i, props[i] ?? (prevProps as any)[i]])
        .filter(i => i[1])
    )
    if ((props.background || props.clear) && (props.width || props.height))
      this.fill(bounds, props.absolute ? bounds : prevBounds, modifiers)

    if (Array.isArray(children) || children?.props) {
      this.renderElement(element.children, bounds, modifiers)
    } else if (typeof children === 'number' || children) {
      const text = children.toString()
      if (text.includes('\n')) {
        const lines = children.toString().split('\n')
        lines.forEach((line: string, index: number) => {
          this.renderElement(line, bounds, modifiers)
          if (index < lines.length - 1) this.carret(prevBounds)
        })
      } else {
        this.cursor.x = this.put(text, bounds, modifiers)
      }
    }

    if (props.block) this.carret(prevBounds)
    if (props.width || props.height) {
      this.cursor.x = props.block ? prevBounds.x : bounds.x2
      this.cursor.y = props.block ? bounds.y2 : prevBounds.y
    }
  }

  fill(bounds: Bounds, prevBounds: Bounds, modifiers: TextProps) {
    for (let y = bounds.y; y < bounds.y2; y++) {
      if (y < Math.max(0, prevBounds.y1) || y >= Math.min(prevBounds.y2, this.buffer.length)) continue
      for (let x = bounds.x; x < bounds.x2; x++) {
        if (x < Math.max(0, prevBounds.x1) || x >= Math.min(prevBounds.x2, this.buffer[y].length)) continue

        this.buffer[y][x] = [' ', modifiers]
      }
    }
  }

  put(text: string, bounds: Bounds, modifiers: TextProps) {
    const { x, y } = bounds

    let i: number
    for (i = 0; i < text.length; i++) {
      if (y < Math.max(0, bounds.y1) || y >= Math.min(this.buffer.length, bounds.y2)) break
      if (x + i < Math.max(0, bounds.x1) || x + i >= Math.min(this.buffer[y].length, bounds.x2)) continue

      this.buffer[y][x + i] = [text[i], modifiers]
    }

    return x + i
  }

  carret(bounds: Bounds) {
    this.cursor.x = bounds.x ?? 0
    this.cursor.y++
  }
}

export default Screen


================================================
FILE: term.ts
================================================
import Renderer from './renderer'
import { type Char, type Color, type Modifier } from './screen'

const ESC = '\x1B'

class Term {
  fullscreen = true
  print = false
  isResized = false
  isMouseEnabled = false
  prevBuffer: Char[][] | undefined
  prevModifier: Modifier = {}
  nextWritePrefix = ''
  size = { width: process.stdout.columns, height: process.stdout.rows }
  offset = { x: 0, y: 0 }
  cursor = { x: 0, y: 0 }
  maxCursor = { x: 0, y: 0 }
  result: any

  async init(fullscreen: boolean, print: boolean) {
    this.fullscreen = fullscreen
    this.print = print

    process.stdout.on('resize', () => {
      this.isResized = true

      // this.offset.y += process.stdout.rows - this.size.height
      this.size = { width: process.stdout.columns, height: process.stdout.rows }
    })

    process.on('exit', this.onExit)

    if (fullscreen) {
      this.append(`${ESC}[?1049h`) // enables the alternative buffer
      this.append(`${ESC}c`) // clear screen
    } else {
      const cursor = await this.termGetCursor()
      this.offset = cursor
    }
    this.append(`${ESC}[?25l`) // make cursor invisible
  }

  reinit() {
    this.prevModifier = {}
    this.prevBuffer = undefined
    this.append(`${ESC}[?1049h${ESC}c${ESC}[?25l`) // enables the alternative buffer, clear screen, make cursor invisible
  }

  onExit = (code: number) => {
    if (code !== 0) return

    process.stdout.write(this.terminate())
    process.exit(0)
  }

  terminate() {
    process.off('exit', this.onExit)

    const sequence: string[] = []
    if (this.fullscreen) {
      sequence.push(`${ESC}[?1049l`) // disables the alternative buffer
    } else {
      const y = this.maxCursor.y - this.cursor.y
      if (y > 0) sequence.push(`${ESC}[${y}B`) // moves cursor down
      const x = this.maxCursor.x - this.cursor.x + 1
      if (x > 0) sequence.push(`${ESC}[${x}C`) // moves cursor right
      sequence.push(`\n`)
    }
    sequence.push(`${ESC}[?25h`) // make cursor visible
    if (this.isMouseEnabled) sequence.push(`${ESC}[?1000l`) // disable mouse
    return sequence.join('')
  }

  append(value: string) {
    this.nextWritePrefix += value
  }

  setResult(result: any) {
    this.result = result
  }

  enableMouse() {
    this.append(`${ESC}[?1000h${ESC}[?1005h`) // enable mouse
    this.isMouseEnabled = true
  }

  async termGetCursor(): Promise<{ x: number; y: number }> {
    process.stdin.setRawMode(true)
    process.stdout.write('\x1b[6n')
    return await new Promise(resolve => {
      process.stdin.once('data', data => {
        const [x, y] = data
          .toString()
          .slice(2, -1)
          .split(';')
          .reverse()
          .map(i => parseInt(i) - 1)
        resolve({ x, y })
        // process.stdin.unref()
        // process.stdin.setRawMode(false)
      })
    })
  }

  parseHexColor(color: string) {
    if (!color.match(/^([\da-f]{6})|([\da-f]{3})$/i)) return

    return (
      color.length === 4
        ? color
            .substring(1, 4)
            .split('')
            .map(i => i + i)
        : (color.substring(1, 7).match(/.{2}/g) as any)
    ).map((i: string) => parseInt(i, 16))
  }

  parseColor(color: Color | string | number, offset = 0) {
    if (typeof color === 'number') {
      if (color < 0 || color > 255) throw new Error('color not found')
      return `${38 + offset};5;${color}`
    }

    if (color.startsWith('#')) {
      const [r, g, b] = this.parseHexColor(color)
      return `${38 + offset};2;${r};${g};${b}`
    }

    const names = {
      black: 30,
      red: 31,
      green: 32,
      yellow: 33,
      blue: 34,
      magenta: 35,
      cyan: 36,
      white: 37,
      brightblack: 90,
      brightred: 91,
      brightgreen: 92,
      brightyellow: 93,
      brightblue: 94,
      brightmagenta: 95,
      brightcyan: 96,
      brightwhite: 97
    }
    const colorFromName = (names as any)[color.toLowerCase()]
    if (colorFromName === undefined) throw new Error('color not found')
    return colorFromName + offset
  }

  createModifierSequence(modifier: Modifier) {
    if (JSON.stringify(modifier) === '{}') return '0'

    const { prevModifier } = this

    const sequence: (number | string)[] = []

    if (modifier.color !== prevModifier.color) sequence.push(modifier.color ? this.parseColor(modifier.color) : 39)
    if (modifier.background !== prevModifier.background)
      sequence.push(modifier.background ? this.parseColor(modifier.background, 10) : 49)
    if (modifier.bold !== prevModifier.bold) sequence.push(modifier.bold ? 1 : modifier.dim ? '22;2' : 22)
    if (modifier.dim !== prevModifier.dim) sequence.push(modifier.dim ? 2 : modifier.bold ? '22;1' : 22)
    if (modifier.italic !== prevModifier.italic) sequence.push(modifier.italic ? 3 : 23)
    if (modifier.underline !== prevModifier.underline) sequence.push(modifier.underline ? 4 : 24)
    if (modifier.blinking !== prevModifier.blinking) sequence.push(modifier.blinking ? 5 : 25)
    if (modifier.inverse !== prevModifier.inverse) sequence.push(modifier.inverse ? 7 : 27)
    if (modifier.strikethrough !== prevModifier.strikethrough) sequence.push(modifier.strikethrough ? 9 : 29)

    return sequence.join(';')
  }

  isIcon(char: string) {
    const code = char.charCodeAt(0)
    return (code >= 9211 && code <= 9214) || [9829, 9889, 11096].includes(code) || (code >= 57344 && code <= 64838)
  }

  render(buffer: Char[][]) {
    let full = false
    let result = ''

    if (this.isResized) {
      if (this.fullscreen) {
        result += `${ESC}[H` // moves cursor to home position
        this.cursor = { x: 0, y: 0 }
        full = true
      }
      this.isResized = false
    }

    for (let y = 0; y < buffer.length; y++) {
      const line = buffer[y]
      const prevLine = this.prevBuffer?.[y]
      let includesEmoji = false
      let includesIcon = false

      const diffLine = full
        ? line
        : line
            .map((i: Char, x: number) => {
              const [prevChar, prevModifier] = prevLine && prevLine[x] ? prevLine[x] : [' ', {}]
              const [char, modifier] = i
              return prevChar !== char || JSON.stringify(prevModifier) !== JSON.stringify(modifier) ? i : null
            })
            .filter(i => i !== undefined)

      const chunks: Record<string, [any, any]> = {}
      let chunksAt = 0
      diffLine.forEach((value, x: number) => {
        if (value === null) {
          chunksAt = x + 1
          return
        }

        const [char, modifier] = value
        if (chunks[chunksAt] === undefined) chunks[chunksAt] = ['', '']
        if (JSON.stringify(modifier) !== JSON.stringify(this.prevModifier)) {
          chunks[chunksAt][1] += `\x1b[${this.createModifierSequence(modifier)}m`
          this.prevModifier = modifier
        }
        chunks[chunksAt][0] += char
        chunks[chunksAt][1] += char
      })

      Object.entries(chunks).map(([index, value]) => {
        const [str, strWithModifiers] = value as [string, string]
        const x = parseInt(index)
        if (/\p{Emoji}/u.test(str)) includesEmoji = true
        if (!includesIcon && str.split('').find((i: string) => this.isIcon(i))) includesIcon = true

        if (x === 0 && y === this.cursor.y + 1) {
          if (!this.fullscreen && y > this.maxCursor.y) {
            this.offset.y -= 1
          }

          result += '\n'
        } else {
          if (!this.fullscreen && y > this.cursor.y && y > this.maxCursor.y) {
            const diff = y - this.maxCursor.y
            result += '\n'.repeat(diff)
            this.cursor = { y: this.cursor.y + diff, x: 0 }

            const rows = this.offset.y + y - (process.stdout.rows - 1)
            if (rows > 0) this.offset.y -= rows
          }

          if (y !== this.cursor.y && x !== this.cursor.x) {
            result += `${ESC}[${y + 1 + this.offset.y};${x + 1}H` // move cursor to position
          } else if (y > this.cursor.y) {
            const diff = y - this.cursor.y
            result += `${ESC}[${diff > 1 ? diff : ''}B` // move cursor down
          } else if (y < this.cursor.y) {
            const diff = this.cursor.y - y
            result += `${ESC}[${diff > 1 ? diff : ''}A` // move cursor up
          } else if (x > this.cursor.x) {
            if (includesEmoji || includesIcon) {
              result += `${ESC}[G${ESC}[${x > 1 ? x : ''}C` // move cursor to column, move cursor right
            } else {
              const diff = x - this.cursor.x
              result += `${ESC}[${diff > 1 ? diff : ''}C` // move cursor right
            }
          } else if (x < this.cursor.x) {
            if (includesEmoji) {
              result += `${ESC}[G${ESC}[${x > 1 ? x : ''}C` // move cursor to start, move cursor right
            } else {
              const diff = this.cursor.x - x
              result += `${ESC}[${diff > 1 ? diff : ''}D` // move cursor left
            }
          }
        }
        result += strWithModifiers

        this.cursor = { x: x + str.length, y }
      })
      // if (this.cursor.x > buffer[y].length - 1) {
      //   this.cursor = { x: 0, y: 0 }
      //   result += `${ESC}[H` // moves cursor to home position
      // }
      if (this.cursor.x > this.maxCursor.x) this.maxCursor.x = this.cursor.x
      if (this.cursor.y > this.maxCursor.y) this.maxCursor.y = this.cursor.y
    }
    this.prevBuffer = buffer

    if (this.nextWritePrefix) {
      result = this.nextWritePrefix + result
      this.nextWritePrefix = ''
    }

    if (this.result !== undefined || this.print) {
      result += this.terminate()
    }

    if (result) {
      if (this.print) return Renderer.terminate(result)

      process.stdout.write(result)
      // log(/* Date.now(), */ result)
    }

    if (this.result !== undefined) {
      Renderer.terminate(this.result)
    }
  }
}

export default Term


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ESNext",
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "allowJs": false,
    "checkJs": false,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "jsx": "react-jsx",
    "noEmit": true
  },
  "exclude": ["node_modules", ".create", ".dist", ".npm", "create/template"]
}


================================================
FILE: utils/chunk.ts
================================================
export default function chunk(arr: any, size: number, cache: any[] = []) {
  const tmp = [...arr]
  while (tmp.length) cache.push(tmp.splice(0, size))
  return cache
}


================================================
FILE: utils/log.ts
================================================
import { createConnection } from 'net'
import { tmpdir } from 'os'
import { join } from 'path'
import { inspect } from 'util'

let socket: any
let once = false

const connect = () => {
  return new Promise((resolve, reject) => {
    socket = createConnection(join(tmpdir(), 'node-log.sock'))
      .on('connect', function () {
        socket.write('\0')
        resolve(socket)
      })
      .on('error', () => {
        reject(undefined)
      })
  })
}

const toString = (data: any[]) => data.map(i => inspect(i, undefined, null, true)).join(' ')

export default async function log(...rest: any) {
  if (once && socket === undefined) return
  try {
    once = true
    if (socket === undefined) socket = await connect()
    socket.write(toString(rest) + '\n')
  } catch {
    socket = undefined
    // console.log(...rest)
  }
}
Download .txt
gitextract_vg983zb_/

├── .gitignore
├── bin/
│   ├── font.js
│   ├── logger.js
│   ├── postcreate.js
│   ├── postdist.js
│   └── postnpm.js
├── components/
│   ├── Banner.tsx
│   ├── Bar.tsx
│   ├── Block.tsx
│   ├── Canvas.tsx
│   ├── Frame.tsx
│   ├── Input.tsx
│   ├── List.tsx
│   ├── ListTable.tsx
│   ├── Scrollbar.tsx
│   ├── Separator.tsx
│   ├── Spinner.tsx
│   ├── Text.tsx
│   └── View.tsx
├── create/
│   ├── Create.tsx
│   ├── readme.md
│   └── template/
│       ├── App.tsx
│       ├── package.json
│       └── tsconfig.json
├── eslint.config.js
├── examples/
│   ├── Banner.tsx
│   ├── Canvas.tsx
│   ├── Chat.tsx
│   ├── Example.tsx
│   ├── Inline.tsx
│   ├── Pong.tsx
│   ├── Prompt.tsx
│   ├── Speed.tsx
│   ├── Todo.tsx
│   └── Visualizer.tsx
├── hooks/
│   ├── useAnimation.ts
│   ├── useBell.ts
│   ├── useChildrenSize.ts
│   ├── useClipboard.ts
│   ├── useExit.ts
│   ├── useInput.ts
│   ├── useMouse.ts
│   ├── useSize.ts
│   └── useWordWrap.ts
├── index.ts
├── input.ts
├── mediacreators/
│   ├── Banner.tsx
│   ├── Bar-1.tsx
│   ├── Bar-2.tsx
│   ├── Block.tsx
│   ├── Canvas-1.tsx
│   ├── Canvas-2.tsx
│   ├── Frame.tsx
│   ├── Input-1.tsx
│   ├── Input-2.tsx
│   ├── List.tsx
│   ├── ListTable.tsx
│   ├── Scrollbar.tsx
│   ├── Separator.tsx
│   ├── Spinner.tsx
│   ├── Text.tsx
│   ├── Trail.tsx
│   ├── View.tsx
│   ├── demo.tsx
│   ├── exampleAnimate.tsx
│   ├── exampleHello.tsx
│   ├── exampleInput.tsx
│   ├── logo.tsx
│   └── useAnimation.tsx
├── package.json
├── prettier.config.js
├── readme.md
├── reconciler.ts
├── renderer.ts
├── screen.ts
├── term.ts
├── tsconfig.json
└── utils/
    ├── chunk.ts
    └── log.ts
Download .txt
SYMBOL INDEX (128 symbols across 24 files)

FILE: components/Banner.tsx
  constant FONT (line 5) | const FONT =
  type BannerProps (line 34) | interface BannerProps extends TextProps {
  function Banner (line 38) | function Banner({ children, ...props }: BannerProps) {

FILE: components/Bar.tsx
  type BarProps (line 62) | interface BarProps extends TextProps {
  function Bar (line 70) | function Bar({ type = 'vertical', y, x, height, width, ...props }: BarPr...

FILE: components/Block.tsx
  type BlockProps (line 4) | interface BlockProps extends TextProps {
  function Block (line 10) | function Block({ width = undefined, align = 'left', children, ...props }...

FILE: components/Canvas.tsx
  class CanvasClass (line 6) | class CanvasClass {
    method constructor (line 22) | constructor(width: number, height: number, mode = { w: 1, h: 2 }) {
    method clear (line 33) | clear() {
    method set (line 38) | set(x: number, y: number, color: Color) {
    method line (line 46) | line(x0: number, y0: number, x1: number, y1: number, color: Color) {
    method render (line 75) | render() {
  type Point (line 103) | interface Point {
  type Line (line 111) | interface Line {
  type CanvasProps (line 121) | interface CanvasProps extends TextProps {
  function Canvas (line 128) | function Canvas({ mode = { w: 1, h: 2 }, width, height, children, ...pro...

FILE: components/Frame.tsx
  constant FRAMES (line 4) | const FRAMES = {
  type FrameProps (line 10) | interface FrameProps extends TextProps {
  function Frame (line 17) | function Frame({ type = 'single', height: _height, width: _width, childr...

FILE: components/Input.tsx
  type InputProps (line 133) | interface InputProps extends TextProps {
  function Input (line 143) | function Input({

FILE: components/List.tsx
  type ListPos (line 51) | interface ListPos {
  type ListBase (line 61) | interface ListBase {
  type ListProps (line 76) | interface ListProps extends ListBase {
  function List (line 80) | function List({

FILE: components/ListTable.tsx
  type ListTableProps (line 19) | interface ListTableProps extends ListBase {
  function List (line 26) | function List({

FILE: components/Scrollbar.tsx
  type ScrollbarProps (line 5) | interface ScrollbarProps {
  function Scrollbar (line 14) | function Scrollbar({ type = 'vertical', offset, limit, length, backgroun...

FILE: components/Separator.tsx
  type SeparatorProps (line 4) | interface SeparatorProps extends TextProps {
  function Separator (line 10) | function Separator({ type = 'vertical', height: _height, width: _width, ...

FILE: components/Spinner.tsx
  type SpinnerProps (line 4) | interface SpinnerProps extends TextProps {
  function Spinner (line 8) | function Spinner({ children, ...props }: SpinnerProps) {

FILE: components/Text.tsx
  type TextProps (line 3) | interface TextProps extends Modifier {
  function Text (line 13) | function Text({ children, ...props }: TextProps) {

FILE: components/View.tsx
  type ViewProps (line 8) | interface ViewProps extends TextProps {
  function View (line 15) | function View({ focus = true, height: _height, scrollbar, vi = true, chi...

FILE: examples/Canvas.tsx
  constant CELL (line 5) | const CELL = 4
  constant COLORS (line 6) | const COLORS = ['Red', 'Green', 'Blue']
  constant DATA (line 7) | const DATA = [...Array(COLORS.length)].map(() => {

FILE: examples/Speed.tsx
  constant TEXT (line 4) | const TEXT = ''

FILE: hooks/useAnimation.ts
  type useAnimation (line 57) | interface useAnimation {

FILE: hooks/useMouse.ts
  type Event (line 4) | interface Event {

FILE: input.ts
  class Input (line 3) | class Input {
    method constructor (line 7) | constructor() {
    method terminate (line 11) | terminate() {
    method parse (line 26) | parse(input: string) {
    method on (line 57) | on(callback: (input: string, raw: () => string) => void) {
    method off (line 62) | off(callback: (input: string, raw: () => string) => void) {
    method render (line 67) | render() {

FILE: reconciler.ts
  class TextElement (line 6) | class TextElement {
    method constructor (line 11) | constructor(props: object = {}) {
    method terminate (line 17) | terminate() {
    method appendChild (line 21) | appendChild(child: any) {
    method commitUpdate (line 25) | commitUpdate(nextProps: any) {
    method insertBefore (line 29) | insertBefore(child: any, beforeChild: any) {
    method removeChild (line 34) | removeChild(child: any) {
  class TextInstance (line 40) | class TextInstance {
    method constructor (line 43) | constructor(value: string) {
    method commitTextUpdate (line 47) | commitTextUpdate(value: string) {
    method toString (line 51) | toString() {
  method appendChild (line 60) | appendChild(parentInstance: any, child: any) { parentInstance.appendChil...
  method appendChildToContainer (line 61) | appendChildToContainer(container: any, child: any) { container.appendChi...
  method appendInitialChild (line 62) | appendInitialChild(parentInstance: any, child: any) { parentInstance.app...
  method clearContainer (line 63) | clearContainer() {}
  method commitTextUpdate (line 64) | commitTextUpdate(textInstance: any, _oldText: any, newText: any) { textI...
  method commitUpdate (line 65) | commitUpdate(instance: any,  _type: any, _prevProps: any, nextProps: any...
  method createInstance (line 66) | createInstance(type: any, props: any) { if (type === 'text') { return ne...
  method createTextInstance (line 67) | createTextInstance(text: string) { return new TextInstance(text) }
  method detachDeletedInstance (line 68) | detachDeletedInstance() {}
  method finalizeInitialChildren (line 69) | finalizeInitialChildren() { return false }
  method getChildHostContext (line 70) | getChildHostContext() { return {} }
  method getPublicInstance (line 71) | getPublicInstance(instance: any) { return instance }
  method getRootHostContext (line 72) | getRootHostContext(rootContainer: any) { return rootContainer }
  method insertBefore (line 73) | insertBefore(parentInstance: any, child: any, beforeChild: any) { parent...
  method insertInContainerBefore (line 74) | insertInContainerBefore(container: any, child: any, beforeChild: any) { ...
  method prepareForCommit (line 75) | prepareForCommit() { return null }
  method prepareUpdate (line 77) | prepareUpdate() { return true }
  method removeChild (line 78) | removeChild(parentInstance: any, child: any) { parentInstance.removeChil...
  method removeChildFromContainer (line 79) | removeChildFromContainer(container: any, child: any) { container.removeC...
  method resetAfterCommit (line 80) | resetAfterCommit() { resetAfterCommit() }
  method shouldSetTextContent (line 81) | shouldSetTextContent() { return false }
  method setCurrentUpdatePriority (line 83) | setCurrentUpdatePriority(newPriority: number) { currentUpdatePriority = ...
  method getCurrentUpdatePriority (line 84) | getCurrentUpdatePriority() { return currentUpdatePriority }
  method resolveUpdatePriority (line 85) | resolveUpdatePriority() { return currentUpdatePriority !== 0 ? currentUp...
  method maySuspendCommit (line 86) | maySuspendCommit() { return false }

FILE: renderer.ts
  class Renderer (line 8) | class Renderer {
    method constructor (line 18) | constructor() {
    method render (line 26) | render(reactElement: ReactElement, options = { fullscreen: true, print...
    method inline (line 36) | inline(reactElement: ReactElement, options = { fullscreen: false, prin...
    method prompt (line 40) | prompt<T>(reactElement: ReactElement, options = { fullscreen: false, p...
    method print (line 48) | print(reactElement: ReactElement, options = { fullscreen: false, print...
    method frame (line 56) | frame(reactElement: ReactElement, options = { fullscreen: false, print...
    method terminate (line 67) | terminate(value: any) {
    method spawnSync (line 77) | spawnSync(
    method bell (line 88) | bell() {
    method exit (line 92) | exit(code: number | any = 0) {

FILE: screen.ts
  type Color (line 5) | type Color =
  type Modifier (line 25) | interface Modifier {
  type Char (line 38) | type Char = [string, Modifier]
  type Bounds (line 40) | interface Bounds {
  class Screen (line 49) | class Screen {
    method constructor (line 54) | constructor() {
    method generateBuffer (line 58) | generateBuffer() {
    method clearBuffer (line 63) | clearBuffer() {
    method render (line 68) | render(elements: TextElement[]) {
    method stringAt (line 73) | stringAt(value: string, limit: number) {
    method renderElement (line 84) | renderElement(element: ReactElement | ReactElement[] | any, prevBounds...
    method fill (line 159) | fill(bounds: Bounds, prevBounds: Bounds, modifiers: TextProps) {
    method put (line 170) | put(text: string, bounds: Bounds, modifiers: TextProps) {
    method carret (line 184) | carret(bounds: Bounds) {

FILE: term.ts
  constant ESC (line 4) | const ESC = '\x1B'
  class Term (line 6) | class Term {
    method init (line 20) | async init(fullscreen: boolean, print: boolean) {
    method reinit (line 43) | reinit() {
    method terminate (line 56) | terminate() {
    method append (line 74) | append(value: string) {
    method setResult (line 78) | setResult(result: any) {
    method enableMouse (line 82) | enableMouse() {
    method termGetCursor (line 87) | async termGetCursor(): Promise<{ x: number; y: number }> {
    method parseHexColor (line 105) | parseHexColor(color: string) {
    method parseColor (line 118) | parseColor(color: Color | string | number, offset = 0) {
    method createModifierSequence (line 152) | createModifierSequence(modifier: Modifier) {
    method isIcon (line 173) | isIcon(char: string) {
    method render (line 178) | render(buffer: Char[][]) {

FILE: utils/chunk.ts
  function chunk (line 1) | function chunk(arr: any, size: number, cache: any[] = []) {

FILE: utils/log.ts
  function log (line 24) | async function log(...rest: any) {
Condensed preview — 79 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (135K chars).
[
  {
    "path": ".gitignore",
    "chars": 46,
    "preview": ".DS_Store\n.create/\n.dist/\n.npm/\nnode_modules/\n"
  },
  {
    "path": "bin/font.js",
    "chars": 3901,
    "preview": "#!/usr/bin/env node\n\n// prettier-ignore\nconst letters = [\n  [ //  !\n    0b00000100,\n    0b00000100,\n    0b00000100,\n    "
  },
  {
    "path": "bin/logger.js",
    "chars": 487,
    "preview": "#!/usr/bin/env node\nimport { rmSync } from 'node:fs'\nimport { createServer } from 'node:net'\nimport { tmpdir } from 'nod"
  },
  {
    "path": "bin/postcreate.js",
    "chars": 921,
    "preview": "#!/usr/bin/env node\nimport { execSync } from 'node:child_process'\nimport { readFileSync, writeFileSync, chmodSync } from"
  },
  {
    "path": "bin/postdist.js",
    "chars": 465,
    "preview": "#!/usr/bin/env node\nimport { readFileSync, writeFileSync, chmodSync, unlinkSync } from 'node:fs'\nimport { argv } from 'n"
  },
  {
    "path": "bin/postnpm.js",
    "chars": 883,
    "preview": "#!/usr/bin/env node\nimport { readFileSync, writeFileSync } from 'node:fs'\n\nconst makeJson = () => {\n  const json = JSON."
  },
  {
    "path": "components/Banner.tsx",
    "chars": 1873,
    "preview": "import chunk from '../utils/chunk'\nimport Text, { type TextProps } from './Text'\nimport { useMemo } from 'react'\n\nconst "
  },
  {
    "path": "components/Bar.tsx",
    "chars": 2221,
    "preview": "import Text, { TextProps } from './Text'\n\nconst getSize = (offset: number, size: number) => {\n  offset = Math.round(offs"
  },
  {
    "path": "components/Block.tsx",
    "chars": 884,
    "preview": "import useSize from '../hooks/useSize'\nimport Text, { TextProps } from './Text'\n\nexport interface BlockProps extends Tex"
  },
  {
    "path": "components/Canvas.tsx",
    "chars": 4822,
    "preview": "import { Color } from '../screen'\nimport chunk from '../utils/chunk'\nimport Text, { TextProps } from './Text'\nimport { C"
  },
  {
    "path": "components/Frame.tsx",
    "chars": 1292,
    "preview": "import useChildrenSize from '../hooks/useChildrenSize'\nimport Text, { TextProps } from './Text'\n\nconst FRAMES = {\n  sing"
  },
  {
    "path": "components/Input.tsx",
    "chars": 6947,
    "preview": "import useInput from '../hooks/useInput'\nimport Renderer from '../renderer'\nimport { Color } from '../screen'\nimport Tex"
  },
  {
    "path": "components/List.tsx",
    "chars": 5013,
    "preview": "import useInput from '../hooks/useInput'\nimport useSize from '../hooks/useSize'\nimport { Color } from '../screen'\nimport"
  },
  {
    "path": "components/ListTable.tsx",
    "chars": 6591,
    "preview": "import useInput from '../hooks/useInput'\nimport useSize from '../hooks/useSize'\nimport { getYO, inputHandler, ListBase, "
  },
  {
    "path": "components/Scrollbar.tsx",
    "chars": 978,
    "preview": "import { Color } from '../screen'\nimport Bar from './Bar'\nimport Text from './Text'\n\nexport interface ScrollbarProps {\n "
  },
  {
    "path": "components/Separator.tsx",
    "chars": 941,
    "preview": "import useSize from '../hooks/useSize'\nimport Text, { TextProps } from './Text'\n\nexport interface SeparatorProps extends"
  },
  {
    "path": "components/Spinner.tsx",
    "chars": 638,
    "preview": "import useAnimation from '../hooks/useAnimation'\nimport Text, { TextProps } from './Text'\n\nexport interface SpinnerProps"
  },
  {
    "path": "components/Text.tsx",
    "chars": 443,
    "preview": "import { Modifier } from '../screen'\n\nexport interface TextProps extends Modifier {\n  readonly absolute?: boolean\n  read"
  },
  {
    "path": "components/View.tsx",
    "chars": 2069,
    "preview": "import useChildrenSize from '../hooks/useChildrenSize'\nimport useInput from '../hooks/useInput'\nimport useSize from '../"
  },
  {
    "path": "create/Create.tsx",
    "chars": 2852,
    "preview": "import ReactCurse, { Input, Spinner, Text, useAnimation, useInput } from '..'\nimport { Logo } from '../mediacreators/log"
  },
  {
    "path": "create/readme.md",
    "chars": 94,
    "preview": "# create-react-curse\n\nGenerate a [react-curse](https://www.npmjs.com/package/react-curse) app\n"
  },
  {
    "path": "create/template/App.tsx",
    "chars": 619,
    "preview": "import { useState } from 'react'\nimport ReactCurse, { Text, useInput } from 'react-curse'\n\nconst App = () => {\n  const ["
  },
  {
    "path": "create/template/package.json",
    "chars": 617,
    "preview": "{\n  \"name\": \"react-curse-app\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"sc"
  },
  {
    "path": "create/template/tsconfig.json",
    "chars": 527,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"module\": \"ESNext\",\n "
  },
  {
    "path": "eslint.config.js",
    "chars": 664,
    "preview": "import js from '@eslint/js'\nimport pluginReact from 'eslint-plugin-react'\nimport { defineConfig } from 'eslint/config'\ni"
  },
  {
    "path": "examples/Banner.tsx",
    "chars": 940,
    "preview": "import ReactCurse, { Text, useInput } from '..'\nimport Banner from '../components/Banner'\nimport { useEffect, useState }"
  },
  {
    "path": "examples/Canvas.tsx",
    "chars": 2638,
    "preview": "import ReactCurse, { Text, useInput, useSize } from '..'\nimport Canvas, { Line } from '../components/Canvas'\nimport { us"
  },
  {
    "path": "examples/Chat.tsx",
    "chars": 1459,
    "preview": "import ReactCurse, { Banner, Input, Separator, Text, Trail, useAnimation, useWordWrap } from '..'\nimport { useState } fr"
  },
  {
    "path": "examples/Example.tsx",
    "chars": 661,
    "preview": "import ReactCurse, { Text, useInput } from '..'\nimport Frame from '../components/Frame'\nimport { useState } from 'react'"
  },
  {
    "path": "examples/Inline.tsx",
    "chars": 215,
    "preview": "import ReactCurse, { Text } from '..'\n\nconst App = () => {\n  return (\n    <>\n      <Text block>Line 1</Text>\n      <Text"
  },
  {
    "path": "examples/Pong.tsx",
    "chars": 1427,
    "preview": "import ReactCurse, { Banner, Canvas, Point, Line, useSize, useInput } from '..'\nimport { useEffect, useState } from 'rea"
  },
  {
    "path": "examples/Prompt.tsx",
    "chars": 2813,
    "preview": "import ReactCurse, { Block, Frame, Input, Text, useAnimation, useInput } from '..'\nimport { useState } from 'react'\n\ncon"
  },
  {
    "path": "examples/Speed.tsx",
    "chars": 1296,
    "preview": "import ReactCurse, { Text, useInput } from '..'\nimport { useEffect, useState } from 'react'\n\nconst TEXT = ''\nconst widt"
  },
  {
    "path": "examples/Todo.tsx",
    "chars": 2997,
    "preview": "import ReactCurse, { Block, Input, List, Text, useInput, useSize } from '..'\nimport useAnimation, { useTrail } from '../"
  },
  {
    "path": "examples/Visualizer.tsx",
    "chars": 1582,
    "preview": "import ReactCurse, { Bar, Text, Trail, useAnimation, useSize } from '..'\nimport { useMemo } from 'react'\n\n// const App ="
  },
  {
    "path": "hooks/useAnimation.ts",
    "chars": 3111,
    "preview": "import Renderer from '../renderer'\nimport { useState, useEffect, useRef } from 'react'\n\nconst interpolate = (toLow: numb"
  },
  {
    "path": "hooks/useBell.ts",
    "chars": 79,
    "preview": "/**\n * @deprecated\n */\nexport default () => {\n  process.stdout.write('\\x07')\n}\n"
  },
  {
    "path": "hooks/useChildrenSize.ts",
    "chars": 878,
    "preview": "import { type ReactElement, useEffect, useState } from 'react'\n\nconst render = (element: ReactElement | ReactElement[] |"
  },
  {
    "path": "hooks/useClipboard.ts",
    "chars": 603,
    "preview": "import { spawnSync } from 'child_process'\n\nexport default (): [() => string, (input: string) => string] => {\n  const get"
  },
  {
    "path": "hooks/useExit.ts",
    "chars": 220,
    "preview": "import Renderer from '../renderer'\nimport process from 'process'\n\n/**\n * @deprecated\n */\nexport default (code: number | "
  },
  {
    "path": "hooks/useInput.ts",
    "chars": 594,
    "preview": "import Renderer from '../renderer'\nimport { useEffect } from 'react'\n\nexport default (callback: (input: string, raw: () "
  },
  {
    "path": "hooks/useMouse.ts",
    "chars": 890,
    "preview": "import Renderer from '../renderer'\nimport { type DependencyList, useEffect } from 'react'\n\ninterface Event {\n  type: 'mo"
  },
  {
    "path": "hooks/useSize.ts",
    "chars": 552,
    "preview": "import { useEffect, useState } from 'react'\n\nconst subscribers = new Set<(size: { width: number; height: number }) => vo"
  },
  {
    "path": "hooks/useWordWrap.ts",
    "chars": 591,
    "preview": "import useSize from './useSize'\n\nexport default (text: string, _width: number | undefined = undefined) => {\n  const widt"
  },
  {
    "path": "index.ts",
    "chars": 1375,
    "preview": "export { default } from './renderer'\nexport { default as Banner } from './components/Banner'\nexport { default as Bar } f"
  },
  {
    "path": "input.ts",
    "chars": 1912,
    "preview": "import EventEmitter from 'events'\n\nexport default class Input {\n  ee: EventEmitter\n  queue: string[] = []\n\n  constructor"
  },
  {
    "path": "mediacreators/Banner.tsx",
    "chars": 220,
    "preview": "/* @vhs 80x3@1 */\nimport ReactCurse, { Banner, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return <Banner>"
  },
  {
    "path": "mediacreators/Bar-1.tsx",
    "chars": 295,
    "preview": "/* @vhs 80x3@1 */\nimport ReactCurse, { Bar, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return (\n    <>\n  "
  },
  {
    "path": "mediacreators/Bar-2.tsx",
    "chars": 613,
    "preview": "/* @vhs 80x3\nHide\nType@0 npm_start\nEnter\nSleep 40ms\n\nShow\nSleep 2s */\nimport ReactCurse, { Bar, Text, useAnimation, useI"
  },
  {
    "path": "mediacreators/Block.tsx",
    "chars": 308,
    "preview": "/* @vhs 80x3@1 */\nimport ReactCurse, { Block } from '..'\n\nconst App = () => {\n  setTimeout(() => {}, 1000)\n\n  return (\n "
  },
  {
    "path": "mediacreators/Canvas-1.tsx",
    "chars": 300,
    "preview": "/* @vhs 80x3@1 */\nimport ReactCurse, { Canvas, Point, Line, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  re"
  },
  {
    "path": "mediacreators/Canvas-2.tsx",
    "chars": 432,
    "preview": "/* @vhs 80x3@1\nSet FontFamily \"Apple Symbols\"\nSet FontSize 21\n\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 250ms"
  },
  {
    "path": "mediacreators/Frame.tsx",
    "chars": 425,
    "preview": "/* @vhs 80x3@1 */\nimport ReactCurse, { Frame, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return (\n    <>\n"
  },
  {
    "path": "mediacreators/Input-1.tsx",
    "chars": 257,
    "preview": "/* @vhs 80x3\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 250ms\nType hello world\nLeft 11\nSleep 1s */\nimport React"
  },
  {
    "path": "mediacreators/Input-2.tsx",
    "chars": 297,
    "preview": "/* @vhs 80x3\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 250ms\nType hello\nEnter\nType world\nEnter\nEnter\nUp@200ms "
  },
  {
    "path": "mediacreators/List.tsx",
    "chars": 486,
    "preview": "/* @vhs 80x8\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 500ms\nType@100ms jjj\nSleep 500ms\nType@100ms kkk\nSleep 1"
  },
  {
    "path": "mediacreators/ListTable.tsx",
    "chars": 834,
    "preview": "/* @vhs 80x8\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 500ms\nType@200ms jjl\nSleep 500ms\nType@200ms kkh\nSleep 1"
  },
  {
    "path": "mediacreators/Scrollbar.tsx",
    "chars": 230,
    "preview": "/* @vhs 80x3@1 */\nimport ReactCurse, { Scrollbar, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return <Scro"
  },
  {
    "path": "mediacreators/Separator.tsx",
    "chars": 274,
    "preview": "/* @vhs 80x3@1 */\nimport ReactCurse, { Separator, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return (\n   "
  },
  {
    "path": "mediacreators/Spinner.tsx",
    "chars": 292,
    "preview": "/* @vhs 80x3\nHide\nType@0 npm_start\nEnter\nSleep 40ms\n\nShow\nSleep 1.5s */\nimport ReactCurse, { Spinner, useInput } from '."
  },
  {
    "path": "mediacreators/Text.tsx",
    "chars": 684,
    "preview": "/* @vhs 80x3@1 */\nimport ReactCurse, { Text, useInput } from '..'\n\nconst App = () => {\n  useInput()\n\n  return (\n    <>\n "
  },
  {
    "path": "mediacreators/Trail.tsx",
    "chars": 451,
    "preview": "/* @vhs 80x8\nHide\nType@0 npm_start\nEnter\nSleep 40ms\n\nShow\nSleep 2s */\nimport ReactCurse, { Text, Trail, useInput } from "
  },
  {
    "path": "mediacreators/View.tsx",
    "chars": 306,
    "preview": "/* @vhs 80x8\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 750ms\nType@20ms jjjjjjjjjj\nSleep 250ms\nType@20ms kkkkkk"
  },
  {
    "path": "mediacreators/demo.tsx",
    "chars": 925,
    "preview": "/* @vhs 80x16\nSet Theme { \"name\": \"gruvbox\", \"black\": \"#32302f\", \"red\": \"#cc241d\", \"green\": \"#98971a\", \"yellow\": \"#d7992"
  },
  {
    "path": "mediacreators/exampleAnimate.tsx",
    "chars": 357,
    "preview": "/* @vhs 80x3\nHide\nType@0 npm_start\nEnter\nSleep 40ms\n\nShow\nSleep 2s */\nimport ReactCurse, { Text, useAnimation, useInput "
  },
  {
    "path": "mediacreators/exampleHello.tsx",
    "chars": 217,
    "preview": "/* @vhs 80x3@1 */\nimport ReactCurse, { Text, useInput } from '..'\n\nconst App = ({ text }: { text: string }) => {\n  useIn"
  },
  {
    "path": "mediacreators/exampleInput.tsx",
    "chars": 574,
    "preview": "/* @vhs 80x3x10\nHide\nType@0 npm_start\nEnter\nSleep 250ms\n\nShow\nSleep 250ms\nType@250ms kkjkkkjjjj\nSleep 250ms */\nimport Re"
  },
  {
    "path": "mediacreators/logo.tsx",
    "chars": 2383,
    "preview": "/* @vhs 48x7\nSet Theme { \"green\": \"#98971a\", \"background\": \"#ffffff\", \"foreground\": \"#ecdbb2\" }\n\nHide\nType@0 npm_start\nE"
  },
  {
    "path": "mediacreators/useAnimation.tsx",
    "chars": 644,
    "preview": "/* @vhs 80x3\nHide\nType@0 npm_start\nEnter\nSleep 40ms\n\nShow\nSleep 2s */\nimport ReactCurse, { Text, useAnimation, useInput "
  },
  {
    "path": "package.json",
    "chars": 2363,
    "preview": "{\n  \"name\": \"react-curse\",\n  \"version\": \"1.0.23\",\n  \"description\": \"Fastest terminal UI for react (TUI, CLI, curses-like"
  },
  {
    "path": "prettier.config.js",
    "chars": 213,
    "preview": "import sortImports from '@trivago/prettier-plugin-sort-imports'\n\nexport default {\n  arrowParens: 'avoid',\n  printWidth: "
  },
  {
    "path": "readme.md",
    "chars": 13465,
    "preview": "# 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 "
  },
  {
    "path": "reconciler.ts",
    "chars": 3104,
    "preview": "import { type TextProps } from './components/Text'\nimport Reconciler from 'react-reconciler'\n\nlet currentUpdatePriority "
  },
  {
    "path": "renderer.ts",
    "chars": 2957,
    "preview": "import Input from './input'\nimport Reconciler, { TextElement } from './reconciler'\nimport Screen from './screen'\nimport "
  },
  {
    "path": "screen.ts",
    "chars": 6060,
    "preview": "import { type TextProps } from './components/Text'\nimport { type TextElement } from './reconciler'\nimport { type ReactEl"
  },
  {
    "path": "term.ts",
    "chars": 9846,
    "preview": "import Renderer from './renderer'\nimport { type Char, type Color, type Modifier } from './screen'\n\nconst ESC = '\\x1B'\n\nc"
  },
  {
    "path": "tsconfig.json",
    "chars": 574,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"module\": \"ESNext\",\n "
  },
  {
    "path": "utils/chunk.ts",
    "chars": 168,
    "preview": "export default function chunk(arr: any, size: number, cache: any[] = []) {\n  const tmp = [...arr]\n  while (tmp.length) c"
  },
  {
    "path": "utils/log.ts",
    "chars": 832,
    "preview": "import { createConnection } from 'net'\nimport { tmpdir } from 'os'\nimport { join } from 'path'\nimport { inspect } from '"
  }
]

About this extraction

This page contains the full source code of the infely/react-curse GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 79 files (122.1 KB), approximately 39.2k tokens, and a symbol index with 128 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!