Skip to content

Latest commit

 

History

History
303 lines (248 loc) · 11.4 KB

manual.md

File metadata and controls

303 lines (248 loc) · 11.4 KB

Manual

Welcome

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 👌.

Getting started

See the readme.md for how to install react-tui into your project.

Understanding components

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:

import { Box } from "@dino-dna/react-tui/components";
render(<Box {...props} />);
/* ^ eventually yields => */ blessed.box(props);

Preparing the TUI

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)

Using react blessed components

Positioning

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 ;).

Interactivity

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 the keys 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.
    • 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, but react-tui provides no HOCs (yet?) to help control that behavior. Using refs, you can hook into blessed nodes to do any sort of special focus customization you may need.
  • Mouse/Scroll
    • mouse - a common prop analogous to keys, 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 or mouse ought generally be appliend in tandem to access such content.

Styling Best-Known-Methods

  • 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" },
    },
  },
}

Debugging

  • REACT_TUI_DEBUG_LOG - set to a filename, e.g. debug.log to turn on various streaming log datas.

What else?

The minimal covered content above should enable you to be somewhat productive. Other, omitted key concepts are covered in the associated technology docs.

The future

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! :)