README
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
const { atom, label, renderer } = require('@acransac/terminal');
const [render, terminate] = renderer();
render(label(atom("abc"), "example"));
setTimeout(terminate, 2000);
$ node example.js
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
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
const { atom, column, cons, renderer } = require('@acransac/terminal');
const [render, terminate] = renderer();
render(cons(atom("abc"), column(50)));
setTimeout(terminate, 2000);
$ node example.js
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
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
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
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
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 tocompose
a handle to the parameters and output of the formf => 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 byrenderer
|
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
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: callsrenderer
|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
$ 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
$ node example.js save
$ node example.js control
--------------------
Example Tests:
1 / 1 test(s) passed
--------------------