Data and a layout
An action is its data and a layout over that data. Four ways a layout reads the data, and one way it writes it back.
pnpm add @niscorp/nova @niscorp/prism @niscorp/strata zodThe kit
Nova draws nothing itself. It resolves a layout to a tree, and an adapter’s components draw the tree. This file uses the terminal kit, so it runs anywhere Node does; in an app the registry holds your own components.
A canvas has no look of its own here: it is drawn as whatever stands on it.
import { createShell, shellSettled, shellView } from '@niscorp/nova';
import type { ActionDefinition } from '@niscorp/nova';
import { createTtyView } from '@niscorp/nova/adapters/tty';
import { defaultRegistry } from '@niscorp/nova/adapters/tty/components';
const registry = defaultRegistry();
registry.register('CanvasSlot', ({ children }) => ({ lines: children.flatMap((child) => child.lines) }));An action, as a document
The action is one object: data, and a layout that reads it. A string that starts with $. is a path into the data. A string with {{…}} in it is a template. A node with if draws one branch or the other. And model binds a field both ways: it shows what the data holds, and what is typed is written back.
Nothing in the layout is code. It is resolved against the data, never run.
const pass: ActionDefinition = {
id: 'pass',
data: { show: 'The Tempest', sold: 87, name: '' },
layout: {
component: 'Stack',
children: [
{ component: 'Text', children: '$.show' },
{ component: 'Text', children: '{{$.sold}} of 120 sold' },
{
if: '$.name',
then: { component: 'Text', children: 'For {{$.name}}' },
else: { component: 'Text', children: 'For nobody yet' },
},
{ component: 'Input', ref: 'name', model: '$.name' },
],
},
};A shell, and the first draw
A shell holds canvases, and an action is pushed onto one. shellSettled waits until the shell has nothing left to do, and draw prints the screen as the terminal kit draws it.
const shell = createShell({ canvases: [{ id: 'main' }], registry, actions: { pass } });
shell.push('main', 'pass');
await shellSettled(shell);
const draw = (): void => console.log(createTtyView(registry, shellView(shell).api).render().text);
draw();Type into the field
A component says what happened as an event. Typing is a ui:model carrying the text, and the runtime writes it to the path the field is bound to. The layout is resolved again, and the conditional takes its other branch.
shell.dispatch({ type: 'ui:model', ref: 'name', payload: 'Grace' });
await shellSettled(shell);
draw();Run it.
The same layout, drawn twice. The second time the data holds a name, so the field shows it and the line above it changed.
npx tsx layout.mts── main ──────────────────────────────────────────────────── The Tempest 87 of 120 sold For nobody yet [1] ⟨⟩ ── main ──────────────────────────────────────────────────── The Tempest 87 of 120 sold For Grace [1] ⟨Grace⟩