TimMikeladze / menubar

#menubar

platformmacOS plugins8 licenseMIT built withElectrobun

A macOS menu bar app for the things you check between builds: pull requests, deployments, service status, feeds, a focus timer, colour maths, time zones, and a garden that grows on whichever metric you point it at.

Lives in the menu bar — no Dock icon, no window to manage. ⌘⇧D from anywhere, or click the tray icon. Built with Electrobun, so the whole thing is a webview and a Bun process rather than a bundled Chromium.

brew install --cask timmikeladze/menubar/menubar

D from anywhere · macOS 11+, Apple silicon

Live The app itself, built from this repo and pointed at invented data. Click anything.

## Getting started

Download the signed build, or run it from source — Bun and a Mac are the whole toolchain.

bash
# Install — Apple silicon, macOS 11+
$ brew install --cask timmikeladze/menubar/menubar

# Or from source
$ bun install

# Develop — one command, HMR included
$ bun run dev

# Build a release
$ bun run build          # stable channel
$ bun run build:canary   # canary channel

# Cut a release — sets both version fields, then tag
$ bun run version 0.2.0

## Built-in Plugins

8 plugins ship in the app. 6 of them need no account, no token and no network beyond a public endpoint. Pick one to open it in the popover.

Same app, same data. It just starts on whichever plugin you picked.

## How it works

Every plugin that fetches does it from a background service rather than its tab: menubar only mounts the active plugin, so a component-owned poller would leave the badge and tray title frozen until you happened to open that tab. Each service caches its last payload, so reopening the popover paints rows instead of skeletons, and polling pauses whenever the popover is off screen.

The GitHub plugin reads via the GraphQL API — three concurrent searches, ~3s, because GitHub runs aliased searches serially inside one document. The only writes it makes are marking notifications read.

Feeds are fetched by the bun process rather than the webview: a site that publishes a feed almost never sends Access-Control-Allow-Origin, and the conditional If-None-Match that keeps polling cheap would be preflighted away even where it does. Feed markup is rewritten against a tag-and-attribute allowlist before it's rendered, and images are off until you ask for them.

The Garden reduces every metric to one number per local day, so growth, wilting and death are a fold over a plain series and live in garden.ts with no React, storage or network in sight. Sources that GitHub can hand back with history (commits, pull requests, reviews, issues, releases) are fetched — and only the ones something is actually planted on, so a garden of pomodoros makes no request at all. Sources with no history to fetch (pomodoros, focus minutes, Vercel deployments, hand-counted habits) are written to a local ledger as menubar sees them, and the ledger always wins over a fetched day. Plants are drawn procedurally from ten silhouettes and a palette, so the catalog costs kilobytes rather than sprite sheets.

## Project structure

A Bun process and a webview, talking over one typed RPC schema.

tree
├── src/
│   ├── bun/
│   │   └── index.ts        # Main process: tray, popover, RPC handlers
│   ├── shared/
│   │   └── rpc-schema.ts   # The contract between the two sides
│   └── mainview/
│       ├── plugins/        # One folder per plugin
│       ├── lib/            # Bridge, stores, plugin registry
│       ├── components/     # Shared UI
│       ├── App.tsx         # Tab shell
│       └── index.css       # Tailwind theme
├── icon.iconset/           # App icon, converted to .icns at build time
├── electrobun.config.ts    # App metadata, bundle, release channel
└── vite.config.ts

## Tokens

Keychain, not local storage

The GitHub and Vercel tokens live in the login keychain, as menubar (github), menubar (github via gh) and menubar (vercel) — not in the webview's own storage, which is an unencrypted file inside the app's data folder. Anything an older build left there is moved across on first launch, and the plaintext copy is deleted once the keychain has taken it. Settings → Connections removes them. The view holds them in memory for the session and never writes them back to disk itself.