npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@flyingrobots/bijou-tui

v7.2.0

Published

TEA runtime for terminal UIs — model/update/view with keyboard input, alt screen, and layout helpers.

Readme

@flyingrobots/bijou-tui

The high-fidelity TEA runtime for Bijou.

@flyingrobots/bijou-tui provides the application loop, layout primitives, and physics-powered orchestration needed to build complex interactive terminal apps.

Role

  • The Elm Architecture (TEA): A deterministic state-update-view loop for industrial-strength terminal software.
  • Fractal TEA: Compose nested sub-apps with createSubAppAdapter(), initSubApp(), updateSubApp(), and mount().
  • Declarative Motion: Interpolate layout changes smoothly with physics-based springs and tween animations.
  • Surface-First Pipeline: Programmable rendering middleware for fragments, diffing, and shader-based transitions.

Install

npm install @flyingrobots/bijou @flyingrobots/bijou-node @flyingrobots/bijou-tui

Quick Start (Sub-App Composition)

import { startApp } from '@flyingrobots/bijou-node';
import { createSubAppAdapter, mount, type App } from '@flyingrobots/bijou-tui';
import { createSurface } from '@flyingrobots/bijou';

type ChildMsg = { type: 'tick' };
type ParentModel = {
  left: { count: number };
  right: { count: number };
};
type ParentMsg = { type: 'left'; msg: ChildMsg } | { type: 'right'; msg: ChildMsg };

const childApp: App<{ count: number }, ChildMsg> = {
  init: () => [{ count: 0 }, []],
  update: (msg, model) => [model, []],
  view: (model) => {
    const s = createSurface(20, 5);
    s.fill({ char: '.' });
    return s;
  }
};

const mapLeft = createSubAppAdapter<ParentMsg, ChildMsg>({
  tick: (msg) => ({ type: 'left', msg }),
});

const mapRight = createSubAppAdapter<ParentMsg, ChildMsg>({
  tick: (msg) => ({ type: 'right', msg }),
});

const app: App<ParentModel, ParentMsg> = {
  init: () => [{ left: { count: 0 }, right: { count: 0 } }, []],
  update: (msg, model) => [model, []],
  view: (model) => {
    const [left] = mount(childApp, { model: model.left, onMsg: mapLeft });
    const [right] = mount(childApp, { model: model.right, onMsg: mapRight });
    
    const screen = createSurface(80, 24);
    screen.blit(left, 0, 0);
    screen.blit(right, 40, 0);
    return screen;
  }
};

await startApp(app);

For Node hosts, prefer startApp() for the first-app path. Reach for run(app, { ctx }) when the host owns context creation explicitly.

When you need to mix small string fragments with surface-returning primitives, keep composition on the surface side: use contentSurface() directly or pass strings into vstackSurface() / hstackSurface(). Raw strings are still not a valid view() return type.

Use proseSurface(text, { width }) when paragraph-like text should wrap at word boundaries before becoming a surface. Keep contentSurface() for preformatted output, tables, boxes, and caller-managed line breaks.

Strategy: Choosing Component Families

Select the family based on the interaction semantic.

Overlays and Interruption

  • drawer(): Supplemental detail while maintaining main context.
  • modal(): Required decision that blocks background activity.
  • toast(): Transient notification for a single event.
  • tooltip(): Micro-explanation for a local target.
  • debugOverlay(): Development-only perf HUD composited onto any app surface.
  • layoutInspectorOverlay(): Development-only geometry HUD for region bounds, focus, and layers.
  • inputRoutingInspectorText(): Development-only route trace for recent key and pointer events.
  • focusMapText(): Development-only focus ownership and tab-order report.
  • evaluateSurfaceBudget(): Deterministic warnings for surface and timing thresholds.
  • surfaceDiffSurface(): Side-by-side or overlay inspection for two rendered surfaces.

Collection Interaction

  • navigableTable(): Keyboard-driven traversal and cell inspection.
  • browsableList(): Description-led traversal in one dimension.
  • commandPalette(): Action discovery and navigation.

Shell and Workspace Layout

  • createFramedApp(): Batteries-included workspace with tabs, panes, help, and a shell-owned perf HUD toggled with the backtick key.
  • splitPane(): Dynamic primary/secondary context comparison.
  • grid(): Stable regions with simultaneous visibility.
  • viewport(): The canonical scroll mask for rich composition.

For framed shells, Node hosts can still prefer startApp(app): the hosted Node bootstrap delegates to self-running framed apps automatically. Use the explicit runner path when you want to stay inside @flyingrobots/bijou-tui or when the host owns ctx directly:

import { createFramedApp, runFramedApp } from '@flyingrobots/bijou-tui';

const app = createFramedApp({ pages: [page] });

await app.run({ ctx });
// or: await runFramedApp({ pages: [page] }, { ctx });

This path keeps the shell batteries included: mouse input defaults to true, the shared runtime loop still does the heavy lifting, and frame timing/budget telemetry stays attached to the frame model for shell-owned UI. For Node-hosted apps, startApp(app) remains the default bootstrap.

Runtime Diagnostics

The EventBus exposes command queue facts through getCommandDiagnostics(). run(app, { commandBackpressureThreshold }) routes a warning when pending commands cross the threshold, without hard-capping legitimate bursts. The default threshold is 1000; use 0 to disable the warning.

Render pipeline middleware is synchronous by contract. If middleware returns a Promise, Bijou reports a pipeline diagnostic, keeps the current frame moving, and ignores any late next() continuation so stage ordering stays deterministic.

Animation

Spring Physics

import { animate, SPRING_PRESETS } from '@flyingrobots/bijou-tui';

const cmd = animate({
  from: 0,
  to: 100,
  spring: 'wobbly',
  onFrame: (v) => ({ type: 'scroll', y: v }),
});

Spring commands integrate physics with bounded fixed timesteps by default, so a slow terminal frame cannot feed a large raw pulse delta into the spring. Tune fixedStepSeconds and maxPulseSeconds only when an app needs a custom simulation cadence.

Timeline Orchestration

import { timeline } from '@flyingrobots/bijou-tui';

const tl = timeline()
  .add('slideIn', { type: 'tween', from: -100, to: 0, duration: 300 })
  .label('settled')
  .add('bounce', { from: 0, to: 10, spring: 'wobbly' }, 'settled')
  .build();

Post-Process Shaders

canvas() can render procedural surfaces at cell, Quad, Braille, or glyph-fit resolution. Use resolution: 'glyph' when a shader should sample a 2x4 grid but collapse into geometry-aware Unicode or ASCII characters instead of Braille.

import { canvas } from '@flyingrobots/bijou-tui';

const logo = canvas(24, 6, ({ u, v }) => {
  const lit = Math.abs(u - v) < 0.08;
  return lit ? { char: 'X', fg: '#ffffff' } : ' ';
}, { resolution: 'glyph' });
import { run, surfaceShaderFilter, scanlines, vignette } from '@flyingrobots/bijou-tui';

await run(app, {
  configurePipeline(pipeline) {
    pipeline.use('PostProcess', surfaceShaderFilter(
      scanlines({ dimFactor: 0.82 }),
      vignette({ edgeFactor: 0.78 }),
    ));
  },
});

Use surfaceShaderFilter(...) to compose built-in post-process passes like scanlines(), flicker(), noise(), and vignette() over the packed target surface before diff/output.

Testing

Use testRuntime() when you want an inspectable harness instead of a one-shot script result:

import { testRuntime } from '@flyingrobots/bijou-tui';

const harness = await testRuntime(app, { ctx });
await harness.press('q');

expect(harness.frame).toBeDefined();
expect(harness.messages).toHaveLength(1);
expect(harness.commands.every((record) => record.settled)).toBe(true);

await harness.teardown();

Keep runScript() for fixture-style interaction playback and GIF/demo capture, and use testRuntime() when you need direct assertions on snapshots, emitted messages, command outcomes, or cleanup disposal.

Mouse playback can use helper steps instead of hand-authored MouseMsg objects. sgrMouse() parses raw SGR mouse sequences when a regression should match terminal input bytes directly.

import { mouseMove, mousePress, mouseRelease, mouseWheel, runScript, sgrMouse } from '@flyingrobots/bijou-tui';

await runScript(app, [
  mouseMove(12, 4),
  mousePress('left', 12, 4),
  mouseRelease('left', 12, 4),
  mouseWheel('down', 12, 8),
  sgrMouse('\x1b[<35;10;20M'),
]);

For framed-app integrations that need to wrap or route page-level commands, import the page-scoped frame helpers from the package root:

import { PAGE_MSG_TOKEN, emitMsgForPage, isPageScopedMsg, wrapCmdForPage, wrapPageMsg } from '@flyingrobots/bijou-tui';

Use surfaceDiffText() or surfaceDiffSurface() when a frame assertion fails and you need cell-level truth instead of a raw string dump:

import { surfaceDiffText } from '@flyingrobots/bijou-tui';

expect(surfaceDiffText(before, after)).toContain('surface diff: 2 changed');

Use layoutInspectorOverlay() when a layout assertion needs visual geometry truth on top of the rendered frame:

import {
  layoutInspectorOverlay,
  layoutInspectorText,
} from '@flyingrobots/bijou-tui';

const inspected = layoutInspectorOverlay(frame, [
  {
    id: 'editor',
    rect: { col: 2, row: 1, width: 40, height: 12 },
    focused: true,
  },
]);

expect(layoutInspectorText([
  { id: 'editor', rect: { x: 2, y: 1, width: 40, height: 12 } },
]))
  .toContain('focused=false');

Use evaluateSurfaceBudget() directly in tests or pass surfaceBudget to run() when an interactive app should route non-fatal runtime warnings:

import { evaluateSurfaceBudget, run } from '@flyingrobots/bijou-tui';

expect(evaluateSurfaceBudget({
  surface: frame,
  thresholds: { maxArea: 4000 },
})).toEqual([]);

await run(app, {
  ctx,
  surfaceBudget: { maxArea: 4000, maxStageDurationMs: { Paint: 8 } },
});

Use appendInputRoutingRecord() and inputRoutingInspectorText() when a shell or app wants to keep the last few routing decisions visible:

import {
  appendInputRoutingRecord,
  inputRoutingInspectorText,
} from '@flyingrobots/bijou-tui';

const history = appendInputRoutingRecord({ records: [] }, {
  event,
  result,
  commandLabels: ['save-file'],
});

console.log(inputRoutingInspectorText(history));

Use createInputGestureRecognizer() and createInputActionMap() when raw input needs to become semantic gestures before routing. Bijou ships standard feature event names such as press, held, release, tap, double-tap, long-press, and lets apps provide their own event type strings.

import {
  createInputActionMap,
  createInputGestureRecognizer,
} from '@flyingrobots/bijou-tui';

const recognizer = createInputGestureRecognizer({ doubleTapMs: 300 });
const actions = createInputActionMap<{ type: 'open-jump-list' }>()
  .bind('nav.doubleG', 'Open jump list', [
    { deviceId: 'keyboard', featureId: 'key.g', type: 'double-tap' },
  ], { type: 'open-jump-list' });

const action = actions.handle(recognizer.observeKey(keyMsg, ctx.runtime.clock.now()));

Use focusMapText() or focusMapSurface() when focus ownership and tab order need the same deterministic treatment:

import { focusMapText } from '@flyingrobots/bijou-tui';

const report = focusMapText([
  {
    id: 'editor',
    owner: 'workspace',
    role: 'textbox',
    rect: { x: 2, y: 1, width: 60, height: 20 },
    tabIndex: 1,
    focusable: true,
    focused: true,
  },
]);

expect(report).toContain('[1] *editor');

Documentation


Built with 💎 by FLYING ROBOTS