Skip to content

Repository files navigation

busybar-claude-status

Turn a BUSY Bar into a live status display for Claude Code: session state, model, reasoning effort, context-window usage and plan rate limits — rendered on the 72×16 front LED matrix with buttery-smooth native animations.

中文文档 / Chinese docs

Avatar style — a faithful pixel Clawd acts out the session state:

Avatar style

Minimal style — everything visible at once:

Minimal style

############################    1px per-pixel animated ring (.anim, 25 fps)
#  Fable 5 max      [##----] #  model + effort (colored by /effort level)
#  5h85% 7d97%        WORK   #  plan remaining · ctx bar · state word
############################

What it shows

Element Meaning
Ring animation Session state, played natively by the firmware's own .anim decoder (same one as the built-in keep out theme): rainbow marquee = WORKING, purple wave = THINKING, green breathing = COMPLETE, orange pulse = WAIT (+ status LED), red blink = ERROR/FAILED, dim gray = IDLE
Model + effort e.g. Fable 5 max, colored with Claude Code's own theme palette per /effort level (inactive gray / permission blue / warning yellow / fastMode orange / effortUltra purple)
Ctx bar Context-window usage, green → yellow → orange → red
Plan usage 5h85% 7d97% — remaining % of the 5-hour and 7-day rate-limit windows; turns orange ≤25%, red ≤10%
State word THINK / WORK / WAIT / ERR / FAIL / DONE / IDLE

Ring only

How it works

statusline command --.                                    USB (10.0.4.20)
                     +--> daemon.py :8765 ---------------> BUSY Bar
settings.json hooks -'      |  session store               /api/display/draw
                            +--> GET /status               (pre-uploaded .anim
                                 (future on-device app)     assets, native fps)
  • Claude Code's statusline JSON (model, effort, context window, rate limits) and hook events (UserPromptSubmit, Pre/PostToolUse, Stop, PermissionRequest, …) are forwarded to a tiny local daemon.
  • The daemon keeps per-session state (multiple Claude sessions supported, even across several computers — the one you last talked to wins) and renders to the device over the HTTP API. Ring animations are pre-rendered .anim files generated by animgen.py — a from-scratch Python encoder for the firmware's undocumented bicycle0 animation format — uploaded once and played by the device itself, so the animation is perfectly smooth with near-zero traffic.
  • Everything is Python 3 stdlib. No dependencies.

Not just Claude: the daemon core is provider-agnostic. Codex, Cursor, CI jobs — anything that can run one curl — can drive the display through POST /v1/report. Claude-specific semantics (effort colors, 5h/7d plan windows) live in a built-in adapter. Wi-Fi and cloud transports are selectable via BUSYBAR_TRANSPORT; a BLE transport is designed. See docs/EXTENDING.md.

Install

Requirements: macOS, Linux or Windows; Python 3.9+; a BUSY Bar connected over USB (firmware 1.1.x); Claude Code with statusline + hooks support. On Windows use py/python instead of python3 — every entry point resolves the interpreter via sys.executable, and the glue layer (report.py, adapters/codex_notify.py) is pure Python with no bash/nohup/pgrep dependencies. (report.sh remains for existing POSIX installs.) Verified on a real Windows machine as a hub client (hooks + statusline forwarded over Wi-Fi, see below); running the daemon itself on Windows with the Bar on its USB port is untested — issues welcome.

git clone https://github.com/Alpharius-003/busybar-claude-status
cd busybar-claude-status

python3 animgen.py anims/                 # generate ring animations
python3 - <<'PY'                          # upload them to the device
import animgen, urllib.request
for f, gen in animgen.ANIMS.items():
    blob = animgen.encode_anim(gen())
    urllib.request.urlopen(urllib.request.Request(
        f"http://10.0.4.20/api/assets/upload?application_name=claude_status&file={f}",
        data=blob, method="POST"), timeout=15)
    print("uploaded", f)
PY

python3 setup_claude.py install           # wire into Claude Code (backs up first)

Start a Claude Code session — the daemon auto-spawns on the first statusline refresh and the display appears. setup_claude.py uninstall reverses everything.

Display styles

Two looks, one codebase — pick with BUSYBAR_STYLE (persist it in an env.sh next to daemon.py, e.g. export BUSYBAR_STYLE=avatar):

  • minimal (default) — the layout above: state word + quotas always visible.
  • avatar — a pixel companion (a 1:1 recreation of the Claude Code terminal mascot) acts out the state on the right: typing at a laptop while WORKING (with blinks), light bulb while THINKING, coffee break when DONE, X-eyes on ERROR, zzz when idle — plus a vertical context gauge. The bottom-left slot shows the state as a word and swaps to quotas once the work is done.

A Codex session in avatar style, with the fast badge:

Avatar + Codex fast badge

Styles are a runtime option, not separate branches — every release contains both.

Display modes

Set BUSYBAR_RENDER_MODE (or edit RENDER_MODE in daemon.py):

  • auto (default) — display whenever an agent is active; after 10 minutes of idle the screen is handed back to the device and returns on the next activity (BUSYBAR_IDLE_CLEAR_S tunes this; 0 = keep the display forever).

  • theme — manual, on the device: the display only shows while "claude" is the currently selected BUSY/CUSTOM theme. Install the theme with python3 install_theme.py — a breathing claude-orange ring with the companion typing in the middle; it appears in the device's theme picker (also the screen during a claude-theme focus session):

    Claude theme

    Picking it toggles the display on, picking another theme toggles it off. (In auto mode the theme is unrelated to the status display — it's just a theme.) python3 claude_card.py install binds the physical CUSTOM key to it (backs up your current card; restore undoes).

  • off — data bridge only (GET /status on 127.0.0.1:8765 and the USB interface for the future on-device app).

Several computers, one Bar

Claude Code on a Mac and a Windows PC (any number of sessions each), one display that follows you. The computer the Bar is plugged into runs the daemon as the hub; every other computer runs nothing — its hooks and statusline are forwarded to the hub over the LAN.

# on the computer with the Bar (the hub)
python3 setup_claude.py install --lan

# on every other computer (Windows: py setup_claude.py ...)
python3 setup_claude.py install --hub http://<hub-name>.local:8765 --tag "#00A4EF"
  • --lan makes the hub listen on 0.0.0.0:8765 (BUSYBAR_LISTEN). --hub writes BUSYBAR_HUB on the client: report.py posts straight to the hub, capped at 1.2 s per hook and backed off for 20 s when the hub is unreachable, so an asleep hub never slows Claude Code down. Both persist in env.sh; a running hub daemon is restarted for you.
  • <hub-name>.local is the hub's Bonjour/mDNS name (macOS: System Settings → General → Sharing → Local hostname; Windows 10 1703+ resolves .local natively). If it doesn't resolve on your network, use the hub's IP and give it a DHCP reservation in your router.
  • --tag marks that computer's sessions on the display: a #RRGGBB color draws a 2×5 flag in the free columns left of the model name (costs no text space); one or two letters (--tag W) go after the model name instead, shortening it if needed (Fabl 5 max W).
  • --token SECRET (same value on hub and clients) makes the hub reject LAN reports without it; loopback never needs one. Off by default — the hub is meant for a home network. If the hub runs a firewall, allow inbound TCP 8765 for Python.
  • Codex on a client works the same way: its adapter posts to the hub.

Which session is shown? The display follows attention, not chatter. Among the sessions doing something, the one you last talked to wins — a prompt you submit, a permission request, or a task starting from idle pulls the display; tool calls and statusline refreshes never do. When that session goes idle, whatever is still running surfaces; when everything is idle, the last one you talked to stays. GET /status includes host and host_tag; GET /health lists every session with its focus_ts.

When the hub sleeps: a standby

The hub is usually a laptop. Close its lid and the Bar goes dark — unless a second computer is a standby: it runs its own daemon, mirrors its sessions to the hub while the hub is up, and paints the Bar itself, over the Bar's own Wi-Fi, the moment the hub is gone. Nothing else changes: whenever the hub is awake, the hub decides what is shown.

Once, on the computer with the Bar (over USB): put the Bar on your Wi-Fi (BUSY app → Wi-Fi; curl http://10.0.4.20/api/wifi/status shows its LAN address) and give its Wi-Fi API a key:

curl -X POST 'http://10.0.4.20/api/access?mode=key&key=1234567890'

Then on the standby (Windows: py setup_claude.py ...):

python3 setup_claude.py install --hub http://<hub-name>.local:8765 --standby \
    --transport wifi --device <Bar LAN IP> --device-token 1234567890 --tag "#00A4EF"
  • The standby takes over after three probes in a row fail (about 10 s) — counted only while the Bar itself still answers, so a standby waking from its own sleep never paints over a live hub — or when the hub reports it cannot reach the Bar (unplugged). It hands back the moment the hub answers again: resync first, then the hub repaints, then the standby stops. GET http://127.0.0.1:8765/standby on it shows what it thinks; GET /hub on either daemon shows role, style and device health.
  • Sessions are mirrored as ages, not timestamps (the two clocks may disagree by seconds), state only when it changed (so even a hub running an older daemon arbitrates as if the hooks had reached it directly; the lease and /redraw need the current one), under a 90 s lease refreshed every 30 s — a standby that vanishes takes its sessions with it. A hub restart, or a hub that forgot a session while asleep, is noticed and resynced within seconds.
  • Keep --style the same on both computers (the standby logs a warning if not) and give the Bar and the hub DHCP reservations. The key grants full control of the Bar to anyone on your Wi-Fi: use 10 digits, keep the Bar on a trusted network, rotate it over USB if a computer is lost. --no-standby turns a standby back into a plain forwarder.
  • install ends with two probes — the hub, and the Bar with the key just written — so a wrong key or a closed port is caught right there.

The on-device app (waiting on firmware ≥ 1.2.0)

device_app/ + install_app.py contain a complete "Claude Status" JS app for the device's APPS menu: it polls the daemon's /status over the USB network and renders locally. JS app support ships in firmware 1.2.0-rc; on 1.1.1 the installer paths crash the firmware, so hold off until your device updates — then python3 install_app.py and set RENDER_MODE=off.

Firmware field notes (1.1.1)

Things discovered the hard way, verified on-device:

  • rectangle elements have an undocumented border_width defaulting to a 1px white border — thin rectangles render pure white unless you send border_width: 0.
  • /api/screen returns the framebuffer base64-encoded in BGR order (screenshot.py handles it).
  • The small font is proportional (~3.8px digits); measure on-device before doing pixel layout.
  • The .anim format (bicycle0): BGRA8888/BGR888/Gray4 + RLE + inter-frame collapsing + named sections. animgen.py implements a compatible encoder with a decode round-trip check.
  • Writing manifest.json or binary data into /ext/user_assets/<app>/appmeta/ crashes and reboots the firmware (half-finished JS-app scanner). Theme dirs under /ext/apps_assets/busy/themes/ are safe.
  • While a focus session is running, all canvas drawing is rejected — even at priority 100 (docs say sessions sit at 90; not on 1.1.1).
  • Sessions can be controlled via PUT /api/busy/snapshot (card_id, is_paused, snapshot_timestamp_ms required; type: NOT_STARTED ends one). The two physical mode keys map to /api/busy/profiles/{busy|custom}.
  • storage API: write = POST raw body, remove = DELETE, rename takes path + new_path.
  • Re-uploading an .anim that is currently being played fails with "Failed to open file for writing" — clear the element (freeing the file handle) before uploading.

Repo layout

File Purpose
daemon.py session store + /status + device renderer (stdlib only)
report.py / report.sh statusline/hook forwarder; auto-spawns the daemon, or forwards to a LAN hub when BUSYBAR_HUB is set (unless BUSYBAR_STANDBY) (.py = cross-platform, .sh = POSIX legacy)
setup_claude.py wire into / out of ~/.claude (with backups); --lan / --hub / --standby / --tag / --token / --style for several computers
animgen.py .anim (bicycle0) encoder + the six ring animations
claude_card.py bind the CUSTOM key to the claude theme (and restore)
install_app.py, device_app/ the future on-device JS app
screenshot.py grab the front display as an upscaled PNG
docs/EXTENDING.md reporting protocol v1, adapter guide, transport guide (incl. BLE design)
adapters/codex_status.py Codex adapter (model/effort/speed, context %, quotas — all derived, no name tables)
adapters/install_codex_autostart.py hook the adapter into Codex's notify so it auto-starts on use
install_theme.py install the on-device "claude" theme (ring + typing companion)

Disclaimers

Not affiliated with BUSY or Anthropic. Tested on BUSY Bar firmware 1.1.1 with Claude Code 2.x: the daemon on macOS (Bar on USB), plus a Windows machine as a hub client over Wi-Fi. The standby role was verified on the Mac with a second daemon (daemon.py --port 8766) driving the Bar over Wi-Fi while the hub was frozen (SIGSTOP) — not yet from a real Windows standby. The firmware quirks above may change in any update. MIT licensed.

About

Live Claude Code status on a BUSY Bar LED matrix - state ring animations, model/effort, context & plan usage

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages