Layout
Rows and columns of panes, sized in cells, by fraction with a minimum, or to fit their content — integer arithmetic, no flexbox, and what happens when the terminal is too small.
A layout is a tree. A leaf is a pane's name; a split is a direction and its parts:
{
direction: 'column', // parts top to bottom; 'row' is left to right
parts: [
{ size: 1, content: 'tabs' },
{ content: { direction: 'row', parts: [ /* … */ ] } },
],
}A part's size is one of three things along its parent's direction:
| size | what the part gets |
|---|---|
| a number | exactly that many cells |
{ fr, min } | a share of what the fixed and fitted parts leave, never below min |
'fit' | as many cells as the pane's content needs: its line count in a column, its widest line in a row |
A part with no size is { fr: 1 }. Only a pane can fit; a split has no content of its own.
Widths are measured with linegauge, so colour codes take no cells and a wide character
takes two.
Seeing the arithmetic
layout(tree, area, contents?) is the function open() calls on every paint, exported so you
can see what it decides. It returns a rectangle per pane:
import { layout } from 'controlroom';
const tree = {
direction: 'column',
parts: [
{ size: 1, content: 'tabs' },
{ content: { direction: 'row', parts: [{ size: { fr: 1, min: 20 }, content: 'learn' }, { size: { fr: 2 }, content: 'tasks' }] } },
{ size: 'fit', content: 'status' },
{ size: 1, content: 'hints' },
],
};
// What `'fit'` measures: the status pane's lines.
const contents = { status: ['Progress: 1/4 completed', 'next: Install the SDK'] };
for (const [width, height] of [[100, 30], [24, 6]]) {
console.log(`${width} × ${height}`);
for (const [pane, r] of layout(tree, { x: 0, y: 0, width, height }, contents)) {
console.log(` ${pane.padEnd(6)} x=${r.x} y=${r.y} ${r.width}×${r.height}`);
}
}100 × 30
tabs x=0 y=0 100×1
learn x=0 y=1 34×26
tasks x=34 y=1 66×26
status x=0 y=27 100×2
hints x=0 y=29 100×1
24 × 6
tabs x=0 y=0 24×1
learn x=0 y=1 20×2
tasks x=20 y=1 4×2
status x=0 y=3 24×2
hints x=0 y=5 24×1At 100 columns, learn and tasks split the width 1 : 2, and the leftover cell goes to the
earlier part, so the same input always lays out the same way. At 24 columns, learn keeps its
minimum of 20 and tasks gets what is left.
The rules
There is no flexbox and no constraint solver. Two passes, in integers:
- Fixed sizes, fitted sizes and every minimum are handed out first, in declared order.
- What is left is shared by fraction, on top of each minimum. A part whose minimum already covers its share takes nothing more.
A terminal too small for the minimums clips the later parts, never the first: the first pane stays readable, because it is the one people look at. Rounding leftovers go one cell each to the earliest fractional parts, so a frame is always filled.
A layout per tab
layout may be a function of the screen's state, called on every paint. That is how a tab
changes what the screen shows:
function layout(state) {
const body = state.active === 0 ? { direction: 'row', parts: [{ content: 'learn' }, { size: { fr: 2 }, content: 'tasks' }] } : 'log';
return { direction: 'column', parts: [{ size: 1, content: 'tabs' }, { content: body }, { size: 1, content: 'hints' }] };
}The dashboard recipe is built on exactly this. A pane that is not in the current tree is not drawn on a terminal; off a terminal every pane is printed, so tabs hide nothing from a pipe or an agent.
Collapse
A pane whose name is in the screen's collapsed set gets a size of 0, and the fractional parts
beside it share the space it gave up. A toggle:<pane> action collapses and expands it
(Tabs, focus and keys). collapse(tree, collapsed) is the same
step as a function, if you want the collapsed tree yourself.
Screens
open(): a screen inline under the transcript or in the alternate screen, its panes, and the five calls a screen answers — update, commit, dispatch, close, and its state.
Tabs, focus and keys
A keymap is data: key to action name. A pure reducer moves tabs, focus, collapse and scrolling, and the hint line is generated from the keymap, so it can never name a key that does nothing.