Thanks for checking out react-tui
. This library wouldn't exist without the
fantastic work from the following projects:
So why another library? Most importantly, to improve the usability of react-blessed
and blessed. Blessed is an imperative API for building terminal UIs.
It is unopinionated on application structure or architecture (MVC, MVVM, MVP, etc),
which lets us pair nicely with react. React can be adapted to various architectures,
including those that popularized react, letting UI be a declartive, pure function of a data model. react-blessed
took a stab at marrying those domains, and tried an
approach that took react props and passed them directly into blessed widget
contructors, or modified blessed widget attributes on update. In fact, that's
still what were doing here in react-tui
, but with more. What more?
Typescript
support. Blessed APIs are wide, sometimes redundant, and hard to piece together for newcomers. By applying Typescript, your react-tui/blessed prop inputs will be checked, to a moderate extent. You also gain some discoverability of how to use components without leaving your editor.- First class component API. This component API will make it easier to to import building blocks into your react app, and discover APIs within them. It also adds support and workarounds for various blessed-react compatibility issues.
- Various utilities to make blessed/terminal/react interop a bit less painless, such as coloring and text formatting.
Aside for @types/react users--JSX.IntrinsicElements
are not augmented by react-tui
.
The Typescript community early on assumed that react would only be used
in web, so currently the prop
defitions for web ship with @types/react
versus @types/react-dom
. Consequently,
we cannot give first class type definitions for what we really pass into the
react-reconciler (e.g. "text"
, "textarea"
, etc) as they conflict with web types.
Fear not--the exported component library has all of typescript goodies
worked out fine 👌.
See the readme.md for how to install react-tui into your project.
If you are a web developer or a react-native developer, developing with react-tui will be challenging at first. Your terminal does not have an excellent box model like we are used to in those environments, and available API implementations in this space are both more limited and subjectively less robust. Understanding what react-tui is doing behind the scenes will help you.
react-tui
ships a component library. It's important to be aware that
components in react-tui
are:
- generally abstractions over blessed::Widgets
- children of a parent blessed::Widget
- rendered inside of a blessed::Screen
import { Box } from "@dino-dna/react-tui/components";
render(<Box {...props} />);
/* ^ eventually yields => */ blessed.box(props);
Much like in the browser when using react, we need an initial, target element
to render into. So too is the case with react-tui
.
Users are responsible for:
- creating a screen
- creating an element on the screen to mount your react application into
// demo.tsx
import React from "react";
import blessed from "neo-blessed";
import { createBlessedRenderer } from "@dino-dna/react-tui";
const screen = blessed.screen({
/* ... */
});
screen.key(["q", "C-c"], () => process.exit(0));
const render = createBlessedRenderer(blessed, screen);
const container = blessed.box();
screen.append(container);
// preparation complete!
// now you are free to pass control off to react!
render(<>Greetings from react-tui</>, container);
// ^look familiar?
// This is roughly the same API as ReactDOM.render(el, container)
As mentioned before, blessed does not have a flowing box model under the hood. What happens if you just add a bunch of boxes to your app?
// boxes-on-boxes
import { Box } from "@dino-dna/react-tui/components";
render(
<>
{[...Array(10)].map(_, i) => <Box>{`greetings from box: ${i}`}</Box>}
</>
)
What's even happening here? All of the boxes are stacking atop one another.
Let's try again, using the top
prop. This will tell how many lines offset
from the top to paint the new node.
// boxes-with-top
import { Box } from "@dino-dna/react-tui/components";
render(
<>
{[...Array(10)].map(_, i) => <Box top={i}>greetings from box: {i}</Box>}
</>
)
Much better. Let's try a more interesting layout.
// verbose-4x4
import { Element, Box } from "@dino-dna/react-tui/components";
render(
<Element>
<Box left={0} top={0} width="50%" children={1} />
<Box left="50%" top={0} width="50%" children={2} />
<Box left={0} top="50%" width="50%" children={3} />
<Box left="50%" top="50%" width="50%" children={4} />
</Element>
);
If this feels like writing old-skool HTML <table />
s, that's because it is.
And it's not so bad either! In a hot second, we'll see how we can reduce
the amount of boilerplate and math you may want to do, especially to support
colspan and rowspans. But first, let's look deeper at box widths. Using the same
example, let's apply border styles.
// verbose-4x4-border-bros
import { Element, Box } from "@dino-dna/react-tui/components";
const styles = {
border: { type: "line" },
style: { border: { fg: "blue" } },
};
render(
<Element>
<Box left={0} top={0} width="50%" {...styles}>
1
</Box>
<Box left="50%" top={0} width="50%" {...styles}>
2
</Box>
<Box left={0} top="50%" width="50%" {...styles}>
3
</Box>
<Box left="50%" top="50%" width="50%" {...styles}>
4
</Box>
</Element>
);
Hmm. Not bad. But clearly not right. Where's that darned bottom border?
What if we specify a height="50%"
on each node? Maybe a label
as well?
Cool. Cool-Tools™.
But who ever has a layout that is a just a 4x4 grid?
More often, you'll want some sort of variable width panes or spanning action. That's where
<Grid />
comes in. Grid asks that you setup all possible square sections,
then lets you snap components into those sections. It's not as snazzy as CSS
Grid, but it is better than doing the math on your own. Let's make a view with
three sections. Section 1, top-left, 1/3 wide, 1/2 tall. Section 2, middle-left,
1/3 wide, 1/2 tall. Section 3, starting at 1/3 left, 2/3 wide, full height.
That may be hard to grok, so let's see the output first this time:
import { Element, Box, Grid } from "@dino-dna/react-tui/components";
render(
<Grid
cols={3} // allow us to have 1/3 increment widths
rows={2} // allow us to have 1/2 increment heights
items={[
{
row: 0,
col: 0,
render: (props) => (
// props have all of our positioning data precomputed. pass em thru!
<Element {...props} {...styles} children="Section 1" />
),
},
{
row: 1,
col: 0,
render: (props) => (
<Element {...props} {...styles} children="Section 2" />
),
},
{
row: 0,
col: 1,
rowSpan: 2,
colSpan: 2,
render: (props) => (
<Element {...props} {...styles} children="Section 3" />
),
},
]}
/>
);
It's a bit verbose. Ideas on improved APIs? Send a patch ;).
Now things get interesting. react-tui
offloads all aspects of interactivity
to blessed. When any question arises or quirk is observed using the interactivity APIs,
it's recommendend to go straight to the blessed
docs, and study the matching
prop/attribute.
The main way users interact with TUIs is their keys. Keys are used for content input and navigation. Let's look at the macro interactive modes and what you may need to do to enable them in your react components.
- Accepting key input
keys
prop - If you want the user to be able to interact with you component you ought apply thekeys
prop to the component. It is not universal on all elements, but common.- You may expect
<Textbox label="textbox" />
to allow for character input on its own--but infact it does not. - This is in stark contrast to web apis, where input controls (e.g.
<input type="text" />
) are by default interactable on focus. - When the user focuses your::
- input element: the
return/enter
key puts the user into text input mode. e.g. you can enter text into a<Textbox />
.Esc
restores the prior tab focus. - non-input element: expect desired key behavior on the focused node. e.g. you should be able scroll a scrollable
<List />
with arrow keys if content is available.
- input element: the
- You may expect
inputOnFocus
- may be applied to input controls- On focus, the user is automatically now entering text
- Consider using this sparingly. it can be jarring to a user tabbing through a UI and then suddent stopped from navigation--the tab is entered instead as a proper tab.
inputOnFocus
is probably best suited for usage only in<Form />
s.
- Tab focusing
- Various props make a node focusable.
keys
or example. Can be controlled and contained within a box, butreact-tui
provides no HOCs (yet?) to help control that behavior. Usingref
s, you can hook into blessed nodes to do any sort of special focus customization you may need.
- Various props make a node focusable.
- Mouse/Scroll
mouse
- a common prop analogous tokeys
, but for mouse input. Scrolling and focus can now occur additionally via mouse.scrollable
- a common prop to allow overflowed contents to be accessible.keys
ormouse
ought generally be appliend in tandem to access such content.
- Communicate focus on interactive views.
- In some cases, a blinking cursor is enough. In bordered views, consider changing attributes on focus:
{
border: { type: "line" },
style: {
border: { fg: "blue" },
focus: {
border: { fg: "red" },
},
},
}
REACT_TUI_DEBUG_LOG
- set to a filename, e.g.debug.log
to turn on various streaming log datas.
The minimal covered content above should enable you to be somewhat productive. Other, omitted key concepts are covered in the associated technology docs.
blessed
is great, but it is not actively maintained. neo-blessed
claims maintenance, and
does exhibit some activity, but isn't necessarily a flourishing community either.
No ill will indended, of course :).
Long term, decoupling our component API from blessed widgets and use lower level blessed's primitives will improve some issues. We may refactor non-widget code from blessed into Typescript and help contribute to move that community forward. We may consider dropping blessed outright and seeing our react components play nicely with Cedric's WIP Document Model. TBD. You tell us what you think--let's do it together! :)