The Workshop · Vitrine

@cleocode/animations

Unicode terminal animations woven on the LOOM — for the cleo CLI and CleoOS.
pnpm add @cleocode/animations

18 braille loaders

Frame-cycled spinners indexed by their generic name. Ported verbatim from unicode-animations (MIT, Gunnar Gray). Each entry is { frames: string[], interval: number } — pure data, no I/O.

9 workshop names

Aliases drawn from CLEO canon (CLEO-VISION.md · NEXUS-CORE-ASPECTS.md). Same Spinner objects as the generic registry — references, not copies — so renaming a generic spinner automatically updates the canon view.

3 woven gauges

Fixed-width segmented bars. Tapestry uses coarse Unicode blocks (░▒▓█) for a woven cloth feel. Cascade uses 1/8 gradient steps (▏▎▍▌▋▊▉█) for a smooth waterfall edge. Refinery uses braille block-fill stages, evoking the BRAIN memory promotion pipeline filling level-by-level.

4 canon flares

Sparks are finite frame sequences. They play once and decay back to empty — not loops. awaken at first dream / cleo init; sweep on BRAIN integrity sweep complete; cascade on release-shipped / task-complete; weave on playbook stage transitions and CANT directive acceptance. (On this page they auto-replay every ~1.4s so you can see the motion.)

AnimateContext

Every primitive in this package routes its output through an AnimateContext. The context is pure data — no I/O, no timers — derived from the LAFS FlagResolution plus environment signals. When silent, primitives return no-op handles so callers don't branch on output mode.

SignalSourceEffectReason code
format !== 'human'LAFS flagsDisable animations (machine output)format-json
quiet === trueLAFS flagsDisable animations (script-friendly)quiet
!isTTYprocess.stdout.isTTYDisable animations (piped/redirected)no-tty
NO_COLOR setprocess.env.NO_COLORDisable animations (no-color.org)no-color

Construct it once at command entry, thread it everywhere downstream:

import { resolveOutputFormat } from '@cleocode/lafs';
import { createAnimateContext } from '@cleocode/animations';

const flags = resolveOutputFormat({ humanFlag: true });
const ctx = createAnimateContext({ flagResolution: flags });

if (ctx.enabled) /* render spinner */;
else console.log(`silent: ${ctx.reason}`);
// → "enabled" | "format-json" | "quiet" | "no-tty" | "no-color"

How to render

Lint rule: any process.stdout.write(...) of a string starting with \r outside @cleocode/animations is a contract violation. Always route through createSpinnerHandle — it owns the timer, the cursor, the exit handler, and the LAFS gate.

Spinner during async work — the canonical pattern
import { resolveOutputFormat } from '@cleocode/lafs';
import { createAnimateContext, createSpinnerHandle } from '@cleocode/animations';

const ctx = createAnimateContext({
  flagResolution: resolveOutputFormat({ humanFlag: true }),
});

const spinner = createSpinnerHandle(ctx, 'looming', 'Weaving tasks…');
spinner.start();
try {
  await doWork();
  spinner.stop('✔ Tapestry complete.');
} catch (err) {
  spinner.stop();
  throw err;
}
Multi-step pipeline with label updates
const spinner = createSpinnerHandle(ctx, 'weaving', 'Connecting to database…');
spinner.start();

const db = await connect();
spinner.update(`Running ${migrations.length} migrations…`);

await db.migrate(migrations);
spinner.stop('✔ Database ready.');
Progress bar with a known ratio
import { renderProgressBar } from '@cleocode/animations';

function tick(done, total) {
  const ratio = done / total;
  const bar = renderProgressBar('refinery', ratio, 36);
  // (progress bars are not yet routed through a handle —
  //  gate manually with ctx.enabled until ProgressBarHandle ships)
  if (ctx.enabled) process.stdout.write(`\r\x1B[2K  ${bar}  ${done}/${total}`);
}
One-shot spark on success
import { sparks, sparkDurationMs } from '@cleocode/animations';

async function playSpark(name) {
  if (!ctx.enabled) return;
  const { frames, interval } = sparks[name];
  for (const f of frames) {
    process.stdout.write(`\r\x1B[2K  ${f}`);
    await new Promise(r => setTimeout(r, interval));
  }
  process.stdout.write('\n');
}

await shipRelease();
await playSpark('cascade'); // total: ~980ms via sparkDurationMs('cascade')

Exports from @cleocode/animations

Spinners
ExportType / Shape
spinnersRecord<BrailleSpinnerName, Spinner> — 18 entries
canonSpinnersRecord<CanonSpinnerName, Spinner> — 9 aliases
CANON_TO_GENERICRecord<CanonSpinnerName, BrailleSpinnerName>
resolveSpinner(name)(string) => Spinner | undefined
gridToBraille(grid)(boolean[][]) => string
makeGrid(rows, cols)(number, number) => boolean[][]
SpinnerTS interface: { frames: readonly string[]; interval: number }
BrailleSpinnerName · CanonSpinnerNameTS string-literal unions
AnimateContext
ExportType / Shape
createAnimateContext(input)(AnimateContextInput) => AnimateContext
SILENT_CONTEXTFrozen AnimateContext — always disabled
AnimateContext{ enabled, reason, inputs }
AnimateContextInput{ flagResolution, isTTY?, noColor? }
FlagResolutionLike{ format: 'json'|'human'; quiet: boolean }
SpinnerHandle (canonical \r owner)
ExportType / Shape
createSpinnerHandle(ctx, name, label, options?)(AnimateContext, name, string, SpinnerHandleOptions?) => SpinnerHandle
SpinnerHandle{ start(); stop(finalLine?); update(label); enabled: boolean }
SpinnerHandleOptions{ delayMs?: number } — defaults to 150
Progress & Sparks
ExportType / Shape
progressBarsRecord<ProgressBarStyle, ProgressBarRenderer>
renderProgressBar(style, ratio, width)(style, number, number) => string
ProgressBarStyle'tapestry' | 'cascade' | 'refinery'
sparksRecord<SparkName, Spark>
sparkDurationMs(name)(SparkName) => number
SparkName'awaken' | 'sweep' | 'cascade' | 'weave'
Canon mapping
Canon nameGeneric nameCleo lore role
loominghelixTwin strands weaving — task on the LOOM
weavingbraillewavePattern threading across columns
heartbeatbreatheOrganic in-out pulse — Hearth presence
awakeningpulseRadial bloom — first dream / cleo init
sweepingscanLeft→right beam — BRAIN integrity Sweep
watchingorbitCircular sentinel — sentient daemon tick
cascadecascadeDiagonal fall — command-success accent
tapestrywaverowsMulti-row sinusoidal — wave of tasks shipping
refinerycolumnsFilling stages — memory promotion pipeline