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

busybar-webos

v0.2.0

Published

A widget runtime + web portal for the BUSY Bar — including AI-generated pixel movies on demand

Readme

BUSY Web OS

A TypeScript "OS" for the BUSY Bar device: a widget runtime + web portal backed by SQLite.

BUSY Web OS portal — widgets rendered inside the real device frame, with live screen preview

  • Widgets live in widgets/<id>/ as simple classes (class WeatherWidget extends Widget), can bundle images, and draw on the device displays.
  • Portal (default http://localhost:3000): start/stop widgets, edit per-widget config, view per-widget logs, live screen preview.
  • Global settings: device access over USB ethernet (http://10.0.4.20), Wi-Fi LAN (optional access key sent as X-API-Token), or the BUSY cloud (https://api.busy.app, Bearer token from cloud.busy.app/api-tokens). First run shows an onboarding page until a connection is saved; the bar's Wi-Fi HTTP access (/access) is manageable from the portal.

Getting started

Run it directly:

npx busybar-webos

Or from a clone:

pnpm install
pnpm dev        # dev server with reload
pnpm start      # plain start
pnpm typecheck  # tsc --noEmit

Data (SQLite DB + your own widgets) lives in ~/.busybar-webos/ (override with BUSYBAR_DATA_DIR), created automatically. A git clone with an existing data/busybar.db keeps using data/. Custom widgets dropped in ~/.busybar-webos/widgets/<id>/ are loaded alongside the bundled ones and survive package updates.

Writing a widget

Create widgets/my_widget/index.ts:

import { Widget } from '../../src/core/widget';

export default class MyWidget extends Widget {
  static title = 'My Widget';
  static description = 'What it does.';
  static tags = ['tools'];
  static author = 'your-github-username'; // shown and linked on the widget card
  static configSchema = {
    githubApiToken: { type: 'secret' as const, label: 'GitHub token', required: true },
    refreshMinutes: { type: 'number' as const, default: 5 },
  };

  async start() {
    // optional: push an image from widgets/my_widget/assets/ to the device
    await this.uploadAsset('icon.png');

    // run immediately, then every N ms; errors are logged, loop keeps going
    this.every(Number(this.config.refreshMinutes) * 60_000, () => this.refresh());
  }

  private async refresh() {
    this.log.info('refreshing…');
    await this.draw([
      { id: 'icon', type: 'image', path: 'icon.png', x: 0, y: 0, timeout: 0 },
      { id: 'label', type: 'text', text: 'Hello', font: 'normal', x: 18, y: 8, align: 'mid_left', timeout: 0 },
    ]);
  }

  async stop() {
    // timers are cleaned up and the display cleared automatically
  }
}

Restart the server (or let pnpm dev reload) to pick up new widgets.

Widget API

| Member | Purpose | | --- | --- | | this.config | Effective config values (schema defaults + portal overrides) | | this.launch | Values from the Start modal (see launchSchema), fresh on every start | | this.log | Per-widget logger (info/warn/error/debug), visible in the portal | | this.every(ms, fn) | Run fn now and on an interval; auto-cleaned on stop | | this.draw(elements, opts?) | Draw on the device (application_name injected) | | this.clear() | Clear this widget's display elements | | this.uploadAsset(name) | Upload widgets/<id>/assets/<name> to the device | | this.bar | Full BusyBarClient (audio, brightness, input, …) |

Config field types:

| Type | Portal input | Stored value | | --- | --- | --- | | string | text (optional pattern regex) | string | | secret | masked text | string (plain text in SQLite) | | number | number | number | | boolean | checkbox | boolean | | color | color picker | #RRGGBBAA (device format) | | location | "use my location" + text fallback | "lat,lon" | | select | dropdown (requires options: [{value, label?}]) | string (one of the option values) |

Mark fields required: true to block install/start until the user explicitly sets them — a schema default does not satisfy a required field. Values are validated server-side on save — an invalid value (bad color, malformed coordinates, regex mismatch…) returns 400 and nothing is stored.

Launch fields

A widget can also declare static launchSchema (same field format as configSchema). When it's non-empty, clicking Start in the portal opens a modal asking for those values; they are validated (coerceLaunchValues), passed to the widget as this.launch, and not persisted — every start asks again. Example: the ai-pixels widget asks for a prompt and a movie duration.

Preview image

Widget cards in the portal show a preview of the widget's rendering if the widget ships one. Accepted locations (first match wins, extensions png/bmp/jpg/webp):

  • widgets/<id>/preview.<ext>
  • widgets/<id>/assets/preview.<ext>
  • widgets/<id>/assets/<id>.<ext> (e.g. weather/assets/weather.bmp)

Ideal ratio is 72:16 like the front display. No file, no preview.

The front display is 72×16 px; keep drawings small. See the device OpenAPI spec for all element types (text, image, animation, countdown, rectangle).

Installing widgets

The portal has two tabs: Installed widgets (startable) and All widgets (the catalog of everything in widgets/), with a search bar and tag filters. Widgets declare their categories with static tags = ['music', 'fun'].

  • A widget with no required config installs with one click on Install.
  • A widget with required config fields must have a valid configuration first: its page shows Validate configuration — if the values pass server-side validation, the widget becomes installed.
  • Widgets can define their own install checks: static validateInstall(config) runs server-side (e.g. AI Pixels pings the configured LLM, music widgets probe the desktop player and trigger the macOS automation consent), and browser sources (e.g. the Decibel widget's microphone) request their permission in the portal before installing.
  • Uninstall removes it from the installed list and deletes its stored config; only installed widgets can be started.

Notifications

POST /api/notify shows a phone-style notification on the bar — icon on the left, title + scrolling text, LED blink in the icon's color, and a notification sound:

curl -X POST localhost:3000/api/notify -H 'Content-Type: application/json' \
  -d '{"title": "Deploy done", "text": "busybar-webos v1.2 is live", "icon": "success"}'

| Field | Default | Notes | | --- | --- | --- | | text | — | required, printable ASCII | | title | none | bold first line; without it the text is centered | | icon | info | info success warning error message bell | | duration | 6 | seconds on screen (1-300) | | priority | 95 | 1-100 — 95 shows over a running BUSY session | | sound | on | false = silent, or a custom stock path (shared/…) | | led | icon color | #RRGGBBAA LED blink override |

HTTP API

The portal is a thin client over the server API:

  • GET /api/widgets, GET /api/widgets/:id
  • POST /api/widgets/:id/start (body: {launch: {...}} for widgets with a launchSchema), POST /api/widgets/:id/stop
  • POST|DELETE /api/widgets/:id/install — install (400 if required config missing) / uninstall
  • PUT /api/widgets/:id/config
  • GET /api/widgets/:id/logs?limit=100
  • GET|PUT /api/settings
  • GET /api/device/status, GET /api/device/screen?display=0|1
  • POST /api/device/test — probe a candidate connection (body: partial settings) without saving it
  • GET|POST /api/device/access — the bar's Wi-Fi HTTP access setting ({mode: disabled|enabled|key, key})