@acransac/terminal

1.0.1 • Public • Published

Introduction

terminal provides a model to make terminal user interfaces. The user sees a display which is either an atom or a list of displays. An atom is a possibly labelled bordered box with text. Using streamer, a display can react to events generated by the user or the environment.

terminal benefits from the high-level modelling of terminal graphics as a tree introduced by blessed. It also borrows its tools and terminology from the Lisp family of programming languages.

How To Use Terminal

Add terminal to a project with:

    $ npm install @acransac/terminal

and import the needed functionalities:

    const { atom, column, compose, cons, emptyList, indent, inline, label, renderer, row, show, sizeHeight, sizeWidth, TerminalTest, vindent } = require('@acransac/terminal');

Make An Inert Atomic Display

The rendering engine is initialized with renderer. The latter returns functions to render a display and terminate the engine. An atomic display is created with atom and passed to the render function. Then, the engine is terminated. An atom's box can be labelled with label:

  • renderer:: Maybe<Stream.Writable> -> (Display -> (), () -> ())

    Parameter / Returned Type Description
    output Maybe<Stream.Writable> A writable Node.js stream to which the displayed characters and escape sequences are written. Default: process.stdout
    returned (Display -> (), () -> ()) An array of two functions. The first renders on the output the display passed as argument. The second tears down the rendering system
  • atom:: String -> Display

    Parameter Type Description
    content String The text to print within the atom box
  • label:: (Atom<Display>, String) -> Atom<Display>

    Parameter Type Description
    atom Atom<Display> The atom to label
    title String The label's text

Examples:

    const { atom, renderer } = require('@acransac/terminal');

    const [render, terminate] = renderer();

    render(atom("abc"));

    setTimeout(terminate, 2000);
    $ node example.js

Alt text

    const { atom, label, renderer } = require('@acransac/terminal');

    const [render, terminate] = renderer();

    render(label(atom("abc"), "example"));

    setTimeout(terminate, 2000);
    $ node example.js

Alt text

Make An Inert List Display

As mentioned before, terminal's tooling inherits the fundamental notions of the Lisp dialects. A display is a recursive structure expressed as nested lists of displays. It is built by cons'ing displays onto lists, ending such ramified chains with the emptyList. The latter can be dimensioned in height or in width with row or column:

  • cons:: (Display, List<Display>) -> List<Display>

    Parameter Type Description
    display Display The display, atom or list, to prepend
    list List<Display> The list of displays to prepend to
  • emptyList:: () -> List<Display>

    Returned Type Description
    returned List<Display> A list display printing nothing. The starting point to which useful displays are prepended
  • row:: Number -> List<Display>

    Parameter Type Description
    height Number The empty list's height proportionally to the underlying list's height if any. Otherwise, it is proportional to the screen's height. Expressed in percentage
  • column:: Number -> List<Display>

    Parameter Type Description
    width Number The empty list's width proportionally to the underlying list's width if any. Otherwise, it is proportional to the screen's width. Expressed in percentage

Examples:

    const { atom, cons, emptyList, renderer } = require('@acransac/terminal');

    const [render, terminate] = renderer();

    render(cons(atom("abc"), emptyList()));

    setTimeout(terminate, 2000);
    $ node example.js

Alt text

Note: The last example display looks the same as the first one, except that it is a list and not an atom.

    const { atom, cons, renderer, row } = require('@acransac/terminal');

    const [render, terminate] = renderer();

    render(cons(atom("abc"), row(50)));

    setTimeout(terminate, 2000);
    $ node example.js

Alt text

    const { atom, column, cons, renderer } = require('@acransac/terminal');

    const [render, terminate] = renderer();

    render(cons(atom("abc"), column(50)));

    setTimeout(terminate, 2000);
    $ node example.js

Alt text

Size And Position Displays

sizeWidth and sizeHeight are used to size atoms. indent and vindent position displays. They allow to make list displays with several atoms or lists visible. Also, inline places a list of atoms in a row:

  • sizeWidth:: (Number, Atom<Display>) -> Atom<Display>

    Parameter Type Description
    size Number The atom's width proportionally to the underlying list's width if any. Otherwise, it is proportional to the screen's width. Expressed in percentage
    atom Atom<Display> The atom to resize
  • sizeHeight:: (Number, Atom<Display>) -> Atom<Display>

    Parameter Type Description
    size Number The atom's height proportionally to the underlying list's height if any. Otherwise, it is proportional to the screen's height. Expressed in percentage
    atom Atom<Display> The atom to resize
  • indent:: (Number, Display) -> Display

    Parameter Type Description
    offset Number The display's offset from the underlying list's left edge if any. Otherwise, it is from the screen's left edge. Expressed in percentage of the underlier's width
    display Display The display to move
  • vindent:: (Number, Display) -> Display

    Parameter Type Description
    offset Number The display's offset from the underlying list's top edge if any. Otherwise, it is from the screen's top edge. Expressed in percentage of the underlier's height
    display Display The display to move
  • inline:: List<Atom<Display>> -> Display

    Parameter Type Description
    lat List<Atom<Display>> A display that is a list of atoms with specified widths that are to be indented so that they show in a row

Examples:

    const { atom, indent, renderer, sizeHeight, sizeWidth, vindent } = require('@acransac/terminal');

    const [render, terminate] = renderer();

    render(vindent(25, indent(25, sizeHeight(50, sizeWidth(50, atom("abc"))))));

    setTimeout(terminate, 2000);
    $ node example.js

Alt text

    const { atom, cons, emptyList, indent, renderer, vindent } = require('@acransac/terminal');

    const [render, terminate] = renderer();

    render(vindent(10, indent(10, cons(atom("abc"), emptyList()))));

    setTimeout(terminate, 2000);
    $ node example.js

Alt text

    const { atom, cons, emptyList, indent, renderer, sizeWidth } = require('@acransac/terminal');

    const [render, terminate] = renderer();

    render(cons(sizeWidth(50, atom("abc")),
                cons(indent(50, sizeWidth(50, atom("def"))),
                     emptyList())));

    setTimeout(terminate, 2000);
    $ node example.js

Alt text

    const { atom, cons, emptyList, inline, renderer, sizeWidth } = require('@acransac/terminal');

    const [render, terminate] = renderer();

    render(inline(cons(sizeWidth(50, atom("abc")),
                       cons(sizeWidth(50, atom("def")),
                            emptyList()))));

    setTimeout(terminate, 2000);
    $ node example.js

Alt text

Make A Reactive Display

A reactive display changes as the user interacts with the application or network messages are received, for example. terminal uses the streamer framework to listen to events and a reactive display is integrated into the process attached to the source. This integration is borne by compose which distributes the stream to a series of components and injects their output into a template. compose is immediately chained with the function generated by passing the render function to show and that executes the rendering of the injected template:

  • Component:: Any... -> Any -> Stream -> ComposerHandle

    Parameter / Returned Type Description
    parameters Any... The state of the component
    predecessor Any The output of the component on the last event processed
    returned Stream -> ComposerHandle The logic of the component which processes the stream and returns to compose a handle to the parameters and output of the form f => f(updatedParameters)(output). Using this handle, compose provides the parameters and predecessor to the component on the next event, and injects the ouput into the template
  • Template:: Component... -> Display

    Parameter Type Description
    components Component... A series of components whose output values are passed as arguments to the template
  • compose:: (Template, Component...) -> Composer

    Parameter Type Description
    template Template The template organizing the display
    reactiveComponents Component... A sequence of components parameterizing the template
  • show:: Display -> () -> Composer -> Process

    Parameter Type Description
    render Display -> () The render function returned by renderer

Example:

    const { continuation, forget, later, now, Source, StreamerTest, value } = require('@acransac/streamer');
    const { atom, compose, renderer, show } = require('@acransac/terminal');

    const [render, terminate] = renderer();

    const loop = async (stream) => {
      if (value(now(stream)) === "end") {
        return terminate();
      }
      else {
        return loop(await continuation(now(stream))(forget(await later(stream))));
      }
    };

    const template = component => atom(component);

    const component = noParameters => predecessor => stream => {
      const processed = predecessor ? predecessor : "";

      if (value(now(stream)) === "end") {
        return f => f(noParameters)(processed);
      }
      else {
        return f => f(noParameters)(`${processed}${value(now(stream))}`);
      }
    };

    Source.from(StreamerTest.emitSequence(["a", "b", "c", "end"], 1000), "onevent")
          .withDownstream(async (stream) => loop(await show(render)(compose(template, component))(stream)));
    $ node example.js

Alt text

Test The Display

TerminalTest.reviewDisplays introduces testing a sequence of displays. A display test is created with makeTestableInertDisplay or makeTestableReactiveDisplay. When run, a script executing this review accepts one command line option: look shows the displays in turn so that they can be visually checked. save writes the characters and escape sequences of the displays to files so that they can be programmatically verified. control runs the actual test, comparing the generated displays to the control files and logging the successes and failures. It is the default option:

  • TerminalTest.reviewDisplays:: ([TestableDisplay], Maybe<String>) -> ()

    Parameter Type Description
    testableDisplays [TestableDisplay] An array of testable displays, whether inert or reactive
    testSuiteName Maybe<String> The name of the test suite that appears in logs. Default: Test Suite
  • TerminalTest.makeTestableInertDisplay:: (() -> Display, String) -> TestableDisplay

    Parameter Type Description
    display () -> Display A deferred inert display
    testName String The name of the test that appears in logs
  • TerminalTest.makeTestableReactiveDisplay:: (ReactiveDisplayTest, String, Maybe<TestInitializer>) -> TestableDisplay

    Parameter Type Description
    produceDisplay ReactiveDisplayTest The test function displaying what is to be checked. Its signature depends on the setup done by the initializer. By default, it should provide a placeholder for the render function as first argument, and then a placeholder for the continuation of the test suite. The test runner provides the actual functions when executing
    testName String The name of the test that appears in logs
    init Maybe<TestInitializer> Logic to execute before running the test. Default: calls renderer
  • TestInitializer:: (Stream.Writable, ReactiveDisplayTest, () -> ()) -> ()

    Parameter Type Description
    displayTarget Stream.Writable A reference to the target for writing the characters and escape sequences of the display. It could be a file or the standard output depending on the mode of the test runner
    test ReactiveDisplayTest A reference to the test function to call once the initialization is done
    finish () -> () A reference to the continuation of the test to call once the test is done

Examples:

    const { continuation, forget, later, now, Source, StreamerTest, value } = require('@acransac/streamer');
    const { atom, compose, show, TerminalTest } = require('@acransac/terminal');

    function reactiveDisplayTest(render, finish) {
      const loop = async (stream) => {
        if (value(now(stream)) === "end") {
          return finish();
        }
        else {
          return loop(await continuation(now(stream))(forget(await later(stream))));
        }
      };

      const template = component => atom(component);

      const component = noParameters => predecessor => stream => {
        const processed = predecessor ? predecessor : "";

        if (value(now(stream)) === "end") {
          return f => f(noParameters)(processed);
        }
        else {
          return f => f(noParameters)(`${processed}${value(now(stream))}`);
        }
      };

      Source.from(StreamerTest.emitSequence(["a", "b", "c", "end"], 1000), "onevent")
            .withDownstream(async (stream) => loop(await show(render)(compose(template, component))(stream)));
    }

    TerminalTest.reviewDisplays([
      TerminalTest.makeTestableInertDisplay(() => atom("abc"), "Inert Display"),
      TerminalTest.makeTestableReactiveDisplay(reactiveDisplayTest, "Reactive Display")
    ], "Example Tests");
    $ node example.js look

Alt text

    $ node example.js save
    $ node example.js control
    --------------------
    Example Tests:

        2 / 2 test(s) passed
    --------------------
    const { continuation, forget, later, now, Source, StreamerTest, value } = require('@acransac/streamer');
    const { atom, compose, renderer, show, TerminalTest } = require('@acransac/terminal');

    function reactiveDisplayTest(render, finish) {
      const loop = async (stream) => {
        if (value(now(stream)) === "end") {
          return finish();
        }
        else {
          return loop(await continuation(now(stream))(forget(await later(stream))));
        }
      };

      const template = component => atom(component);

      const component = noParameters => predecessor => stream => {
        const processed = predecessor ? predecessor : "";

        if (value(now(stream)) === "end") {
          return f => f(noParameters)(processed);
        }
        else {
          return f => f(noParameters)(`${processed}${value(now(stream))}`);
        }
      };

      return async (stream) => loop(await show(render)(compose(template, component))(stream));
    }

    function testInitializer(displayTarget, test, finish) {
      const [render, terminate] = renderer(displayTarget);

      const conclude = () => {
        terminate();

        return finish();
      }

      Source.from(StreamerTest.emitSequence(["a", "b", "c", "end"], 1000), "onevent")
                              .withDownstream(async (stream) => test(render, conclude)(stream));
    }

    TerminalTest.reviewDisplays([
      TerminalTest.makeTestableReactiveDisplay(reactiveDisplayTest,
                                               "Reactive Display With Initializer",
                                               testInitializer)
    ], "Example Tests");
    $ node example.js look

Alt text

    $ node example.js save
    $ node example.js control
    --------------------
    Example Tests:

        1 / 1 test(s) passed
    --------------------

Readme

Keywords

none

Package Sidebar

Install

npm i @acransac/terminal

Weekly Downloads

1

Version

1.0.1

License

MIT

Unpacked Size

737 kB

Total Files

35

Last publish

Collaborators

  • acransac