react-curse
A curses-like blazingly fast react renderer
- 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:
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 - Database manager supports mongodb, mysql/mariadb, postgresql, sqlite and json-server
- nfi - Simple nerd fonts icons cheat sheet that allows you to quickly find and copy glyph to clipboard
Installation
Just run npm init react-curse
answer a few questions and you are ready to go
Examples
Hello world
import React from 'react'
import ReactCurse, { Text } from 'react-curse'
const App = ({ text }) => {
return <Text color="Red">{text}</Text>
}
ReactCurse.render(<App text="hello world" />)
How to handle input
import React, { useState } from 'react'
import ReactCurse, { Text, useInput, useExit } 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') useExit()
},
[counter]
)
return (
<Text>
counter: <Text bold>{counter}</Text>
</Text>
)
}
ReactCurse.render(<App />)
How to animate
import React from 'react'
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 />)
Contents
Components
<Text>
Base component
The only component required to do anything
Every other component uses this one to draw
number
| string
y?, x?: 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'
number
| string
height?, width?: Size of block, will be cropped by parent
See absolute
to avoid this behavior
boolean
absolute?: Makes position and size ignoring parent container
number
| string
background?, color?: Background and foreground color
Example: 31, 'Red', '#f04020', '#f42'
boolean
clear?: Clears block before drawing content
height
and width
boolean
block?: Moves cursor to a new line after its content relative to parent
boolean
bold?, dim?, italic?, underline?, blinking?, inverse?, strikethrough?: Text modifiers
Examples
<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>
<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
boolean
= true
focus?: Makes it active
'text'
| 'password'
| 'hidden'
= ‘text'
type?: string
initialValue?: number
| string
cursorBackground?: () => void
onCancel?: (string) => void
onChange?: (string) => void
onSubmit?: Examples
<Input background="#404040" height={1} width={8} />
<Banner>
Displays big text
number
| string
y?, x?: number
| string
background?, color?: string
children: Examples
<Banner>{new Date().toTimeString().substring(0, 8)}</Banner>
<Bar>
Displays vertical or horizontal bar with 1/8 character resolution
'vertical'
| 'horizontal'
type: number
y & height, x & width: Examples
<>
{[...Array(24)].map((_, index) => (
<Bar key={index} type="vertical" x={index * 2} height={(index + 1) / 8} />
))}
</>
Compare to <Text>
<Block>
Aligns content
number
width?: 'left'
| 'center'
| 'right'
= 'left'
align?: Examples
<Block>left</Block>
<Block align="center">center</Block>
<Block align="right">right</Block>
<Canvas>
Create a canvas for drawing with one these modes
{ h: 1, w: 1 }
| { h: 2, w: 1 }
| { h: 2, w: 2 }
| { h: 4, w: 2 }
mode: Pixels per character
number
height, width: Size in pixels
Point
| Line
)[]
children: (<Point>
Draws a point at the coordinates
number
y, x: number
| string
color?: <Line>
Draws a line using coordinates
number
y, x, dy, dx: number
| string
color?: Examples
<Canvas width={80} height={6}>
<Point x={1} y={1} color="Yellow" />
<Line x={0} y={5} dx={79} dy={0} />
</Canvas>
Braille's font demo ({ h: 4, w: 2 }
)
<Frame>
Draws frame around its content
string
children: 'single'
| 'double'
| 'rounded'
= 'single'
type?: number
height?, width?: Examples
<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>
<List>
Creates a list with navigation support
Vim shortcuts are supported
boolean
focus?: number
}
initialPos?: { y: any[]
data?: (object) => JSX.Element
renderItem?: number
height?, width?: boolean
scrollbar?: boolean
scrollbarBackground?: boolean
scrollbarColor?: boolean
= true
vi?: any
pass?: (object) => void
onChange?: (object) => void
onSubmit?: Examples
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>}
/>
)
<ListTable>
: <List>
Creates a table with navigation support
Vim shortcuts are supported
'cell'
| 'row'
= 'cell'
mode?: any[]
head?: (object) => JSX.Element
renderHead?: any[][]
data?: Examples
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>
))
}
/>
)
<Scrollbar>
Draws a scrollbar with 1/8 character resolution
'vertical'
| 'horizontal'
= 'vertical'
type?: number
offset: number
limit: number
length: number
| string
background?, color?: Examples
<Scrollbar type="horizontal" offset={10} limit={80} length={160} />
<Separator>
Draws a vertical or horizontal line
'vertical'
| 'horizontal'
type: number
height, width: Examples
<Separator type="vertical" height={3} />
<Separator type="horizontal" y={1} x={1} width={79} />
<Spinner>
Draws an animated spinner
string
children?: Examples
<Spinner block />
<Spinner color="BrightGreen">-\|/</Spinner>
<View>
Creates a scrollable viewport
Vim shortcuts are supported
boolean
focus?: number
height?: boolean
scrollbar?: boolean
= true
vi?: any
children: Examples
<View>{JSON.stringify(json, null, 2)}</View>
hooks
useAnimation
number
, fps?: 'number'
= 60
) => object
(time: Creates a timer for a specified duration
That gives you time and interpolation functions each frame of animation
return
number
ms: number
, to: number
, delay?: number
)
interpolate: (from: string
, to: string
: delay?: number
)
interpolateColor: (from: Examples
const { ms } = useAnimation(1000, 4)
return ms // 0, 250, 500, 750, 1000
const { interpolate } = useAnimation(1000, 4)
return interpolate(0, 80) // 0, 20, 40, 60, 80
const { interpolateColor } = useAnimation(1000, 4)
return interpolateColor('#000', '#0f8') // #000, #042, #084, #0c6, #0f8
<Trail>
Mutate array of items to show one by one with latency
number
delay: JSX.Element[]
children: Examples
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>
)
useTrail
number
, items: JSX.Element[]
, key?: string
= 'key'
) => JSX.Element[]
(delay: Same as <Trail>
but hook
You can pass it to data
property of <List>
component for example
Examples
<List data={useTrail(items)} />
useBell
void
() => Makes a terminal bell
useBell() // ding
useChildrenSize
string
) => object
(value: Gives you content size
return
number
height, width: Examples
useChildrenSize('1\n22\n333') // { height: 3, width: 3 }
useClipboard
array
() => Allows you to work with the system clipboard
return
() => string
getClipboard: (value: string) => void
setClipboard: Examples
const { getClipboard, setClipboard } = useClipboard()
const string = getClipboard()
setClipboard(string.toUpperCase()) // copied
useExit
number
= 0
) => void
(code: Allows you to exit from an application that waits for user input or has timers
Examples
useInput(input => {
if (input === 'q') useExit()
})
useInput
(string) => void
, dependencies: any[]
) => void
(callback: Allows you to handle keyboard input
Examples
set[(counter, setCounter)] = useState(0)
useInput(
input => {
if (input === 'k') setCounter(counter + 1)
if (input === 'j') setCounter(counter - 1)
},
[counter]
)
useMouse
(object) => void
, dependencies: any[]
)
(callback: Allows you to handle mouse input
Examples
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
number
height, width: Examples
useSize() // { height: 24, width: 80 }
useWordWrap
string
, width?: number
) => object
(text: Gives your text a word wrap
return
number
height, width: Examples
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