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

bare-tui

v0.0.3

Published

A little TUI framework for Bare, based on bubbletea

Readme

bare-tui

A little TUI framework for Bare, based on The Elm Architecture. It's a functional, stateful way to build terminal apps that's pleasant for both simple and complex programs — and it runs anywhere Bare runs, with no Node.js dependencies.

It's designed for AI. You should be able to point your agent to CLAUDE.md and instruct it to build out your UI. That's how the mock claude code example was built.

Mock Claude Code

[!NOTE]
This an experimental library. A version 1.0.0 release will signal stability.

bare-tui is shaped after Charm's wonderful Bubble Tea; if you know that, you already know this. It ships its own component set (the Bubbles equivalent) and a styling/layout helper (the Lip Gloss equivalent), all built on Bare's native primitives (bare-tty, bare-ansi-escapes).

The snippets below import require('bare-tui'). Inside this repo's own examples/ and test/, that's a relative require('..').

Tutorial

This tutorial assumes you have Bare installed. We'll build a simple counter.

bare-tui programs are made of a model describing the application state, and three methods on that model:

  • init — a function that returns an initial command (or null).
  • update — a function that handles incoming messages and updates the model.
  • view — a function that renders the model to a string.

The Model

Start with a model — anything that holds your app's state. A class is idiomatic:

const { Program, quit, key } = require('bare-tui')

class Counter {
  constructor() {
    this.count = 0
  }
}

Initialization

init returns the first command to run, or null for none. Commands are how you kick off work (timers, I/O); more on them below.

init() {
  return null
}

The Update Method

update is called when a message arrives. A message is any tagged value — a keypress, a window resize, the result of a command. It returns a [model, command] pair (returning a bare model means "no command").

update(msg) {
  if (msg.type !== 'key') return [this, null]
  if (key.matches(msg, 'q', 'ctrl+c')) return [this, quit]   // quit the program
  if (key.matches(msg, 'up', 'k')) this.count++
  if (key.matches(msg, 'down', 'j')) this.count--
  return [this, null]
}

Mutating this and returning [this, cmd] is the idiomatic style here.

The View Method

view renders the current model to a string. bare-tui draws it for you and only repaints the lines that changed.

view() {
  return `count: ${this.count}\n\n↑/↓ change · q quit`
}

All Together Now

const { Program, quit, key } = require('bare-tui')

class Counter {
  constructor() {
    this.count = 0
  }
  init() {
    return null
  }
  update(msg) {
    if (msg.type !== 'key') return [this, null]
    if (key.matches(msg, 'q', 'ctrl+c')) return [this, quit]
    if (key.matches(msg, 'up', 'k')) this.count++
    if (key.matches(msg, 'down', 'j')) this.count--
    return [this, null]
  }
  view() {
    return `count: ${this.count}\n\n↑/↓ change · q quit`
  }
}

new Program(new Counter()).run()

Run it with bare counter.js. The Program puts the terminal into raw mode, enters the alternate screen, decodes input into messages, and — importantly — restores the terminal on exit, even if your code throws.

Commands

A command (Cmd) is a function () => Msg | Promise<Msg> | null. The runtime runs it off the update path and feeds whatever message it returns back into update. This is how you do anything asynchronous — timers, file or network I/O, talking to a worker — without blocking the UI.

const { quit, batch, sequence, tick, every, suspend } = require('bare-tui')

quit // a Cmd that quits the program
tick(1000, () => ({ type: 'tick' })) // fire a Msg after 1s
every(1000, () => ({ type: 'tick' })) // fire on the wall-clock second
batch(cmdA, cmdB) // run several Cmds concurrently
sequence(cmdA, cmdB) // run several Cmds in order
suspend(fn) // drop the TUI, run fn() with the real terminal, then resume

Use suspend to hand the terminal to an external program that needs it — an editor ($EDITOR), a pager, a sub-shell. The runtime drops raw mode, leaves the alt-screen and releases stdin while fn() runs, then re-attaches and repaints; the message fn resolves to is delivered once the TUI is back:

return [
  model,
  suspend(async () => {
    await spawnEditor(file) // owns the real terminal while it runs
    return { type: 'edited', file }
  })
]

An async command just returns a promise:

const load = () =>
  fetch(url)
    .then((res) => res.json())
    .then((data) => ({ type: 'loaded', data }))

Return commands from init or update; the result comes back as a message.

Key & mouse input

Keys arrive as { type: 'key' } messages (a KeyMsg). Match them with key.matches, which is null- and type-safe:

if (key.matches(msg, 'enter')) ...
if (key.matches(msg, 'ctrl+c', 'q')) ...

Define reusable, self-documenting bindings with key.binding — the help component renders them automatically:

const keys = {
  up: key.binding({ keys: ['up', 'k'], help: { key: '↑/k', desc: 'up' } })
}

Enable the mouse with a Program option; clicks/scroll/drag arrive as { type: 'mouse', action, button, x, y }:

new Program(model, { mouse: true }) //  true | 'drag' | 'all'

Components

Ready-made, composable pieces — each is a model (update/view) you embed in your own. See each doc for options, methods, messages, and keybindings.

| Component | Description | | ------------------------------------ | --------------------------------------- | | spinner | Animated loading indicator | | textinput | Single-line text field | | autocomplete | Text field with a suggestion menu | | textarea | Multi-line text editor | | list | Selectable, filterable list | | select | Compact dropdown over a fixed list | | radio | Single choice from a fixed set | | checkbox | Boolean toggle | | focus | Focus ring across child components | | table | Columns with selectable, scrolling rows | | viewport | Scrollable window over long content | | paginator | Page state + indicator | | progress | Progress bar | | help | Keybinding hints from key.bindings | | stopwatch | Counts elapsed time up | | timer | Counts a duration down | | filepicker | Browse the filesystem and pick a file |

To embed one, hold it as a field, route messages to it, and thread its command back up:

update(msg) {
  if (msg.type === 'spinner.tick') {
    const [s, cmd] = this.spinner.update(msg)
    this.spinner = s
    return [this, cmd]
  }
  ...
}

Styling & layout

style is a small, chainable, immutable styling and layout helper (a Lip Gloss equivalent). All measurement is ANSI- and wide-character-aware, so styled text composes correctly.

const { style } = require('bare-tui')

style()
  .bold(true)
  .foreground('cyan') // name, 0–255, or #hex
  .padding(1, 2)
  .border(style.borders.rounded)
  .borderForeground('blue')
  .render('Hello')

// Layout: place blocks side by side or stacked.
style.joinHorizontal(style.position.top, left, '  ', right)
style.joinVertical(style.position.left, header, body, footer)

Set .width(n) to pin a block to a fixed width (short/blank lines pad out, so a bordered box tracks the screen edge instead of shrinking to its content).

Testing

bare-tui is built to be tested headlessly — no real terminal, no real I/O.

  • Drive a Program with injected streams: pass { input, output, isTTY: true } (a bare-stream PassThrough and a capturing Writable), write key bytes to input, await program.run(), and assert on the captured output. Use { fps: 0 } to render synchronously per update for deterministic frames.
  • Mock dependencies: components that touch the outside world take their deps by injection. For example filepicker.mock(tree) returns an in-memory { fs, path } so you can test a file browser with zero disk access.
  • Unit-test components by calling update(msg) and asserting on state or view(). style.stripAnsi(view) gives you the visible text.

Examples

Runnable, one per concept, in examples/:

counter · form · controls · list · table · dashboard · pager · progress · paginator · mouse · textarea · timer · filepicker · claude-code

bare examples/dashboard.js

Building your own component

Components are just models, so you can build your own and they'll compose like the built-ins. Keep to the conventions the built-ins follow — a create() factory, update → [model, cmd] / view → string, ignore unrelated messages, gate input on a focused flag, define keymaps with key.binding, stay style-agnostic, and do animation/I/O through commands (with an injectable, mockable dependency). The shipped components are short and meant to be read; copy the closest one. The tea-tui skill in this repo walks through it in detail.

Acknowledgments

Deeply indebted to Charm — bare-tui is a port of the ideas in Bubble Tea, Bubbles, and Lip Gloss to the Bare runtime. Built on Bare by Holepunch.

Logo

tui-bird

A Tui bird of New Zealand

License

Apache-2.0