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.
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.
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 to | What |
|---|---|
| $PREFIX/bin/sxbar | the binary |
| $PREFIX/share/man/man1/sxbar.1 | man page |
| $PREFIX/share/sxbarc | fallback 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.
| key | default | does |
|---|---|---|
| height | 19 | bar height in px |
| bottom_bar | false | dock at the screen bottom instead of top |
| vertical_padding | 0 | gap between the bar and the screen edge |
| horizontal_padding | 0 | gap on both sides of the bar |
| text_padding | 0 | inner padding before the first workspace/module |
| border / border_width | false / 0 | draw 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. |
| font | monospace:size=8 | Xft 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_bar | false | adds 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_PARTIALso 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.
| module | shows | default | needs | popup |
|---|---|---|---|---|
| clock | HH:MM:SS | on | — | hover → open calendar |
| date | YYYY-MM-DD | on | — | hover → open calendar |
| battery | charge % | off | /sys/class/power_supply | hover → status + power-saver toggle |
| volume | volume % | on | wpctl | hover → slider |
| cpu | usage % | off | /proc/stat | hover → cpu / memory / per-core |
| brightness | brightness % | off | brightnessctl | hover → slider |
| bluetooth | On / Off | off | bluetoothctl | hover → power/scan/pair menu |
| usermenu | current username | on | systemctl | hover → sleep/logout/shutdown |
| network | Online / Offline | off | ip | hover → WiFi/Ethernet + IP |
| media | play/pause glyph + Artist - Title | off | playerctl, curl for remote art | hover → album art + Previous/Play-Pause/Next |
| taskbar | not like the others — see Taskbar below | |||
Full detail on each popup is in Built-in popups below.
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
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.
| script | subcommands |
|---|---|
| clock.sh | bar text (default) · menu |
| date.sh | bar text (default) · menu |
| battery.sh | capacity (bar, default) · status · toggle-powersave · menu |
| volume.sh | get (bar/slider, default) · set VALUE · menu |
| brightness.sh | get (bar/slider, default) · set VALUE · menu |
| cpu.sh | usage [PREFIX] (bar/popup, default) · mem · cores · menu |
| bluetooth.sh | status (bar, default) · pair · menu |
| usermenu.sh | bar text (default) · menu |
| network.sh | status (bar, default) · wifi · ethernet · menu |
| media.sh | bar 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:
| script | used as | does |
|---|---|---|
| battery_icon.sh | prefix_cmd | picks a battery glyph by charge level, swaps to a bolt glyph while charging |
| volume_icon.sh | prefix_cmd | picks a volume glyph, mute-aware |
| demo_popup.sh | popup_item / popup_set | fires a desktop notification — a safe target for testing popup rows before wiring up real commands |
| demo_menu.sh | module : demo_menu : true : 999 | self-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.sh | module : startmenu : true : 3600 | a 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.
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.
Opens on left-click
Toggles on click; closes again on a second click, or on clicking anywhere else — like an ordinary dropdown menu.
usermenu doesn't make "Shut down" any easier to
trigger by accident.Row types
A popup is a list of rows. Text, button, slider, image and segmented-button rows all mix freely in the same popup.
popup_info
Purely informational, not clickable at all. Label is a shell command's output, re-run fresh every time the popup opens (e.g. current IP).
popup_item
Runs its own command on click; the popup stays open. Label is fixed text, set once.
popup_live_item
Same as popup_item, but the label is a
shell command's output instead of fixed text, re-run fresh every time
the popup opens (like popup_info) — still
clickable, its own click command spawns independently of the label.
Use this for a single button that shows its own current state, e.g.
"Lid-suspend: OFF" that toggles when clicked.
popup_set
Draggable 0–100% track; dragging doesn't close the popup. Starting position comes from the module's own command output.
popup_image
Renders an image file, e.g. album art. Command's stdout is a path to a
local image, re-run fresh every popup open, scaled to fit a 160px box
by default — override per module with
popup_image_size. Not clickable. No extra
build dependency needed. Anchors the popup's width — any text row in
the same popup is capped to it and scrolls instead of stretching the
popup wider than the image.
popup_buttons
One row split into N equal-width button segments side by side (e.g.
media transport controls), instead of N stacked full-width
popup_item rows. Each segment runs its own
command on click; the popup stays open.
popup_item : module_name : "Label" : "command" # button row, repeatable popup_live_item : module_name : "label command" : "command" # button row, label re-run on every open popup_info : module_name : "shell command" # text row, repeatable popup_image : module_name : "shell command" # image row, stdout = path to an image file popup_image_size : module_name : pixels # image row box size, default 160 popup_buttons : module_name : "L1" : "cmd1" : "L2" : "cmd2" ... # one row, N button segments popup_set : module_name : "command" # slider row, receives new value as $1 (e.g. "45%")
The first popup_item/popup_live_item/
popup_info/
popup_image/popup_buttons line for
a module replaces its built-in default rows (if any, including a
popup_set slider row) — later lines append. Add
popup_set again afterwards if you cleared a built-in
slider this way and still want it back.
A module script's own menu subcommand can emit
popup_live_item the same way (no module-name field,
same as its other popup directives):
popup_live_item : "~/.config/sxbar/scripts/lidsuspend.sh status" : "~/.config/sxbar/scripts/lidsuspend.sh toggle"
The label command's output becomes the button text on every popup open
(e.g. "Lid-suspend: OFF"), and clicking it runs the
second command regardless of what the label currently says.
Example combining all five on one module (mymodule
resolving to your own ~/.config/sxbar/scripts/mymodule.sh):
module : mymodule : true : 5 popup : mymodule : hover : buttons popup_info : mymodule : "echo 'status: '$(whoami)" popup_image : mymodule : "echo /path/to/icon.png" popup_buttons : mymodule : "⏮" : "mymodule-ctl prev" : "⏭" : "mymodule-ctl next" popup_item : mymodule : "Restart" : "systemctl --user restart myservice" popup_set : mymodule : "myservice-set-level"
Or skip the sxbarc overrides entirely and declare the same popup from
mymodule.sh itself via its menu
subcommand — see Any name is a module above and
scripts/demo_menu.sh for a runnable example of exactly this.
Built-in popups
What each built-in module's popup contains by default. This
content isn't hardcoded in the binary — it's each module's own
<script> menu output (see
Any name is a module), shown below as the
equivalent sxbarc directives for reference. Edit the script directly to
change these, or override wholesale from sxbarc with the directives
above — either works, since the first popup_item/
popup_info line for a module always replaces
its current rows regardless of where they came from.
clock · date — open calendar
popup : clock : hover : buttons popup_item : clock : "Open calendar" : "gsimplecal" popup : date : hover : buttons popup_item : date : "Open calendar" : "gsimplecal"
Same action on both, since clock and date are usually shown next to each other.
battery — status + power-saver toggle
popup : battery : hover : buttons popup_info : battery : "~/.config/sxbar/scripts/battery.sh status" popup_item : battery : "Toggle power saver" : "~/.config/sxbar/scripts/battery.sh toggle-powersave"
The status row (state, time remaining, health) needs upower;
the toggle needs power-profiles-daemon — both beyond the
plain /sys/class/power_supply read the bar text itself uses.
brightness · volume — slider
popup : brightness : hover : slider popup_set : brightness : "~/.config/sxbar/scripts/brightness.sh set" popup : volume : hover : slider popup_set : volume : "~/.config/sxbar/scripts/volume.sh set"
cpu — three status rows
Fresh CPU sample, memory line, and a per-core breakdown (two
/proc/stat samples 0.2s apart, paired up by
position via an awk array — no process substitution, since
/bin/sh may be dash).
One script, cpu.sh, handles all three via subcommand.
popup : cpu : hover : buttons popup_info : cpu : "~/.config/sxbar/scripts/cpu.sh usage 'CPU: '" popup_info : cpu : "~/.config/sxbar/scripts/cpu.sh mem" popup_info : cpu : "~/.config/sxbar/scripts/cpu.sh cores"
bluetooth — power / scan / pair
popup : bluetooth : hover : buttons popup_item : bluetooth : "Turn on" : "bluetoothctl power on" popup_item : bluetooth : "Turn off" : "bluetoothctl power off" popup_item : bluetooth : "Search for devices" : "bluetoothctl --timeout 10 scan on" popup_item : bluetooth : "Pair last found device" : "~/.config/sxbar/scripts/bluetooth.sh pair"
The first three are plain passthrough commands (no logic to extract into a
script); "pair" has real parsing/control flow, so it's the one that's scripted.
No interactive device list — it targets whichever device
bluetoothctl devices saw most recently.
usermenu — sleep / log out / shut down
popup : usermenu : hover : buttons popup_item : usermenu : "Sleep" : "systemctl suspend" popup_item : usermenu : "Log out" : "pkill sxwm" popup_item : usermenu : "Shut down" : "systemctl poweroff"
pkill <your-wm>
— minimal window managers without a session manager have no external way to
trigger their own quit keybind. Simplest fix: edit the pkill sxwm
line directly in your own copy of usermenu.sh for your
WM/session — or override the "Log out" row's command
from sxbarc as shown above if you'd rather keep it in your config file.network — WiFi / Ethernet status
Bar text is Online/Offline
based on whether a default route exists. Popup has two informational rows: first
interface with a /sys/class/net/<if>/wireless
directory for WiFi, first non-virtual ARPHRD_ETHER
interface for Ethernet — each showing its IPv4 address or "disconnected".
popup : network : hover : buttons popup_info : network : "~/.config/sxbar/scripts/network.sh wifi" popup_info : network : "~/.config/sxbar/scripts/network.sh ethernet"
media — album art + transport controls
Bar text is a play/pause glyph plus Artist - Title,
via playerctl (controls whichever player it considers
active — most players speak MPRIS2 automatically: Spotify, VLC, mpv, browser
tabs, etc.). The popup's image row resolves mpris:artUrl:
a file:// path is used directly; an
http(s):// one is downloaded once and cached under
$XDG_CACHE_HOME/sxbar-media-art/ (needs
curl). The transport buttons are one
popup_buttons row (step-backward/play/step-forward
Nerd Font glyphs, U+F048/U+F04B/U+F051 — shown as
<...> placeholders below since they render as
tofu without that font) rather than three stacked rows.
popup : media : hover : buttons popup_image : media : "~/.config/sxbar/scripts/media.sh art" popup_info : media : "~/.config/sxbar/scripts/media.sh track" popup_buttons : media : "<step-backward>" : "~/.config/sxbar/scripts/media.sh prev" : "<play>" : "~/.config/sxbar/scripts/media.sh playpause" : "<step-forward>" : "~/.config/sxbar/scripts/media.sh next"
Bigger album art? Add
popup_image_size : media : 220 alongside the
lines above — any pixel size, default is 160.
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.
| directive | syntax |
|---|---|
| module | module : name : true|false : interval |
| prefix | prefix : name : "text" |
| prefix_cmd | prefix_cmd : name : "command" |
| icon_only | icon_only : name : true|false |
| colour | colour : name : #rrggbb |
| width | width : name : min_pixels |
| align | align : name : left|center|right |
| bar | bar : name : primary|secondary |
| click | click : name : "command" |
| scroll_up / scroll_down | scroll_up : name : "command" |
| popup | popup : name : hover|click : buttons|slider |
| popup_item | popup_item : name : "Label" : "command" |
| popup_info | popup_info : name : "shell command" |
| popup_image | popup_image : name : "shell command" |
| popup_image_size | popup_image_size : name : pixels |
| popup_set | popup_set : name : "command" |
| height / bottom_bar / *_padding / border* | global bar geometry — see Global & bar options |
| background_colour / foreground_colour / border_colour / font | global appearance |
| show_version / version_text | global |
| secondary_bar | secondary_bar : true|false |
| workspace_icon | workspace_icon : name : "icon text" |