otter-bar and similar daemons use widget structs, not Surface Description trees. Each widget is a Zig struct with draw, setArea, getWidth, and optional input hooks. Layout code lines them up in a row and calls those methods directly.
Stick to this path for config-driven status bar rows. New windows and forms should use Surface Description.
Widget shell
Every bar widget embedsui.Widget:
Widget tracks area and pointer motion state. No vtable on the hot path for bar specs (see below).
Widget registries
FieldRegistry(Owner, specs)
Maps layout string names ("clock", "volume") to optional struct fields on the bar owner:
getWidth, setArea, hit test, popup enumeration, damage collection, drawByName.
NamedWidgetList(Owner, spec)
Same dispatch for dynamic lists like button_menu:
Draw specs (widgets/specs/)
Bar widgets render through draw specs under widgets/specs/ (for example widgets/specs/clock.zig). Specs are comptime structs with draw(frame, clip, ...) that call drawing primitives or emit Surface Description subtrees for complex popups.
Registries invoke specs plus widget hooks for:
- Input (
click,scroll,motion) - Popups (
getPopupSupport, volume/MPRIS/network menus) - Damage (
collectDamage,markFullRedraw)
Available bar widgets
| Widget | Config name | Notes |
|---|---|---|
Clock | clock | Time/date, optional calendar command |
Battery | battery | UPower D-Bus |
Brightness | brightness | Backlight sysfs, scroll to adjust |
Button | button_menu, custom | Runs shell command |
Workspaces | workspaces | otter-tag / ext-workspace / dwl IPC |
ActiveWindow | title | Focused window title |
PowerProfiles | power_profiles | power-profiles-daemon |
CpuLoad | cpu_load | /proc CPU percent |
CpuTemp | cpu_temp | Thermal zones |
Memory | memory | RAM percent |
Network | network | NetworkManager stats and menu |
Weather | weather | Reads otter-weather cache file |
Falcond | falcond | SCX scheduler status |
Volume | volume | PipeWire, scroll, device popup |
Mpris | mpris | Media player controls |
SystemTray | system_tray | StatusNotifierItem row |
otter-bar.conf (workspaces_enabled, falcond_enabled, etc.). See otter-bar.
D-Bus and PipeWire dependencies
Several widgets need runtime services:| Widget | Requires |
|---|---|
| Battery, PowerProfiles, MPRIS, SystemTray, Network | D-Bus (-Denable_dbus=true) |
| Volume | PipeWire (-Denable_pipewire=true) |
-Denable_dbus=false or -Denable_pipewire=false to drop widgets at compile time.
Popups and tooltips
Widgets implementinggetPopupSupport can open PopupMenu or custom layer surfaces (volume slider, network Wi-Fi list). HoverState drives tooltip delay. Popups render in a separate draw pass with damage tracking.
Damage and redraw
Bar polls D-Bus, timers, and config inotify. Each widget reports damage via the registry soDamageTracker gets minimal rects. Full redraw on theme or config reload.
Adding a custom bar widget
- Create
widgets/my_widget.zigstruct withdraw,setArea,getWidth. - Add
widgets/specs/my_widget.zigdraw spec. - Register field in
otter-barconfig schema and layout parser. - Add
FieldSpecentry to the bar registry.
NamedWidgetList and button_menu config entries instead of a new widget type.
Surface Description in bar popups
Some widgets (volume popup, MPRIS expanded) build a smallUiState internally for the popup panel while the bar strip stays on the widget path. Hybrid pattern: bar strip = widget spec; popup = Surface Description.
Next
- Surface Description: default path for new apps
- otter-bar: end-user configuration
- Creating an app: choose surface type and lifecycle

