A status bar for Sway. GPU-rendered with
iced on wlr-layer-shell — no GTK, no config DSL. Widgets are
native Rust, or sandboxed WASM you install from a registry.
The default look: lilac islands — square panels floating over your wallpaper, filled GPU sparklines, a dark base. Square, not rounded; dark, not busy. Drop in a preset to reskin the whole thing in seconds.
- GPU-rendered, and it shows. Filled sparklines, eased workspace cross-fades, live graphs — drawn on the GPU via iced/wgpu, never pixmaps.
- A real plugin platform — two ways. A widget is just Rust (one trait, no IPC, no DSL). Or ship a sandboxed WASM plugin: it runs in a wasmtime sandbox, can only do read-only, user-granted things, hot-reloads, and installs from a git-backed registry with consent pinned to the binary's content hash. No other status bar has this.
- Live everything. Theme, presets, and config reload instantly — no restart; reskin from
the on-bar
▾. - Real popups. Hover/click panels are actual layer-shell surfaces, not floating overlays.
- One static binary. No daemon zoo, no GTK;
config.tomlis plain TOML and fully optional.
Built-in widgets: workspaces · window title · clock · CPU / temperature / memory / ping
graphs · volume · battery · media (MPRIS) · GitHub · Google Calendar · Spotify · kubectl ·
stock ticker · live Claude-usage · and a no-code custom command widget. Clicking, scrolling,
and hover popups all work.
Needs stable Rust (≥ 1.88) and a Wayland + Vulkan stack.
Arch / AUR:
yay -S ezbarArch:
sudo pacman -S --needed rust wayland libxkbcommon vulkan-icd-loader fontconfig
# + a driver: vulkan-radeon | vulkan-intel | nvidia-utilsDebian/Ubuntu:
sudo apt install pkg-config libwayland-dev libxkbcommon-dev libvulkan1 \
mesa-vulkan-drivers libfontconfig1-devThen:
cargo build --release # -> target/release/ezbar./target/release/ezbarAdd it to your Sway config — ezbar install does this for you (idempotent, and
it never edits or removes an existing line; it backs up the config and appends
one exec_always), or add it by hand:
exec_always /path/to/ezbar
ezbar is a thin launcher: it respawns the bar if the output disappears (monitor
sleep / hotplug). For a single foreground instance (debugging):
EZBAR_CHILD=1 ./target/release/ezbarEverything lives under ~/.config/ezbar/ and is optional — a widget with no
config just stays quiet. Nothing is hardcoded; no secrets ship in the binary.
Every module's [modules.<id>] options are listed in
docs/config-reference.md.
| Widget | Reads from |
|---|---|
| Calendar | calendar_url (your secret iCal URL) or $GOOGLE_CALENDAR_ICAL_URL |
| GitHub | $GH_TOKEN / $GITHUB_TOKEN / gh auth token; optional github_config.json (reasons, exclude_repos) |
| Spotify | spotify_config.json (client_id, client_secret); or $SPOTIFY_ACCESS_TOKEN |
| Stock | $EZBAR_STOCK_SYMBOL (default NQ=F), $EZBAR_STOCK_API_KEY (optional) |
| Ping | [modules.ping].target (default 8.8.8.8) |
Theming is live — edit [theme] in ~/.config/ezbar/config.toml and the bar
re-skins instantly, no restart:
[theme]
style = "islands" # islands (default, floating square panels) | solid (one slab)
primary = "#cba6f7" # the accent — workspace chip, pill borders, highlights (graphs are per-widget, see below)
background = { base = "#1e1e2e", weak = "#313244", strong = "#45475a" }
text = "#cdd6f4"; ok = "#a6e3a1"; warn = "#f9e2af"; urgent = "#f38ba8"Six ready palettes ship in presets/ — ezbar-dark (the lilac-islands
default), noir (the original flat black slab), catppuccin-mocha, gruvbox-dark,
nord, tokyo-night. Drop them into ~/.config/ezbar/presets/, then switch live
from the on-bar ▾, or from a keybind:
ezbar msg preset gruvbox-dark # or: preset next | prev
ezbar msg reload # popup <kind> | volume <up|down|mute>Presets are theme-only TOML bundles with a [palette] variable layer (primary = "$mauve"), selected via a state file that never edits your config.toml. Workspace
chips come in four square styles (boxed · filled · outlined · underbar).
Full design: RFC 0002.
The inline sparklines (cpu · temperature · memory · ping) are functional by
default: they run green→red by load, so a pinned core looks hot. That colour carries
meaning, so it isn't a global theme token — it's a per-widget knob under
[modules.<id>.graph], not [theme]:
line_color |
line |
|---|---|
"threshold" (default, or omitted) |
green→yellow→orange→red by load |
"accent" "ok" "warn" "urgent" "fg" "fg_dim" |
that theme token, flat |
"#rrggbb" / "#rrggbbaa" |
that literal colour, flat |
# cpu keeps the functional default — leave it out entirely for this
[modules.cpu.graph]
line_color = "threshold" # green when idle, red when pinned
[modules.temperature.graph]
line_color = "accent" # flat lilac, matches the bar accent
[modules.memory.graph]
line_color = "#89b4fa" # a fixed blueWant every graph flat-accent (the r/unixporn ricer look)? Set line_color = "accent" on
each — it's deliberately opt-in per widget, so going aesthetic never silently drops the
load signal on a monitor you forgot about. A typo falls back to threshold, so a bad
value can't blank a graph.
| Widget | Action |
|---|---|
| cpu / temp / mem / ping | click the label to toggle its graph |
| volume | click to mute, scroll to change |
| media | click to play/pause, scroll to skip; hides when nothing's playing |
| kubectl | left-click clears the context, right-click opens the picker |
| calendar | click for today's meetings; blinks when one is imminent/ongoing |
| github | click for the grouped list; click a row to open + mark read, right-click to dismiss, [clear all] to mark all |
| spotify | click to play/pause (or authorize), scroll to skip; long titles marquee |
1 · Compile-in modules (Rust). A widget is a Module: just iced — a view, an
update, an optional background subscription and popup. No IPC, no DSL, no config schema.
Develop one in a normal desktop window with the harness — no bar, no sway needed:
cargo run -p ezbar-harness --example counter # a complete starter widget
cargo run --example harness -- github # preview a built-in moduleGuide: ezbar-plugin-author ·
Design: RFC 0001.
2 · Sandboxed WASM plugins. Ship a widget anyone can run safely. It compiles to a
wasm32-wasip2 component, runs in a wasmtime sandbox, hot-reloads, and can only do
user-granted, fine-grained things — default-deny, scoped per [modules.<id>]:
| cap | grant | what it allows |
|---|---|---|
network |
["api.example.com", "*"] |
HTTP-GET to allow-listed hosts (* = any) |
feeds |
["cpu", "*"] |
host-sampled cpu/memory/temperature/battery/net |
sway |
true |
read-only workspace list + focused title |
fs |
[{ path = "~/notes", mode = "rw" }] |
preopened dirs (WASI-jailed; r/rw) |
exec |
["kubectl"] |
run allow-listed programs (any args) |
Or flip [plugins] yolo = true to grant every plugin everything ("I trust my plugins,"
like Claude Code's bypass). Even in yolo the resource sandbox holds (cpu/mem/epoch) — a
plugin can read your files but can't hang or OOM the bar. fs/exec are a dangerous tier:
never silently activated by ezbar add — you grant them by hand (or yolo). Design:
RFC 0015.
Consent is bound to the plugin's content hash, not its name — a swapped binary inherits nothing (it runs sandboxed until you re-approve). The whole lifecycle is a few commands:
ezbar search <term> --registry <dir|git-url> # discover plugins in a registry
ezbar add <id> --registry <dir|git-url> # resolve newest in-window version → verify sha256 → install → print grant block
ezbar update [<id>] # re-install newer versions (skips already-latest; remembers each plugin's source)
ezbar list # installed plugins: version, consent state, declared caps
ezbar inspect plugin.wasm # what it declares + the exact [modules.<id>] block to paste
ezbar grant <id> # approve its current bytes (re-run after a legit rebuild/update)
ezbar remove <id> # uninstall (.wasm + consent + pin — never your config.toml)
ezbar package plugin.wasm # author: embed an ezbar:manifest + print the registry entryA plugin may embed a manifest declaring what it needs; ezbar inspect reads it and prints
the grant block to paste (it never writes your config), and the host warns if a plugin
wants a capability you didn't grant. The registry is just plugins/<id>/<version>.toml index
files in a git repo (host it anywhere — --registry takes a local dir or any git URL);
installs are sha256-verified, WIT-version-negotiated, and TOFU publisher-pinned.
Guide: ezbar-wasm-plugin-author ·
Design: RFC 0014.
src/ the bar: state, update, view, subscriptions, launcher
src/sources/ one module per data source (off-thread I/O via spawn_blocking)
src/modules/ built-in widgets (RFC 0001) + the inline-markup renderer
src/{grants,registry, the CLI: capability consent + the plugin registry
package,install}.rs (grant/inspect/add/list/remove/package/install)
crates/ezbar-plugin the module SDK — the Module trait, stable API
crates/ezbar-harness standalone dev harness for modules
crates/ezbar-wasm the wasmtime host — sandboxes + drives WASM plugins (the reactor)
crates/ezbar-plugin-wasm the WASM guest SDK — write a plugin in Rust (TinyGo too)
Elm architecture: one State, one Message, update, view, and one
Subscription stream per data source. Popups are extra layer-shell surfaces
(the multi-window daemon pattern). WASM plugins run on a single shared wasmtime
reactor, capability-gated and epoch-bounded.
MIT © Johannes Brüderl