Spinners, progress and task lists
The three components that show work in motion — spinner, progress and tasks — and what each prints off a terminal.
Three built-in components show work that is still going. Each is a factory on its own subpath, so a program that only wants a spinner pays for a spinner.
Spinners
import { spinner } from 'flagstaff/spinner';
spinner(); // the `dots` style
spinner('line'); // any registered style, by nameThe state is { text, status }. status is running when omitted, and one of ok, fail,
warn or info when the work is done. A finished status draws its glyph — ✔, ✖, ⚠,
ℹ — the same in every mode; a running one is the style's frames on a terminal and its static
text (… for the built-ins) everywhere else.
import { hoist } from 'flagstaff/loop';
import { spinner } from 'flagstaff/spinner';
import { json, rt } from './rt.mjs';
const flag = hoist(spinner('line'), rt, { text: 'uploading' }, { json });
flag.update({ text: 'uploading: retrying once', status: 'warn' });
flag.lower({ text: 'upload failed', status: 'fail' });… uploading
⚠ uploading: retrying once
✖ upload failedTwo styles ship built in, dots and line, and they are registered through the same
register() a third-party plugin uses. The ~80 styles of
cli-spinners are not bundled; if you already
depend on it, fromCliSpinners() turns it into a plugin — see Plugins.
An unknown name is refused with E_UNKNOWN_SPINNER and the list of names that exist.
Progress
import { progress } from 'flagstaff/progress';
progress(); // a 24-cell bar of █ and ░
progress({ width: 40, glyphs: { filled: '#', empty: '.' } });The state is { done, total, label? }, and done is clamped into [0, total]. A terminal
gets a drawn bar with the count; everything else gets the count and the percentage, because a
bar of blocks in a log tells a reader nothing:
import { hoist } from 'flagstaff/loop';
import { progress } from 'flagstaff/progress';
import { json, rt } from './rt.mjs';
const flag = hoist(progress(), rt, { done: 0, total: 30, label: 'files' }, { json });
flag.update({ done: 12, total: 30, label: 'files' });
flag.lower({ done: 30, total: 30, label: 'files' });0/30 files · 0%
12/30 files · 40%
30/30 files · 100%Off a terminal every state that changes the text is a line, so a loop that updates on every item writes a line per item. Progress in logs shows how to update in steps instead.
Task lists
import { tasks } from 'flagstaff/tasks';
tasks(); // the running task drawn with the `dots` spinner
tasks({ spinner: 'line' });The state is { tasks: [{ title, status?, detail? }] }. status is pending when omitted,
then running, then one of ok, fail, warn or info. A terminal draws every task, the
running one animated and its detail under it. Off a terminal a task is printed once, when
it settles — a pending or running task is not news yet, and a finished one is never printed
twice:
import { hoist } from 'flagstaff/loop';
import { tasks } from 'flagstaff/tasks';
import { json, rt } from './rt.mjs';
const flag = hoist(tasks(), rt, { tasks: [{ title: 'install', status: 'running' }, { title: 'build' }, { title: 'test' }] }, { json });
flag.update({ tasks: [{ title: 'install', status: 'ok' }, { title: 'build', status: 'running', detail: 'tsc' }, { title: 'test' }] });
flag.update({ tasks: [{ title: 'install', status: 'ok' }, { title: 'build', status: 'ok' }, { title: 'test', status: 'running' }] });
flag.lower({ tasks: [{ title: 'install', status: 'ok' }, { title: 'build', status: 'ok' }, { title: 'test', status: 'fail' }] });✔ install
✔ build
✖ testBuild steps runs a real list of steps this way.
Coming from ora
flagstaff/ora is ora 9's API, graded by ora's own test suite, and it behaves as ora does —
including in a pipe, where ora writes a start line and a final line and nothing in between. The
spinner above is the loop underneath it; Coming from ora and
Incremental migration cover moving from one to the other.