Nimbin[12]?Sway / SwBr / README.md

SwBr git · main

SwBr - status bar for sway

sway wayland c bar · first commit 2026-08-27 · last commit 2026-10-05 (4 days ago) · synced 3 days ago · upstream: github.com/nimbin2/SwBr

C 95.9% Markdown 3.7%
git clone https://git.christianimmanuel.de/sway/SwBr.gitwget https://git.christianimmanuel.de/sway/SwBr/archive/SwBr.tar.gz
README.md 18 KB · 405 lines raw

swbr

A floating bar for Sway, on the layer shell. It sits on top of the tiled windows at the screen edge instead of taking space from them. Hover it and press space and it folds into a thin strip that still tells you what is going on.

swbr

swbr, folded

100% vibecode, but tested.

Build

sudo apt install build-essential libwayland-dev
make
sudo make install     # /usr/bin, examples in /usr/share/swbr
make config           # optional: config.example -> ~/.config/swbr/config
make link             # or: config -> the installed example, kept in sync

libwayland-client is the only library it links. The layer-shell protocol glue and the font rasteriser are vendored, so there is no wayland-scanner step. Needs a running sway session ($SWAYSOCK) and any TTF font.

Use

exec_always swbr --replace

--replace terminates a bar that is already running and waits for it to go. Use it instead of a separate pkill swbr line: sway forks each exec_always without waiting, so a kill on one line races the bar started on the next and usually wins.

left click a workspaceswitch to it
the pointerturns into a hand over anything that does something
hover the bar, press spacefold it into the strip, and back
click a cellrun its button1= command
pkill -USR1 swbrfold or unfold every bar

The right hand side is made of cells: one command each, run on an interval or kept running and read line by line.

cell=clock
clock.cmd=date '+%H:%M'
clock.interval=20

cell=vol
vol.cmd=pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]*%' | head -1
vol.interval=2
vol.button1=pavucontrol

bar={workspaces||vol,clock}

bar= places everything: || splits left / center / right, , separates cells, (a,b) glues two together. Keep it last in the config.

Sources

Some cells need no script. source= fills them in directly; setting cmd= on the same cell switches it back to your own command.

cell=cmus                 cell=battery              cell=mpv
cmus.source=cmus          battery.source=battery    mpv.source=window
                                                    mpv.program=mpv

cmus runs cmus-remote -Q and reads the status line, so playing and paused are facts rather than a guess at what a script printed. Prints ▶ Artist — Title or ⏸ …, nothing when cmus is stopped. Left click toggles pause, middle skips forward, right goes back. cmus_fmt: %i icon, %n artist — title, %a, %t, %s; default %i %n. Two cells work — one %i, one %n — if you want the icon separate from the title.

battery reads /sys/class/power_supply: 83+ filling, 83- draining, no sign when full. bat_path= picks one if yours is not BAT0 or you have two. src_fmt: %c capacity, %i sign, %w watts now, %h hours left, %s the word — %c%i %w %h gives 83- 21.5W 1.7h.

window asks sway which workspaces have a window of program= and prints mpv [3|5]. Matched against the app id, the X11 class, then the title. src_fmt: %n the cell's name, %w the workspaces, %c the count.

Folded

space over the bar folds it to a few pixels.

NAME.slim=on the strip
ticka block, on while the cell has output
bara gauge, slim_min–slim_max
clocktwelve bars lit up to the hour, softer before noon
presencea block, there only while the program is
mediaa full block playing, a short dim one paused, nothing stopped
autopick from what the cell prints
offnothing

A cell keeps its own colour when folded, markup included. slim_color overrides it, slim_color2 is the clock's minutes. For slim=media on a plain command cell, slim_on= says which output counts as playing; a source= cell already knows.

slim_align=1 (the default) gives each mark its cell's whole place, inset by the same padding the text had — so folding changes the height of the bar and nothing else. Every folded frame works the open layout out first with nothing drawn, so the marks are right from the first frame, --slim included. slim_align=0 packs them against the right edge instead.

ws_slots=10 (the default) puts the numbers 1–10 in the bar whether or not they exist. A free number is the same button in the same place, ws_empty_alpha faint, and clicking it opens that workspace. 0 shows only open ones.

Folded and lined up, the workspace marks are the open bar's own buttons — the same set, the same left edges, the same widths — so you click the same spot in either mode. Brightness says used from unused. slim_ws_w forces a width; slim_ws_slots only applies to the packed strip (slim_align=0).

slim_bar_segs=10 splits a folded gauge into ten blocks, so four lit blocks reads as forty percent. slim_bar_gap is the space between them, slim_bar_pad the space either side of the whole gauge, and 0 segments gives one solid bar. A cell too narrow to show the dividers halves the count; give it a slim_w instead — on a gauge that wins over the alignment and grows about the cell's middle, which is what a two-digit battery needs.

signals=0 leaves a plain strip.

A floating bar (min_width > 0) sizes itself to its content. It grows at once, but ignores getting narrower by less than shrink_px=24 and waits shrink_s=30 before a bigger narrowing — it is centred, so every width change moves everything on it.

Redrawing

redraw_ms=250 holds cell updates and merges whatever lands in the same window into one frame — every frame is a round trip through the compositor, and ten cells on their own timers would otherwise wake the bar three times a second.

Anything coming from sway is exempt. A workspace switch is drawn on the next frame, so switching back and forth stays visible instead of the two changes merging into one late frame.

Hover

NAME.hover= is what a cell shows while the pointer is on it, with the same placeholders its source understands, or %s for a command's output:

battery.hover=%w  %h %e        # 21.5W  1.7h left, or 0.3h to full on the cable
battery.hover_click=1          # opens on a click, not on the pointer
battery.hover_slim=0

%e says which way the clock is running — half an hour means nothing on its own when it could be to full or to empty. With nothing to report, a full battery or a kernel that gives no rate, the panel falls back to the status word rather than showing blanks.

hover_click=1 — set on the battery and the calendar in the shipped config — waits for a left click and stays open until that cell is clicked again, something else is, or the pointer leaves the bar — so you can read it without holding still. Pointing at such a cell does nothing.

The cell is measured with that text It slides down out of the bar and fades in over anim_ms, centred on the cell and padded like one: square where the two meet, rounded on the far side. Near an end of the bar it snaps flush with it and both square that corner, so the outer edge runs straight from the bar into the panel. The surface grows to make room and shrinks again when you leave. The padding either side of a cell counts as part of it, and the panel itself takes the pointer, so resting on it keeps it open.

NAME.hover_cal=1 puts this month in the panel, with today in the hl colour. It is built in rather than shelled out to cal(1), so the columns line up whatever the font and resting on the clock spawns nothing. Weeks start on Monday, and ‹ › at either end of the title row page the month; it opens on the current one every time.

NAME.hover_cmd= is the general form: any command's output, run once when the hover starts rather than on a timer. It is wrapped in timeout 2, since it runs on the frame the pointer arrives and a command that blocks would block the whole bar. The panel is as tall as the output has lines either way.

clock.hover_cal=1
clock.hover_cmd=cal      # the same idea, if you prefer cal's own layout

Folded there is no room for a panel, so there is no hover unless hover_slim=1, which opens the bar while you rest on that cell. Off by default.

Going flat

battery.alert=30,20,10,5,1
battery.alert_msg=BATTERY %c%%
battery.alert_tone=250
battery.alert_cmd=[ %c -le 1 ] && poweroff

Each level fires once on the way down and rearms when the charge comes back above it, or when the cable goes in — so 19% does not nag every ten seconds. The message goes through the bar's own message system, as a warning except for the last level, which is an error.

alert_cmd runs at every level and only at a level, so a guard like [ %c -le 1 ] needs 1 in the list — otherwise it is evaluated at 5% and never again. poweroff also needs root.

swbr --alert-test [CELL] fires an alert now: it prints the levels, plays the tone through your own player and reports its exit status, and shows what alert_cmd would run at each level without running it. A player that fails during a real alert now says so on the bar instead of failing silently.

alert_gain (35%) is how loud the tone itself is, which costs nothing and moves no mixer. When that is not enough, alert_vol_min raises the mixer for the tone and puts it back:

battery.alert_vol_min=50
battery.alert_vol_get=amixer -M get Master | grep -o '[0-9]*%' | head -1
battery.alert_vol_set=amixer -M -q set Master %v%

Any pair of commands works — the first prints a number, the second takes %v. The level is only raised if it is already below the floor, and the restore is the shell's own EXIT trap, so it happens even if the alert is killed part way through. ALSA has no per-stream volume, so this is the master control: other audio is louder for the second or two the tone lasts. A muted output stays muted; append unmute to the set command if you would rather it spoke anyway.

The tone is a sine with a raised-cosine edge either side, written as raw PCM straight into alert_play (aplay -q -f cd - by default, or paplay --raw --rate=44100 --format=s16le --channels=2 on pipewire). Soft edges mean it starts and stops without the click a test tone makes. alert_tone=0 for silence, alert_ms and alert_beeps for length and count. The last two levels press harder — the one before last gets an extra beep and a fifth more length, the last three extra beeps and half as long again, so 2.1 s of sound at 30% becomes 8.4 s at 5% and you can tell them apart from the next room.

alert_cmd= runs at every level with %c as the percentage, which is where a last-ditch [ %c -le 1 ] && systemctl poweroff goes.

Load per workspace

A column of dots up the right edge of each workspace button, and up its slot in the folded strip. The scale is in cores: 1.0 is one core kept busy, ws_cpu_full=4 fills the column.

0.000 cores  ......   a terminal at a prompt
0.02         +.....   cmus playing
0.4          #.....   mpv decoding
1.9          ###...   a browser working
4.0          ######   make -j4

Anything alive but below the scale gets one faint dot, so a workspace that is doing something never looks asleep; ws_cpu_idle is where that starts.

swbr asks sway which window belongs to which workspace, reads /proc, and credits each process to the nearest ancestor that owns a window — a build in a terminal counts towards that terminal's workspace. Each process is measured by its own cpu time plus the time of the children it has reaped, which is what makes a build show up at all: every compiler process is gone before the next sample, and its time only survives in its parent's totals.

ws_cpu=1  ws_cpu_interval=3  ws_cpu_idle=0.01  ws_cpu_min=0.25  ws_cpu_full=4

The numbers go to $XDG_RUNTIME_DIR/swbr-cpu so swov can draw the same thing without measuring anything itself. The last thirty-two windows to be focused go to $XDG_RUNTIME_DIR/swbr-focus in the same spirit: swov opens and closes in a moment and can never watch focus itself, so its tab key reads the order from here.

Other monitors

Workspaces are ordered by number, not in the order sway lists them — that is grouped by output, so with a second monitor the numbers come out interleaved and which way round depends on which cable went in first. ws_sort=0 keeps sway's order.

ws_other=1 adds the workspaces from your other screens as short pills on the bar's bottom edge, in smaller, dimmer type. Folded they take a slot like any other, but never this screen's emphasis.

Messages

Other programs can shout at the bar, which shows the text in place of a cell, pulses the folded strip in the message colour, and drops the words into the panel for msg_panel seconds (3 by default) so they can be read when the bar is folded:

swbr --msg 'warn: battery at 9%'
swbr --msg clear
echo 'info: build done' > $XDG_RUNTIME_DIR/swbr.fifo

msg_target= says which cell is taken over, msg_timeout= for how long, msg_flash=0 stops the pulse and msg_panel=0 the panel.

Config

${XDG_CONFIG_HOME:-~/.config}/swbr/config, key=value, # comments. Every key is also a command line option:

swbr position=bottom height=32
swbr --dump-config > ~/.config/swbr/config
swbr --probe        # outputs, sizes, font metrics, and what every cell decided

Two are shipped: config.example is short, one of each thing, and config.advanced.example is the full one — every cell, the alerts, the panels, and a poweroff at one percent. Either goes to ~/.config/swbr/config.

config.advanced.example lists everything with defaults. The ones worth knowing:

key
position, layer, heightwhere the bar sits and how tall it is
exclusive1 = reserve the space, 0 = float above the windows
min_width, align_x, side_margina floating bar sizes itself to its content
radiuscorner radius; the corners at the screen edge stay square
outputs, ws_otherwhich monitors get a bar, and whose workspaces show
ws_names, ws_inset, ws_radius, ws_bordernumbers or names, pills or blocks, outlined or not
ws_slots, ws_empty_alphashow every number, open or not, and how faint
slim_ws_w, slim_bar_segs, slim_bar_gapfolded slot width, gauge blocks
font, font_alt, ui_scale, text_pxtext
markupthe pango subset: <span foreground=..>, <b>, &amp;
hide_key, collapsed_px, anim_msfolding
signals, slim_align, slim_ws_slots, slim_bar_segswhat the folded strip shows
status_commandi3bar-style status, used when no cells are configured

Colours and fonts for swbr, swov and swas can be set once in ${XDG_CONFIG_HOME:-~/.config}/sw/config under role names (surface, accent, hl, …) that each program maps onto its own keys; sw_theme.h has the table. Keys before any section go to all three, a [swbr] section to swbr only. This config is read afterwards and wins, the command line wins over that.

make install also drops the shipped examples in /usr/share/swbr/. make link points your config at one of them, so the next install is the config you are running — handy if you want to follow the examples rather than keep your own copy. Edits you make there are overwritten on install.

Notes