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

v3.0.0

Published

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

Downloads

2,990

Readme

@flyingrobots/bijou-tui

The high-fidelity TEA runtime for Bijou.

bijou-tui provides the application loop, layout primitives, motion, and orchestration needed to build complex interactive terminal apps on top of the Bijou core.

V3.0.0 Evolution

The TUI package has been completely overhauled in v3.0.0 to operate as a true graphics engine.

🌟 What's New

  • Honest view contract: App.view and framed pane renderers now speak ViewOutput (string | Surface | LayoutNode). Strings still work, but they are the legacy compatibility path.
  • Programmable Rendering Pipeline: The TEA view output is now processed through a 5-stage middleware pipeline (Layout -> Paint -> PostProcess -> Diff -> Output). Add custom fragment shaders or logging middleware effortlessly.
  • Fractal TEA (Sub-Apps): Compose nested apps with initSubApp(), updateSubApp(), mount(), and mapCmds() instead of flattening everything into one update loop.
  • Bijou CSS (BCSS): Style supported V3 surface components and frame shell regions with type/class/id selectors, var() token lookups, and terminal-aware media queries (@media (width < 80)). This is not yet a global cascade across arbitrary layout nodes.
  • Declarative Motion: Wrap any component in motion({ key: 'id' }, ...) and watch it smoothly interpolate layout changes (move, resize) using physics-based springs.
  • Unified Heartbeat: All animations and physics calculations are now synchronized to a single PulseMsg, eliminating timer jitter and saving CPU.

Installation

npm install @flyingrobots/[email protected] @flyingrobots/[email protected] @flyingrobots/[email protected]

If you are upgrading an existing app, see ../../docs/MIGRATING_TO_V3.md.

Quick Start (V3 Sub-App Composition)

import { initDefaultContext } from '@flyingrobots/bijou-node';
import { run, mount, mapCmds, type App } from '@flyingrobots/bijou-tui';
import { createSurface, type Surface } from '@flyingrobots/bijou';

initDefaultContext();

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

interface Model { 
  left: { count: number }; 
  right: { count: number }; 
}

// Parent App mounting two independent Sub-Apps
const app: App<Model, any> = {
  init: () => [{ left: { count: 0 }, right: { count: 0 } }, []],
  update: (msg, model) => [model, []],
  view: (model) => {
    // Render the children (they return Surfaces!)
    const [leftSurface] = mount(childApp, { model: model.left, onMsg: m => m });
    const [rightSurface] = mount(childApp, { model: model.right, onMsg: m => m });
    
    // Composite them onto the main screen
    const screen = createSurface(80, 24);
    screen.blit(leftSurface, 0, 0);
    screen.blit(rightSurface, 40, 0);
    return screen;
  }
};

run(app);

Quick Start (Basic)

import { initDefaultContext } from '@flyingrobots/bijou-node';
import { run, quit, type App, isKeyMsg } from '@flyingrobots/bijou-tui';

initDefaultContext();

type Model = { count: number };

const app: App<Model> = {
  init: () => [{ count: 0 }, []],

  update: (msg, model) => {
    if (isKeyMsg(msg)) {
      if (msg.key === 'q') return [model, [quit()]];
      if (msg.key === '+') return [{ count: model.count + 1 }, []];
      if (msg.key === '-') return [{ count: model.count - 1 }, []];
    }
    return [model, []];
  },

  view: (model) => `Count: ${model.count}\n\nPress +/- to change, q to quit`,
};

run(app);

Runtime Behavior Note

run() behaves differently by output mode:

  • interactive: full TEA loop (event bus, key/resize/mouse handling, command-driven updates).
  • pipe / static / accessible: render view(initModel) once and return immediately.

In non-interactive modes, there is no normal interactive event loop.

Features Breakdown

  • TEA runtime core: deterministic model/update/view loop with command-driven side effects.
  • Motion system: spring physics, tweens, and timeline sequencing for orchestrated terminal animation.
  • Layout engine: flexbox helpers, stacks, split panes, named-area grids, viewport scrolling, and resize-aware rendering.
  • Input architecture: keymaps, grouped bindings, generated help views, and layered input stack for modal flows.
  • Overlay composition: modal, toast, drawer, tooltip, and painter-style compositing primitives (including panel-scoped drawers).
  • App shell: createFramedApp() for tabs/help/chrome/pane-focus boilerplate with optional command palette.
  • Stateful building blocks: navigable table, browsable list, file picker, focus area, and DAG pane with vim-friendly keymaps.

Animation

Spring Physics

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

// Physics-based (default) — runs until the spring settles
const cmd = animate({
  from: 0,
  to: 100,
  spring: 'wobbly',  // or 'default', 'gentle', 'stiff', 'slow', 'molasses'
  onFrame: (v) => ({ type: 'scroll', y: v }),
});

// Duration-based with easing
const fade = animate({
  type: 'tween',
  from: 0,
  to: 1,
  duration: 300,
  ease: EASINGS.easeOutCubic,
  onFrame: (v) => ({ type: 'fade', opacity: v }),
});

// Skip animation (reduced motion)
const jump = animate({
  from: 0, to: 100,
  immediate: true,
  onFrame: (v) => ({ type: 'scroll', y: v }),
});

Timeline

GSAP-style orchestration — pure state machine, no timers:

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

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

// Drive from TEA update:
let tlState = tl.init();
// on each frame:
tlState = tl.step(tlState, 1/60);
const { slideIn, fadeIn, bounce } = tl.values(tlState);
const fired = tl.firedCallbacks(prev, tlState); // ['onReady']

Position syntax: '<' (parallel), '+=N' (gap), '-=N' (overlap), '<+=N' (offset from previous start), absolute ms, 'label', 'label+=N'.

Layout

Flexbox

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

// Sidebar + main content, responsive to terminal width
flex({ direction: 'row', width: cols, height: rows, gap: 1 },
  { basis: 20, content: sidebarText },
  { flex: 1, content: (w, h) => renderMain(w, h) },
);

// Header + body + footer
flex({ direction: 'column', width: cols, height: rows },
  { basis: 1, content: headerLine },
  { flex: 1, content: (w, h) => renderBody(w, h) },
  { basis: 1, content: statusLine },
);

Children can be render functions (width, height) => string — they receive their allocated space and reflow automatically when the terminal resizes.

Viewport

import { viewport, createScrollState, scrollBy, pageDown } from '@flyingrobots/bijou-tui';

let scroll = createScrollState(content, viewportHeight);

// Render visible window with scrollbar
const view = viewport({ width: 60, height: 20, content, scrollY: scroll.y });

// Handle scroll keys
scroll = scrollBy(scroll, 1);   // down one line
scroll = pageDown(scroll);       // down one page

Basic Layout

import { vstack, hstack } from '@flyingrobots/bijou-tui';

vstack(header, content, footer);       // vertical stack
hstack(2, leftPanel, rightPanel);      // side-by-side with gap

Split Pane

import {
  createSplitPaneState, splitPane, splitPaneResizeBy, splitPaneFocusNext,
} from '@flyingrobots/bijou-tui';

let state = createSplitPaneState({ ratio: 0.35 });

// in update:
state = splitPaneResizeBy(state, 2, { total: cols, minA: 16, minB: 16 });
state = splitPaneFocusNext(state);

// in view:
const output = splitPane(state, {
  direction: 'row',
  width: cols,
  height: rows,
  minA: 16,
  minB: 16,
  paneA: (w, h) => renderSidebar(w, h),
  paneB: (w, h) => renderMain(w, h),
});

Grid

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

const output = grid({
  width: cols,
  height: rows,
  columns: [24, '1fr'],
  rows: [3, '1fr', 8],
  areas: [
    'header header',
    'nav main',
    'logs main',
  ],
  gap: 1,
  cells: {
    header: (w, h) => renderHeader(w, h),
    nav: (w, h) => renderNav(w, h),
    logs: (w, h) => renderLogs(w, h),
    main: (w, h) => renderMain(w, h),
  },
});

Resize Handling

Terminal resize events are dispatched automatically as ResizeMsg:

update(msg, model) {
  if (msg.type === 'resize') {
    return [{ ...model, cols: msg.columns, rows: msg.rows }, []];
  }
  // ...
}

view(model) {
  return flex(
    { direction: 'row', width: model.cols, height: model.rows },
    { basis: 20, content: sidebar },
    { flex: 1, content: (w, h) => mainContent(w, h) },
  );
}

Event Bus

The runtime uses an EventBus internally. You can also create your own for custom event sources:

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

const bus = createEventBus<MyMsg>();
bus.connectIO(ctx.io);           // keyboard + resize
bus.on((msg) => { /* ... */ });  // single subscription
bus.emit(customMsg);             // synthetic events
bus.runCmd(someCommand);         // command results re-emitted
bus.dispose();                   // clean shutdown

See ARCHITECTURE.md for the full event flow and GUIDE.md for detailed usage patterns.

Keybinding Manager

Declarative key binding with modifier support, named groups, and runtime enable/disable:

import { createKeyMap, type KeyMsg } from '@flyingrobots/bijou-tui';

type Msg = { type: 'quit' } | { type: 'help' } | { type: 'move'; dir: string };

const kb = createKeyMap<Msg>()
  .bind('q', 'Quit', { type: 'quit' })
  .bind('?', 'Help', { type: 'help' })
  .bind('ctrl+c', 'Force quit', { type: 'quit' })
  .group('Navigation', (g) => g
    .bind('j', 'Down', { type: 'move', dir: 'down' })
    .bind('k', 'Up', { type: 'move', dir: 'up' })
  );

// In TEA update:
const action = kb.handle(keyMsg);
if (action !== undefined) return [model, [/* ... */]];

// Runtime enable/disable
kb.disableGroup('Navigation');
kb.enable('Quit');

Help Generation

Auto-generate help text from registered bindings:

import { helpView, helpShort, helpFor } from '@flyingrobots/bijou-tui';

helpView(kb);           // full grouped multi-line help
helpShort(kb);          // "q Quit • ? Help • Ctrl+c Force quit • j Down • k Up"
helpFor(kb, 'Nav');     // only Navigation group

Input Stack

Layered input dispatch for modal UIs — push/pop handlers with opaque or passthrough behavior:

import { createInputStack, type KeyMsg } from '@flyingrobots/bijou-tui';

const stack = createInputStack<KeyMsg, Msg>();

// Base layer — global keys, lets unmatched events fall through
stack.push(appKeys, { passthrough: true });

// Modal opens — captures all input (opaque by default)
const modalId = stack.push(modalKeys);

// Dispatch returns first matched action, top-down
const action = stack.dispatch(keyMsg);

// Modal closes
stack.remove(modalId);

KeyMap implements InputHandler, so it plugs directly into the input stack.

Overlay Compositing

Paint overlays (modals, toasts) on top of existing content:

import { composite, modal, toast } from '@flyingrobots/bijou-tui';

// Create a centered dialog
const dialog = modal({
  title: 'Confirm',
  body: 'Delete this item?',
  hint: 'y/n',
  screenWidth: 80,
  screenHeight: 24,
});

// Create a toast notification
const notification = toast({
  message: 'Saved successfully',
  variant: 'success',        // 'success' | 'error' | 'info'
  anchor: 'bottom-right',    // 'top-right' | 'bottom-right' | 'bottom-left' | 'top-left'
  screenWidth: 80,
  screenHeight: 24,
});

// Paint overlays onto background content
const output = composite(backgroundView, [dialog, notification], { dim: true });

Each overlay is a { content, row, col } object. composite() splices them onto the background using painter's algorithm (last overlay wins on overlap). The dim option fades the background with ANSI dim.

drawer() now supports left/right/top/bottom anchors and optional region mounting for panel-scoped overlays.

App Frame

createFramedApp() wraps page-level TEA logic in a shared shell:

  • tabs + page switching
  • pane focus and per-pane scroll isolation
  • frame help (?) and optional command palette (ctrl+p / :)
  • overlay factory with pane rects for panel-scoped drawers/modals

Pane renderers may return a legacy string, a Surface, or a LayoutNode. The shell normalizes those outputs into the framed scroll/focus path for you.

See examples/release-workbench/main.ts for the canonical shell demo and examples/app-frame/main.ts for a compact focused example.

Building Blocks

Reusable stateful components that follow the TEA state + pure transformers + sync render + convenience keymap pattern:

Navigable Table

import {
  createNavigableTableState, navigableTable, navTableFocusNext,
  navTableKeyMap, helpShort,
} from '@flyingrobots/bijou-tui';

const state = createNavigableTableState({ columns, rows, height: 10 });
const output = navigableTable(state, { ctx });
const next = navTableFocusNext(state);

Browsable List

import {
  createBrowsableListState, browsableList, listFocusNext,
  browsableListKeyMap,
} from '@flyingrobots/bijou-tui';

const state = createBrowsableListState({ items, height: 10 });
const output = browsableList(state);

File Picker

import {
  createFilePickerState, filePicker, fpFocusNext, fpEnter, fpBack,
  filePickerKeyMap,
} from '@flyingrobots/bijou-tui';
import { nodeIO } from '@flyingrobots/bijou-node';

const io = nodeIO();
const state = createFilePickerState({ cwd: process.cwd(), io, height: 15 });
const output = filePicker(state);

Focus Area

import {
  createFocusAreaState, focusArea, focusAreaScrollBy,
  focusAreaKeyMap,
} from '@flyingrobots/bijou-tui';

const state = createFocusAreaState({ content, width: 60, height: 20, overflowX: 'scroll' });
const output = focusArea(state, { focused: true, ctx });

DAG Pane

import {
  createDagPaneState, dagPane, dagPaneSelectChild,
  dagPaneSelectParent, dagPaneKeyMap,
} from '@flyingrobots/bijou-tui';

const state = createDagPaneState({ source: nodes, width: 80, height: 24, ctx });
const output = dagPane(state, { focused: true, ctx });
const next = dagPaneSelectChild(state, ctx); // arrow-key navigation

All building blocks include *KeyMap() factories for preconfigured vim-style keybindings.

Related Packages

License

MIT


.-:::::':::   .-:.     ::-.::::::.    :::.  .,-:::::/
;;;'''' ;;;    ';;.   ;;;;';;;`;;;;,  `;;;,;;-'````'
[[[,,== [[[      '[[,[[['  [[[  [[[[[. '[[[[[   [[[[[[/
`$$$"`` $$'        c$$"    $$$  $$$ "Y$c$$"$$c.    "$$
 888   o88oo,.__ ,8P"`     888  888    Y88 `Y8bo,,,o88o
 "MM,  """"YUMMMmM"        MMM  MMM     YM   `'YMUP"YMM
:::::::..       ...     :::::::.      ...   :::::::::::: .::::::.
;;;;``;;;;   .;;;;;;;.   ;;;'';;'  .;;;;;;;.;;;;;;;;'''';;;`    `
 [[[,/[[['  ,[[     \[[, [[[__[[\.,[[     \[[,   [[     '[==/[[[[,
 $$$$$$c    $$$,     $$$ $$""""Y$$$$$,     $$$   $$       '''    $
 888b "88bo,"888,_ _,88P_88o,,od8P"888,_ _,88P   88,     88b    dP
 MMMM   "W"   "YMMMMMP" ""YUMMMP"   "YMMMMMP"    MMM      "YMmMY"