Ramblings of an aging IT geek
← Ramblings of an aging IT geek
linux

awob: Another Wayland Overlay Bar

A drop-in replacement for wob with KDL theming, typed IPC, and a small ecosystem of self-supervising event listeners.

Listen to this post
A laptop with a slim status bar glowing along the top of the screen

If wob covers what you need, use wob. It's tiny, fast, and battle-tested. awob exists for the moment where you want one more thing wob doesn't try to give you.

I've been using wob for years. It does exactly one thing, reading a number from a FIFO and drawing a bar, and it does it without ceremony. Volume up? A bar. Brightness down? A bar. Battery at 5%? A red-ish bar. That's the entire shape of it, and most days it's exactly the right shape.

But the moments where it isn't quite enough kept piling up. I wanted an icon next to the bar to remind me whether I was looking at volume or brightness. I wanted the active sink's name when the volume changed. I wanted a different style for "muted" versus "loud". I wanted a battery OSD to fire on AC plug immediately, not twenty seconds later when UPower's polling timer caught up.

Each of those was solvable in shell glue: a wob call wrapped in a script that read the audio state and rendered text with notify-send. But that's not really wob's fault. It's me wanting something wob doesn't try to be. So I built awob, Another Wayland Overlay Bar.

What's different

Same shape as wob: a transient overlay that appears, draws something, fades out. Same Wayland surface model (wlr-layer-shell-v1, no GTK, no Qt). What changed:

  • The theme is a data file, not a soup of CLI flags. The whole visual is described in a KDL scene file: a palette, named styles, an element tree (rectangles, text, icons, the bar) and an animation timeline. Hot-reloaded on save: change a colour, see it on the next OSD.
  • Listeners are processes, not shell glue. PipeWire volume, sysfs battery via udev, backlight, keyboard backlight; each is its own small binary that subscribes to its upstream and forwards typed events. The daemon's supervisor auto-discovers them on PATH and respawns crashes.
  • IPC is typed, not positional. Instead of writing 78 critical into a FIFO you send awob send --preempt --icon audio-volume-high volume 78 100. The wob FIFO format still works through a shim listener, so existing keybinds keep working.

A theme is one file

The default theme is deliberately "every concept the engine supports, in one self-contained file". It doubles as documentation:

palette {
    bg     "rgba(28,28,35,0.85)"
    fg     "#f3e8d7"
    track  "rgba(255,255,255,0.08)"
    low    "#8fdc55"
    normal "#baea96"
    warn   "#e89a49"
    crit   "#dc8855"
}

surface {
    width 360 height 64 anchor "bottom" offset 0 -56
    fade-in "150ms"  show "2000ms"  fade-out "150ms"
}

scene {
    rect z=0 width="100%" height="100%" radius=12 fill="$bg" shadow="0 8 24 rgba(0,0,0,0.4)"
    image z=1 src="{$icon ?? icon($event)}" x=14 y="center" width=22 height=22
    text z=1 value="{truncate($app ?? label($event), 24)}" x=46 y=14 font="Inter 14 500" colour="$fg"
    text z=1 anchor="top-right" value="{int($progress * 100)}%" x=14 y=14 font="Inter 14 500" colour="$fg"
    bar z=2 x=46 y=42 width="100%-60" height=8 radius=999 fill="$accent" min=0 max="$max" value="$value" from="{$lastValue ?? $value}"
}

$value, $max, $lastValue, $event, $app, $icon are bindings populated at send-time; $lastValue is what makes the bar tween smoothly between consecutive sends. A second shipped theme, wob, is a pixel-faithful clone of upstream (so existing rices keep working), and you switch between them at runtime with awob theme set wob.

The listener that justified the design

The interesting case is the battery listener. The first cut used UPower over D-Bus, because that's what every other Linux app does. The problem: UPower polls the kernel for some events on a thirty-second timer, so plugging in AC at 11% wouldn't fire the "charging" OSD for up to half a minute.

But sysfs has the data already. /sys/class/power_supply/BAT0/status flips from Discharging to Charging the instant the kernel fires a power_supply uevent. So: drop UPower, subscribe to udev, read sysfs directly. AC plug to "charging" OSD now lands in a few hundred milliseconds, and the zbus dependency falls out of the listener entirely.

There's a wrinkle: on some hardware (Dell, ThinkPad, Framework) the battery driver lags the AC-adapter uevent by a few seconds, so reading sysfs at AC-event time sees the old state. A five-second burst-poll window after any uevent re-reads sysfs every second and handles it cleanly. Adding a new event source is a new binary, not a daemon patch, which is the whole point.

Five-minute install on Arch

paru -S awob-bin awob-listeners-all
systemctl --user enable --now awob.service

That's enough anywhere systemd wires up graphical-session.target (Hyprland with uwsm, GNOME, KDE). On Hyprland without uwsm, add exec-once = awob-daemon to hyprland.conf instead.

What it deliberately isn't

It's not a notification daemon: for toast notifications run mako, swaync, or histui (the one I run alongside it). It's not cross-platform; Wayland and Linux only. And it's not a kitchen-sink OSD framework: rectangles, text, icons, a bar, a shadow, a value tween. That's the lot.

Pre-1.0 as I write this, so the wire format and theme schema may still shift; pin a version in scripts. Repo and docs are on GitHub; issues and PRs welcome.