{"id":21884026,"url":"https://github.com/infely/react-curse","last_synced_at":"2026-03-16T14:02:11.140Z","repository":{"id":65391907,"uuid":"591542127","full_name":"infely/react-curse","owner":"infely","description":"A curses-like blazingly fast react renderer","archived":false,"fork":false,"pushed_at":"2024-11-18T19:34:22.000Z","size":2634,"stargazers_count":148,"open_issues_count":4,"forks_count":5,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-12-11T17:27:12.471Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/infely.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-21T03:00:33.000Z","updated_at":"2024-12-03T10:40:16.000Z","dependencies_parsed_at":"2024-06-19T11:28:50.265Z","dependency_job_id":"61ce0f3a-b5cd-48e2-9c66-2b2b62f21c15","html_url":"https://github.com/infely/react-curse","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infely%2Freact-curse","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infely%2Freact-curse/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infely%2Freact-curse/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infely%2Freact-curse/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/infely","download_url":"https://codeload.github.com/infely/react-curse/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230511482,"owners_count":18237658,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-28T10:11:55.831Z","updated_at":"2026-03-16T14:02:11.125Z","avatar_url":"https://github.com/infely.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# react-curse\n\n\u003cdiv align=\"center\"\u003e\n  \u003cbr\u003e\n  \u003cimg width=\"690\" src=\"media/logo.gif\"\u003e\u003cbr\u003e\n  \u003cbr\u003e\n\u003c/div\u003e\n\nFastest terminal UI for react (TUI, CLI, curses-like)\n\n- It is fast, intuitive and easy to use\n- It draws only changed characters\n- It uses a small amount of SSH traffic\n\nSee it in action:\n\n![](media/demo.gif)\n\nStill here? Let's go deeper:\n\n- It has fancy components that are ready to use or can be tree-shaked from your final bundle\n- It supports keyboard and mouse\n- It works in fullscreen and inline modes\n- It has cool hooks like animation with trail\n- It is solely dependent on react\n- It can generate an all-in-one bundle around 100 kb\n\nYou can easily build full-scale terminal UI applications like:\n\n## Apps that use it\n\n- [mngr](https://github.com/infely/mngr) - Database manager supports mongodb, mysql/mariadb, postgresql, sqlite and json-server\n- [nfi](https://github.com/infely/nfi) - Simple nerd fonts icons cheat sheet that allows you to quickly find and copy glyph to clipboard\n- [cosmo](https://github.com/turutupa/cosmo) - A tool for visualizing graphs on terminal. And it allows you to pan around and search by id/value nodes, and in the near future edit/remove/add nodes too\n\n## Installation\n\nJust run `npm init react-curse` answer a few questions and you are ready to go\n\n## Examples\n\n#### Hello world\n\n```jsx\nimport ReactCurse, { Text } from 'react-curse'\n\nconst App = ({ text }) =\u003e {\n  return \u003cText color=\"Red\"\u003e{text}\u003c/Text\u003e\n}\n\nReactCurse.render(\u003cApp text=\"hello world\" /\u003e)\n```\n\n![](media/exampleHello.png)\n\n#### How to handle input\n\n```jsx\nimport { useState } from 'react'\nimport ReactCurse, { Text, useInput, exit } from 'react-curse'\n\nconst App = () =\u003e {\n  const [counter, setCounter] = useState(0)\n\n  useInput(\n    input =\u003e {\n      if (input === 'k') setCounter(counter + 1)\n      if (input === 'j') setCounter(counter - 1)\n      if (input === 'q') exit()\n    },\n    [counter]\n  )\n\n  return (\n    \u003cText\u003e\n      counter: \u003cText bold\u003e{counter}\u003c/Text\u003e\n    \u003c/Text\u003e\n  )\n}\n\nReactCurse.render(\u003cApp /\u003e)\n```\n\n![](media/exampleInput.gif)\n\n#### How to animate\n\n```jsx\nimport ReactCurse, { useAnimation } from 'react-curse'\n\nconst App = () =\u003e {\n  const { interpolate, interpolateColor } = useAnimation(1000)\n\n  return \u003cText width={interpolate(0, 80)} background={interpolateColor('#282828', '#d79921')} /\u003e\n}\n\nReactCurse.render(\u003cApp /\u003e)\n```\n\n![](media/exampleAnimate.gif)\n\n## Contents\n\n- [Components](#components)\n  - [`\u003cText\u003e`](#text)\n  - [`\u003cInput\u003e`](#input)\n  - [`\u003cBanner\u003e`](#banner)\n  - [`\u003cBar\u003e`](#bar)\n  - [`\u003cBlock\u003e`](#block)\n  - [`\u003cCanvas\u003e`](#canvas), [`\u003cPoint\u003e`](#point), [`\u003cLine\u003e`](#line)\n  - [`\u003cFrame\u003e`](#frame)\n  - [`\u003cList\u003e`](#list)\n  - [`\u003cListTable\u003e`](#listtable)\n  - [`\u003cScrollbar\u003e`](#Scrollbar)\n  - [`\u003cSeparator\u003e`](#separator)\n  - [`\u003cSpinner\u003e`](#spinner)\n  - [`\u003cView\u003e`](#view)\n- [Hooks](#hooks)\n  - [`useAnimation`](#useanimation), [`useTrail`](#usetrail), [`\u003cTrail\u003e`](#trail)\n  - [`useChildrenSize`](#usechildrensize)\n  - [`useClipboard`](#useclipboard)\n  - [`useInput`](#useinput)\n  - [`useMouse`](#usemouse)\n  - [`useSize`](#usesize)\n  - [`useWordWrap`](#useWordWrap)\n- [API](#api)\n  - [`render`](#render)\n  - [￼`inline`￼](#inline)\n  - [`bell`](#bell)\n  - [`exit`](#exit)\n\n## Components\n\n### `\u003cText\u003e`\n\nBase component\\\nThe only component required to do anything\\\nEvery other component uses this one to draw\n\n##### y?, x?: `number` | `string`\n\nPosition from top left corner relative to parent\\\nContent will be cropped by parent\\\nSee `absolute` to avoid this behavior\\\nExample: `32, '100%', '100%-8'`\n\n##### height?, width?: `number` | `string`\n\nSize of block, will be cropped by parent\\\nSee `absolute` to avoid this behavior\n\n##### absolute?: `boolean`\n\nMakes position and size ignoring parent container\n\n##### background?, color?: `number` | `string`\n\nBackground and foreground color\\\nExample: `31, 'Red', '#f04020', '#f42'`\n\n##### clear?: `boolean`\n\nClears block before drawing content\\\n`height` and `width`\n\n##### block?: `boolean`\n\nMoves cursor to a new line after its content relative to parent\n\n##### bold?, dim?, italic?, underline?, blinking?, inverse?, strikethrough?: `boolean`\n\nText modifiers\n\n#### Examples\n\n```jsx\n\u003cText color=\"Red\" block\u003ehello world\u003c/Text\u003e\n\u003cText color=\"Green\" bold block\u003ehello world\u003c/Text\u003e\n\u003cText color=\"BrightBlue\" underline block\u003ehello world\u003c/Text\u003e\n\u003cText y={0} x=\"50%\"\u003e\n  \u003cText color={128} italic block\u003ehello world\u003c/Text\u003e\n  \u003cText x=\"100%-11\" color=\"#1ff\" strikethrough block\u003ehello world\u003c/Text\u003e\n  \u003cText x=\"50%-5\" color=\"#e94691\" inverse\u003ehello world\u003c/Text\u003e\n\u003c/Text\u003e\n```\n\n![](media/Text.png)\n\n### `\u003cInput\u003e`\n\nText input component with cursor movement and text scroll support\\\nIf its height is more than 1, then it switches to multiline, like textarea\\\nMost terminal shortcuts are supported\n\n##### focus?: `boolean` = `true`\n\nMakes it active\n\n##### type?: `'text'` | `'password'` | `'hidden'` = `‘text'`\n\n##### initialValue?: `string`\n\n##### cursorBackground?: `number` | `string`\n\n##### onCancel?: `() =\u003e void`\n\n##### onChange?: `(string) =\u003e void`\n\n##### onSubmit?: `(string) =\u003e void`\n\n#### Examples\n\n```jsx\n\u003cInput background=\"#404040\" height={1} width={8} /\u003e\n```\n\n![](media/Input-1.gif)\n\n![](media/Input-2.gif)\n\n### `\u003cBanner\u003e`\n\nDisplays big text\n\n##### y?, x?: `number` | `string`\n\n##### background?, color?: `number` | `string`\n\n##### children: `string`\n\n#### Examples\n\n```jsx\n\u003cBanner\u003e{new Date().toTimeString().substring(0, 8)}\u003c/Banner\u003e\n```\n\n![](media/Banner.png)\n\n### `\u003cBar\u003e`\n\nDisplays vertical or horizontal bar with 1/8 character resolution\n\n##### type: `'vertical'` | `'horizontal'`\n\n##### y \u0026 height, x \u0026 width: `number`\n\n#### Examples\n\n```jsx\n\u003c\u003e\n  {[...Array(24)].map((_, index) =\u003e (\n    \u003cBar key={index} type=\"vertical\" x={index * 2} height={(index + 1) / 8} /\u003e\n  ))}\n\u003c/\u003e\n```\n\n![](media/Bar-1.png)\n\nCompare to `\u003cText\u003e`\n\n![](media/Bar-2.gif)\n\n### `\u003cBlock\u003e`\n\nAligns content\n\n##### width?: `number`\n\n##### align?: `'left'` | `'center'` | `'right'` = `'left'`\n\n#### Examples\n\n```jsx\n\u003cBlock\u003eleft\u003c/Block\u003e\n\u003cBlock align=\"center\"\u003ecenter\u003c/Block\u003e\n\u003cBlock align=\"right\"\u003eright\u003c/Block\u003e\n```\n\n![](media/Block.png)\n\n### `\u003cCanvas\u003e`\n\nCreate a canvas for drawing with one these modes\n\n##### mode: `{ h: 1, w: 1 }` | `{ h: 2, w: 1 }` | `{ h: 2, w: 2 }` | `{ h: 4, w: 2 }`\n\nPixels per character\n\n##### height, width: `number`\n\nSize in pixels\n\n##### children: (`Point` | `Line`)`[]`\n\n#### `\u003cPoint\u003e`\n\nDraws a point at the coordinates\n\n##### y, x: `number`\n\n##### color?: `number` | `string`\n\n#### `\u003cLine\u003e`\n\nDraws a line using coordinates\n\n##### y, x, dy, dx: `number`\n\n##### color?: `number` | `string`\n\n#### Examples\n\n```jsx\n\u003cCanvas width={80} height={6}\u003e\n  \u003cPoint x={1} y={1} color=\"Yellow\" /\u003e\n  \u003cLine x={0} y={5} dx={79} dy={0} /\u003e\n\u003c/Canvas\u003e\n```\n\n![](media/Canvas-1.png)\n\nBraille's font demo (`{ h: 4, w: 2 }`)\n\n![](media/Canvas-2.png)\n\n### `\u003cFrame\u003e`\n\nDraws frame around its content\n\n##### children: `string`\n\n##### type?: `'single'` | `'double'` | `'rounded'` = `'single'`\n\n##### height?, width?: `number`\n\n#### Examples\n\n```jsx\n\u003cFrame type=\"single\" color=\"Red\"\u003esingle border type\u003c/Frame\u003e\n\u003cFrame type=\"double\" color=\"Green\" y={0}\u003edouble border type\u003c/Frame\u003e\n\u003cFrame type=\"rounded\" color=\"Blue\" y={0}\u003erounded border type\u003c/Frame\u003e\n```\n\n![](media/Frame.png)\n\n### `\u003cList\u003e`\n\nCreates a list with navigation support\\\nVim shortcuts are supported\n\n##### focus?: `boolean`\n\n##### initialPos?: { y: `number` }\n\n##### data?: `any[]`\n\n##### renderItem?: `(object) =\u003e JSX.Element`\n\n##### height?, width?: `number`\n\n##### scrollbar?: `boolean`\n\n##### scrollbarBackground?: `boolean`\n\n##### scrollbarColor?: `boolean`\n\n##### vi?: `boolean` = `true`\n\n##### pass?: `any`\n\n##### onChange?: `(object) =\u003e void`\n\n##### onSubmit?: `(object) =\u003e void`\n\n#### Examples\n\n```jsx\nconst items = [...Array(8)].map((_, index) =\u003e ({ id: index + 1, title: `Task ${index + 1}` }))\nreturn (\n  \u003cList\n    data={items}\n    renderItem={({ item, selected }) =\u003e \u003cText color={selected ? 'Green' : undefined}\u003e{item.title}\u003c/Text\u003e}\n  /\u003e\n)\n```\n\n![](media/List.gif)\n\n### `\u003cListTable\u003e`: `\u003cList\u003e`\n\nCreates a table with navigation support\\\nVim shortcuts are supported\n\n##### mode?: `'cell'` | `'row'` = `'cell'`\n\n##### head?: `any[]`\n\n##### renderHead?: `(object) =\u003e JSX.Element`\n\n##### data?: `any[][]`\n\n#### Examples\n\n```jsx\nconst head = ['id', 'title']\nconst items = [...Array(8)].map((_, index) =\u003e [index + 1, `Task ${index + 1}`])\nreturn (\n  \u003cListTable\n    head={head}\n    renderHead={({ item }) =\u003e\n      item.map((i, key) =\u003e (\n        \u003cText key={key} width={8}\u003e\n          {i}\n        \u003c/Text\u003e\n      ))\n    }\n    data={items}\n    renderItem={({ item, x, y, index }) =\u003e\n      item.map((text, key) =\u003e (\n        \u003cText key={key} color={y === index \u0026\u0026 x === key ? 'Green' : undefined} width={8}\u003e\n          {text}\n        \u003c/Text\u003e\n      ))\n    }\n  /\u003e\n)\n```\n\n![](media/ListTable.gif)\n\n### `\u003cScrollbar\u003e`\n\nDraws a scrollbar with 1/8 character resolution\n\n##### type?: `'vertical'` | `'horizontal'` = `'vertical'`\n\n##### offset: `number`\n\n##### limit: `number`\n\n##### length: `number`\n\n##### background?, color?: `number` | `string`\n\n#### Examples\n\n```jsx\n\u003cScrollbar type=\"horizontal\" offset={10} limit={80} length={160} /\u003e\n```\n\n![](media/Scrollbar.png)\n\n### `\u003cSeparator\u003e`\n\nDraws a vertical or horizontal line\n\n##### type: `'vertical'` | `'horizontal'`\n\n##### height, width: `number`\n\n#### Examples\n\n```jsx\n\u003cSeparator type=\"vertical\" height={3} /\u003e\n\u003cSeparator type=\"horizontal\" y={1} x={1} width={79} /\u003e\n```\n\n![](media/Separator.png)\n\n### `\u003cSpinner\u003e`\n\nDraws an animated spinner\n\n##### children?: `string`\n\n#### Examples\n\n```jsx\n\u003cSpinner block /\u003e\n\u003cSpinner color=\"BrightGreen\"\u003e-\\|/\u003c/Spinner\u003e\n```\n\n![](media/Spinner.gif)\n\n### `\u003cView\u003e`\n\nCreates a scrollable viewport\\\nVim shortcuts are supported\n\n##### focus?: `boolean`\n\n##### height?: `number`\n\n##### scrollbar?: `boolean`\n\n##### vi?: `boolean` = `true`\n\n##### children: `any`\n\n#### Examples\n\n```jsx\n\u003cView\u003e{JSON.stringify(json, null, 2)}\u003c/View\u003e\n```\n\n![](media/View.gif)\n\n## hooks\n\n### `useAnimation`\n\n##### (time: `number`, fps?: `'number'` = `60`) =\u003e `object`\n\nCreates a timer for a specified duration\\\nThat gives you time and interpolation functions each frame of animation\n\n#### return\n\n##### ms: `number`\n\n##### interpolate: (from: `number`, to: `number`, delay?: `number`)\n\n##### interpolateColor: (from: `string`, to: `string`: delay?: `number`)\n\n#### Examples\n\n```jsx\nconst { ms } = useAnimation(1000, 4)\nreturn ms // 0, 250, 500, 750, 1000\n```\n\n```jsx\nconst { interpolate } = useAnimation(1000, 4)\nreturn interpolate(0, 80) // 0, 20, 40, 60, 80\n```\n\n```jsx\nconst { interpolateColor } = useAnimation(1000, 4)\nreturn interpolateColor('#000', '#0f8') // #000, #042, #084, #0c6, #0f8\n```\n\n![](media/useAnimation.gif)\n\n#### `\u003cTrail\u003e`\n\nMutate array of items to show one by one with latency\n\n##### delay: `number`\n\n##### children: `JSX.Element[]`\n\n#### Examples\n\n```jsx\nconst items = [...Array(8)].map((_, index) =\u003e ({ id: index + 1, title: `Task ${index + 1}` }))\nreturn (\n  \u003cTrail delay={100}\u003e\n    {items.map(({ id, title }) =\u003e (\n      \u003cText key={id} block\u003e\n        {title}\n      \u003c/Text\u003e\n    ))}\n  \u003c/Trail\u003e\n)\n```\n\n![](media/Trail.gif)\n\n#### `useTrail`\n\n##### (delay: `number`, items: `JSX.Element[]`, key?: `string` = `'key'`) =\u003e `JSX.Element[]`\n\nSame as `\u003cTrail\u003e` but hook\\\nYou can pass it to `data` property of `\u003cList\u003e` component for example\n\n#### Examples\n\n```jsx\n\u003cList data={useTrail(items)} /\u003e\n```\n\n### `useChildrenSize`\n\n##### (value: `string`) =\u003e `object`\n\nGives you content size\n\n#### return\n\n##### height, width: `number`\n\n#### Examples\n\n```jsx\nuseChildrenSize('1\\n22\\n333') // { height: 3, width: 3 }\n```\n\n### `useClipboard`\n\n#### () =\u003e `array`\n\nAllows you to work with the system clipboard\n\n#### return\n\n##### getClipboard: `() =\u003e string`\n\n##### setClipboard: `(value: string) =\u003e void`\n\n#### Examples\n\n```jsx\nconst { getClipboard, setClipboard } = useClipboard()\nconst string = getClipboard()\nsetClipboard(string.toUpperCase()) // copied\n```\n\n### `useInput`\n\n##### (callback: `(string) =\u003e void`, dependencies: `any[]`) =\u003e `void`\n\nAllows you to handle keyboard input\n\n#### Examples\n\n```jsx\nset[(counter, setCounter)] = useState(0)\n\nuseInput(\n  input =\u003e {\n    if (input === 'k') setCounter(counter + 1)\n    if (input === 'j') setCounter(counter - 1)\n  },\n  [counter]\n)\n```\n\n### `useMouse`\n\n##### (callback: `(object) =\u003e void`, dependencies: `any[]`)\n\nAllows you to handle mouse input\n\n#### Examples\n\n```jsx\nset[(counter, setCounter)] = useState(0)\n\nuseMouse(\n  event =\u003e {\n    if (event.type === 'wheelup') setCounter(counter + 1)\n    if (event.type === 'wheeldown') setCounter(counter - 1)\n  },\n  [counter]\n)\n```\n\n### `useSize`\n\n##### () =\u003e `object`\n\nGives you terminal size\\\nUpdates when size is changing\n\n#### return\n\n##### height, width: `number`\n\n#### Examples\n\n```jsx\nuseSize() // { height: 24, width: 80 }\n```\n\n### `useWordWrap`\n\n##### (text: `string`, width?: `number`) =\u003e `object`\n\nGives your text a word wrap\n\n#### return\n\n##### height, width: `number`\n\n#### Examples\n\n```jsx\nuseWordWrap('hello world', 5) // hello\\nworld\n```\n\n## API\n\n### `render` (children: `JSX.Element`) =\u003e `void`\n\nRenders your fullscreen application to `stdout`\n\n### `inline` (children: `JSX.Element`) =\u003e `void`\n\nRenders your inline application to `stdout`\n\n### `bell`\n\n#### () =\u003e `void`\n\nMakes a terminal bell\n\n```jsx\nbell() // ding\n```\n\n### `exit`\n\n##### (code: `number` = `0`) =\u003e `void`\n\nAllows you to exit from an application that waits for user input or has timers\n\n#### Examples\n\n```jsx\nuseInput(input =\u003e {\n  if (input === 'q') exit()\n})\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finfely%2Freact-curse","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Finfely%2Freact-curse","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finfely%2Freact-curse/lists"}