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.
Avatar style — a faithful pixel Clawd acts out the session state:
Minimal style — everything visible at once:
############################ 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
############################
| 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 |
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
.animfiles generated byanimgen.py— a from-scratch Python encoder for the firmware's undocumentedbicycle0animation 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.
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.
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:
Styles are a runtime option, not separate branches — every release contains both.
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_Stunes 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 withpython3 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):Picking it toggles the display on, picking another theme toggles it off. (In
automode the theme is unrelated to the status display — it's just a theme.)python3 claude_card.py installbinds the physical CUSTOM key to it (backs up your current card;restoreundoes). -
off— data bridge only (GET /statuson127.0.0.1:8765and the USB interface for the future on-device app).
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"--lanmakes the hub listen on0.0.0.0:8765(BUSYBAR_LISTEN).--hubwritesBUSYBAR_HUBon the client:report.pyposts 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 inenv.sh; a running hub daemon is restarted for you.<hub-name>.localis the hub's Bonjour/mDNS name (macOS: System Settings → General → Sharing → Local hostname; Windows 10 1703+ resolves.localnatively). If it doesn't resolve on your network, use the hub's IP and give it a DHCP reservation in your router.--tagmarks that computer's sessions on the display: a#RRGGBBcolor 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.
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/standbyon it shows what it thinks;GET /hubon either daemon shows role, style and device health. - Sessions are mirrored as ages, not timestamps (the two clocks may
disagree by seconds),
stateonly when it changed (so even a hub running an older daemon arbitrates as if the hooks had reached it directly; the lease and/redrawneed 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
--stylethe 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-standbyturns a standby back into a plain forwarder. installends 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.
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.
Things discovered the hard way, verified on-device:
rectangleelements have an undocumentedborder_widthdefaulting to a 1px white border — thin rectangles render pure white unless you sendborder_width: 0./api/screenreturns the framebuffer base64-encoded in BGR order (screenshot.pyhandles it).- The
smallfont is proportional (~3.8px digits); measure on-device before doing pixel layout. - The
.animformat (bicycle0): BGRA8888/BGR888/Gray4 + RLE + inter-frame collapsing + named sections.animgen.pyimplements a compatible encoder with a decode round-trip check. - Writing
manifest.jsonor 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_msrequired;type: NOT_STARTEDends one). The two physical mode keys map to/api/busy/profiles/{busy|custom}. storageAPI: write = POST raw body, remove = DELETE, rename takespath+new_path.- Re-uploading an
.animthat is currently being played fails with "Failed to open file for writing" — clear the element (freeing the file handle) before uploading.
| 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) |
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.


