sxbar

A small, fast status bar for Xorg — one C99 binary, configured entirely through a plain-text file. Workspaces, system stats, floating popups with sliders and menus, all driven by shell commands you control.

C99 Xlib + Xft + Xinerama ~36 KB binary no daemon dependencies

Overview

sxbar renders a slim EWMH workspace switcher plus a row of modules — clock, battery, volume, scripts you write yourself, anything you configure — and can pop a floating slider or menu window out of any module.

Everything is controlled from a single config file, read from (in order) $XDG_CONFIG_HOME/sxbarc, $XDG_CONFIG_HOME/sxbar/sxbarc, ~/.config/sxbarc, ~/.config/sxbar/sxbarc, then a system-wide fallback at /usr/local/share/sxbarc. There's nothing to recompile to change modules, colours, icons, click actions or popups — it's all config-file syntax of the form key : value.

Multi-monitor is automatic: sxbar detects screens via Xinerama and opens one bar per monitor, each with its own workspace switcher filtered to windows actually on that screen.

Install & build

Standard GNU make build. Needs Xlib, Xinerama and Xft (with freetype2 headers) development packages, plus a C99 compiler. Album art (popup_image, the media module) needs no extra package — image decoding is a vendored, public-domain single-header library (src/stb_image.h) compiled straight into the binary.

git clone <this repo> sxbar
cd sxbar
make
sudo make install       # PREFIX defaults to /usr/local
Installed toWhat
$PREFIX/bin/sxbarthe binary
$PREFIX/share/man/man1/sxbar.1man page
$PREFIX/share/sxbarcfallback default config
$PREFIX/share/sxbar/scripts/reference icon/demo scripts — copy to ~/.config/sxbar/scripts/ and edit your own copy

Start it from your window manager's autostart / .xinitrc with a plain sxbar &.

Quick start

Copy the shipped default config and enable a few modules to get oriented.

mkdir -p ~/.config/sxbar
cp /usr/local/share/sxbarc ~/.config/sxbar/sxbarc
font                : JetBrainsMono Nerd Font:size=10
background_colour   : #000000
foreground_colour   : #7abccd

module : clock   : true  : 1
module : date    : true  : 60
module : battery : true  : 30
module : volume  : true  : 5

prefix : clock   : " "
colour : clock   : #50fa7b

Restart sxbar to pick up changes — it re-reads the config on startup, not live.

Global & bar options

Set once at the top of the config; apply to every bar unless noted.

keydefaultdoes
height19bar height in px
bottom_barfalsedock at the screen bottom instead of top
vertical_padding0gap between the bar and the screen edge
horizontal_padding0gap on both sides of the bar
text_padding0inner padding before the first workspace/module
border / border_widthfalse / 0draw a border around the bar window
background_colour
foreground_colour
border_colour
#000000
#7abccd
#005577
hex (#rrggbb) or X colour name. These three don't support a trailing # comment on the same line — comment on its own line instead.
fontmonospace:size=8Xft font name, Family:size=N[:style=Bold]. Use a Nerd Font family here to render icon glyphs.
show_version
version_text
true
sxbar ver. 1.1
optional version string at the bar's right edge
secondary_barfalseadds a second, modules-only bar on the opposite edge — see below

Secondary bar

A second bar on the edge opposite bottom_bar — no workspace switcher, no version text, just whichever modules you tag onto it. Shares the primary bar's font/colours/height.

bottom_bar    : false
secondary_bar : true

module : battery : true : 30
module : volume  : true : 5

bar : battery : secondary
bar : volume  : secondary

bar : module_name : primary|secondary defaults to primary — clock, date and the workspace switcher always stay on the primary bar.

Workspaces & monitors

The workspace switcher reads standard EWMH properties, so it works with any EWMH-compliant window manager — no sxbar-specific config needed.

  • Workspace names come from _NET_DESKTOP_NAMES, the current one from _NET_CURRENT_DESKTOP.
  • Each workspace pill shows up to 4 small boxes for windows on it — counted per monitor, so a bar only reflects the windows actually on its own screen, not every window across every screen.
  • One bar opens per monitor automatically (via Xinerama); each computes its own _NET_WM_STRUT_PARTIAL so multiple bars don't clobber each other's reserved screen edge.

Workspace icons

workspace_icon : name : "icon text"

Replaces a workspace's displayed label — its _NET_DESKTOP_NAMES string, typically a plain number like "1" — with different text instead, e.g. a Nerd Font glyph. Purely cosmetic: switching still targets the same underlying desktop, only what's drawn on the pill changes. Needs a Nerd Font set via font to render glyphs. Repeatable, one line per workspace; any workspace without a matching line just shows its plain name, same as always.

workspace_icon : 1 : "<glyph>"
workspace_icon : 2 : "<glyph>"
workspace_icon : 3 : "<glyph>"

Built-in modules

Enabled with module : name : true|false : refresh_interval_seconds. Everything below is opt-in/opt-out by default as shown.

moduleshowsdefaultneedspopup
clockHH:MM:SSonhover → open calendar
dateYYYY-MM-DDonhover → open calendar
batterycharge %off/sys/class/power_supplyhover → status + power-saver toggle
volumevolume %onwpctlhover → slider
cpuusage %off/proc/stathover → cpu / memory / per-core
brightnessbrightness %offbrightnessctlhover → slider
bluetoothOn / Offoffbluetoothctlhover → power/scan/pair menu
usermenucurrent usernameonsystemctlhover → sleep/logout/shutdown
networkOnline / Offlineoffiphover → WiFi/Ethernet + IP
mediaplay/pause glyph + Artist - Titleoffplayerctl, curl for remote arthover → album art + Previous/Play-Pause/Next
taskbarnot like the others — see Taskbar below

Full detail on each popup is in Built-in popups below.

Not special-cased in the binary. Every module — the ones above and any you write yourself — is resolved by name to a script, purely from module : lines in sxbarc; there's no separate "custom module" concept or directive. Each name resolves (in order) to your own copy at ~/.config/sxbar/scripts/<name>.sh, then the reference copy make install places at /usr/local/share/sxbar/scripts/<name>.sh, or a harmless no-op if neither exists. Copy any of them over and edit — no recompile needed, and no sxbarc editing needed either to change a module's popup content (e.g. adding a row to usermenu) — see Reference scripts below for the full list, and "Any name is a module" in that section for writing your own.

Taskbar

Every module above renders one line of its own text. taskbar doesn't: it shows one clickable segment per window on the current workspace, so you can switch focus between them — the motivating case being a window manager's monocle mode, where only one window is visible at a time and there'd otherwise be no way to reach the others from the bar. Typical setup is its own row on the secondary bar, directly beneath the primary status bar.

secondary_bar : true
module        : taskbar : true : 1
bar           : taskbar : secondary
Needs wmctrl, for both listing windows and requesting focus — and a window manager that actually acts on a _NET_ACTIVE_WINDOW client message (what wmctrl -i -a sends to request focus). Checking sxwm's own source (not just its advertised _NET_SUPPORTED list, which does list it) showed that message was never handled — sxwm published _NET_ACTIVE_WINDOW read-only to reflect its own focus, but accepted no external requests to change it, and has no other IPC that could either. patches/net-active-window-mrjensk.patch in the sxwm repo adds that handling (reusing sxwm's own set_input_focus(), the same function focus_next/focus_prev use, so monocle-mode raising behaves identically to switching focus with a keybind). Other window managers may already support this out of the box — check yours.

It ignores its own align — as a fill module, it always occupies whatever's left between the left- and right-aligned modules on its bar, split into equal-width segments, rather than anchoring to one side. The currently-focused window's segment is highlighted the same way the active workspace pill is.

Any name is a module

There's no separate "custom module" directive. A module's command is resolved purely by name — write a script, name it <name>.sh, and it's a module.

module : temp : true : 5

...with ~/.config/sxbar/scripts/temp.sh being e.g.:

#!/bin/sh
sensors | grep 'Package' | awk '{print $4}'

Resolution order: your own copy at ~/.config/sxbar/scripts/<name>.sh wins if it exists, then the reference copy make install places at $PREFIX/share/sxbar/scripts/<name>.sh, else the module is a silent no-op — so a typo'd module name just shows nothing rather than erroring. A module you write yourself takes every other directive on this page too — prefix, colour, click, popup, and so on — exactly like any module sxbar ships a script for; there's no distinction between the two once module : has resolved a name to a script. If the script understands a menu subcommand, it can also declare its own popup, exactly like the reference scripts below — see the note under the table.

Reference scripts

Every script sxbar ships with lives in scripts/ and installs to $PREFIX/share/sxbar/scripts/ as a reference copy — copy any of them to ~/.config/sxbar/scripts/ and edit your own copy freely, no sxbarc changes needed for the name to keep resolving to it.

One script per module, dispatched by subcommand where a module needs more than one piece of output. Every one of them also answers a menu subcommand — see the note below the table.

scriptsubcommands
clock.shbar text (default) · menu
date.shbar text (default) · menu
battery.shcapacity (bar, default) · status · toggle-powersave · menu
volume.shget (bar/slider, default) · set VALUE · menu
brightness.shget (bar/slider, default) · set VALUE · menu
cpu.shusage [PREFIX] (bar/popup, default) · mem · cores · menu
bluetooth.shstatus (bar, default) · pair · menu
usermenu.shbar text (default) · menu
network.shstatus (bar, default) · wifi · ethernet · menu
media.shbar text (default) · track · art · prev · playpause · next · menu
menu is what makes a module self-contained: run once at startup for every module (whichever script its name resolves to), it prints that module's popup definition — the same popup/popup_item/ popup_info/popup_set directives sxbarc itself uses, minus the module-name field, since a script only ever describes itself. Edit your own copy of e.g. usermenu.sh to add, remove or reorder rows — sxbarc's own directives can still override the result wholesale if you'd rather keep everything in one config file (see Row types).

Plus five standalone helpers:

scriptused asdoes
battery_icon.shprefix_cmdpicks a battery glyph by charge level, swaps to a bolt glyph while charging
volume_icon.shprefix_cmdpicks a volume glyph, mute-aware
demo_popup.shpopup_item / popup_setfires a desktop notification — a safe target for testing popup rows before wiring up real commands
demo_menu.shmodule : demo_menu : true : 999self-declares one of every popup row type (text, image, button, slider, segmented buttons) via its own menu subcommand — a runnable reference, and a safe sandbox for testing hover/click/drag before building your own
startmenu.shmodule : startmenu : true : 3600a hover popup of favorite apps as buttons — edit its popup_item lines to add/remove favorites, no sxbarc editing needed

battery_icon.sh, trimmed:

#!/bin/sh
pct=$(printf '%s' "$1" | tr -dc '0-9')
status=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -n1)

if [ "$status" = "Charging" ]; then
    printf '%s ' ''   # bolt glyph
    exit 0
fi

if   [ "$pct" -ge 90 ]; then printf '%s ' ''
elif [ "$pct" -ge 50 ]; then printf '%s ' ''
else printf '%s ' ''
fi

The module's current output (e.g. "83%") is passed in as $1, shell-quoted — built-in commands don't expose extra state like charging/muted beyond that string, so scripts needing it re-check the system themselves.

Icons, colour, layout, clicks

These directives apply to any module, by name.

Prefix / icon

prefix : module_name : "icon text"    # icon : ... is an accepted alias

Static text prepended to a module's output. Needs a Nerd Font set via font to render glyphs. Declare prefix after the module's module line.

Dynamic icon (prefix_cmd)

prefix_cmd : module_name : "command or script path"   # icon_cmd is an alias

Runs a script instead of fixed text, so the icon can reflect live state (battery charging, volume muted). Re-run every refresh, with the module's own current output passed in as $1. Its stdout becomes the prefix verbatim — include your own trailing space.

Colour

colour : module_name : #rrggbb   # color : ... also works

Overrides foreground_colour for one module.

Width

width : module_name : min_pixels

Reserves a minimum pixel width for the module's slot so neighbours don't shift when its digit count changes (e.g. cpu going 9%16%100%). Text is never truncated — wider values just use their natural width.

Max width (scrolling / marquee text)

max_width : module_name : max_pixels

The opposite of width above: caps a module's slot at this many pixels. Text that fits draws as normal; text wider than the cap scrolls left within that fixed width instead of stretching the bar — handy for anything with unpredictable-length output, like media's "Artist - Title". sxbar only redraws faster than its usual once-a-second cadence while something is actually scrolling, so this costs nothing when nothing overflows.

max_width : media : 220

This is a bar-level directive; a popup's own text rows (e.g. media's track title) scroll the same way automatically whenever the popup also has an image, slider or button-row to anchor its width — no separate config needed there, see Row types.

Alignment

align : module_name : left|center|right   # default: right

Three independent groups per bar: left continues on from the workspace switcher, center is centered across the full bar width, right is anchored before version_text.

icon_only

icon_only : module_name : true|false

Shows only the prefix/icon, hiding the module's own text entirely. The command, refresh interval and any popup keep running exactly as normal — only the rendered bar text changes. Needs a prefix or prefix_cmd set, or there's nothing left to show.

Click & scroll

click       : module_name : "command"
scroll_up   : module_name : "command"
scroll_down : module_name : "command"
click       : volume : "pavucontrol"
scroll_up   : volume : "wpctl set-volume --limit 1.0 @DEFAULT_AUDIO_SINK@ 5%+"
scroll_down : volume : "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"

Runs detached (double-fork) so sxbar never blocks. If a module also has a popup with a click trigger, the popup takes over left-click and click is ignored for it — scroll_up/scroll_down still work independently either way.

Floating popups

A module can open a small floating (override-redirect) window instead of — or as well as — running a plain click command. Only one popup is open at a time.

popup : module_name : hover|click : buttons|slider

The third field only decides whether the module has a popup at all these days — popup_item/popup_info/popup_set decide what's actually in it, in any combination, so either word works there.

hover

Opens on mouse-over

Reveals the popup while the pointer is over the module; closes when the pointer leaves both the module and the popup. Every built-in popup uses this.

click

Opens on left-click

Toggles on click; closes again on a second click, or on clicking anywhere else — like an ordinary dropdown menu.

Hovering only reveals a menu — an actual click on a button row is still needed to run anything. A hover-triggered usermenu doesn't make "Shut down" any easier to trigger by accident.

Full example config

A config combining most of the directives on this page.

# appearance
height              : 20
bottom_bar          : false
vertical_padding    : 5
horizontal_padding  : 5
background_colour   : #000000
foreground_colour   : #7abccd
font                : JetBrainsMono Nerd Font:size=10

# modules
module : clock      : true  : 1
module : date       : true  : 60
module : battery    : true  : 30
module : volume     : true  : 5
module : cpu        : true  : 3
module : network    : true  : 10
module : usermenu   : true  : 300

# icons + colours
prefix : clock   : " "
prefix : network : " "
colour : clock   : #50fa7b
colour : battery : #ffb86c
colour : volume  : #ff79c6
colour : network : #2ee6d6

# layout
align     : clock   : center
width     : battery : 48
width     : volume  : 48
icon_only : network : true

# actions
scroll_up   : volume : "wpctl set-volume --limit 1.0 @DEFAULT_AUDIO_SINK@ 5%+"
scroll_down : volume : "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"

# override the default logout command
popup_item : usermenu : "Sleep"     : "systemctl suspend"
popup_item : usermenu : "Log out"  : "loginctl terminate-session $XDG_SESSION_ID"
popup_item : usermenu : "Shut down" : "systemctl poweroff"

# a module you wrote yourself (~/.config/sxbar/scripts/mem.sh)
module : mem : true : 10
prefix : mem : " "

Directive reference

Every config key, at a glance.

directivesyntax
modulemodule : name : true|false : interval
prefixprefix : name : "text"
prefix_cmdprefix_cmd : name : "command"
icon_onlyicon_only : name : true|false
colourcolour : name : #rrggbb
widthwidth : name : min_pixels
alignalign : name : left|center|right
barbar : name : primary|secondary
clickclick : name : "command"
scroll_up / scroll_downscroll_up : name : "command"
popuppopup : name : hover|click : buttons|slider
popup_itempopup_item : name : "Label" : "command"
popup_infopopup_info : name : "shell command"
popup_imagepopup_image : name : "shell command"
popup_image_sizepopup_image_size : name : pixels
popup_setpopup_set : name : "command"
height / bottom_bar / *_padding / border*global bar geometry — see Global & bar options
background_colour / foreground_colour / border_colour / fontglobal appearance
show_version / version_textglobal
secondary_barsecondary_bar : true|false
workspace_iconworkspace_icon : name : "icon text"