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 to compose
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 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
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
$ 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
--------------------