Nimbin[12]?Sway / SwBr / commits / 6f10454

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

Add fancy process load display in slim And stuff

Christian Immanuel · 2026-08-27 22:43 · 6f104541dc06e018c145edb350d429b03a1d9b94

 README.md           | 397 ++++++++++--------------
 sw_theme.h          | 194 ++++++++++++
 swbr.c              | 850 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 swbr_config.example |  66 +++-
 4 files changed, 1236 insertions(+), 271 deletions(-)

diff --git a/README.md b/README.md
index 7c67e52..179962c 100644
--- a/README.md
+++ b/README.md
@@ -1,289 +1,220 @@
-# SwBr
+# swbr
 
-**100% Vibecode but tested.**
+A floating bar for [Sway](https://swaywm.org), 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.
 
-A floating status bar for Sway. Replaces `swaybar`. Binary: `swbr`.
+![swbr](screenshot.png)
 
-It sits on the layer shell above the tiled windows, flush with the top edge.
-Hover it, press space, and it rolls up into a 5px signal strip.
-
-![SwBr](screenshot.png)
-
-Folded: workspace slots, twelve clock dots, gauges — still readable, still
-clickable.
-
-![SwBr folded](screenshot-slim.png)
-
-Version: **0.8.1**
-
-## Why not swaybar
-
-- Floats above the windows instead of taking their space (`exclusive=0`).
-- Folds away on a key, without re-tiling anything.
-- Bar squared off against the screen edge, rounded on the free side. The
-  focused workspace is a full-height block in the appwheel orange.
-- Same look and same `key=value` config as `swov` and `appwheel`.
+100% vibecode, but tested.
 
 ## Build
 
 ```sh
+sudo apt install build-essential libwayland-dev
 make
+make install          # ~/.local/bin, or PREFIX=/usr/local
+make config           # optional: swbr_config.example -> ~/.config/swbr/config
 ```
 
-or by hand:
+`libwayland-client` is the only library it links. The wlr-layer-shell protocol
+glue is vendored, so there is no `wayland-scanner` step. Fonts come from
+`stb_truetype.h`, also vendored.
 
-```sh
-cc -std=c11 -O2 -Wall -Wextra -o swbr swbr.c -lwayland-client -lm
-```
+Needs a running sway session (`$SWAYSOCK`) and any TTF font. `fc-match` is used
+to pick one if it is there.
 
-`stb_truetype.h` sits next to the source, same as for `appwheel`.
-The layer-shell protocol glue is vendored inside `swbr.c`, so there is no
-`wayland-scanner` step.
+## Use
 
-Needs: `libwayland-client`, a running Sway session (`$SWAYSOCK`).
-Optional: `fc-match` to pick up the desktop font.
+```
+exec_always swbr --replace
+```
 
-## Install
+`--replace` terminates a bar that is already running and waits for it to go,
+which is what you want on a sway reload. Do not use `pkill swbr` on a separate
+line for this: sway forks each `exec_always` without waiting for it, so the
+kill and the new bar race each other and the kill usually wins — which looks
+exactly like the bar refusing to start.
 
-```sh
-make install          # -> ~/bin/swbr, override with PREFIX= or BINDIR=
-make config           # -> ~/.config/swbr/config, never overwrites
-```
+| | |
+| --- | --- |
+| left click a workspace | switch to it |
+| hover the bar, press `space` | fold it into the signal strip, and back |
+| click a cell | run its `button1=` command |
+| `pkill -USR1 swbr` | fold or unfold every bar |
 
-**Delete the `bar { ... }` block from your sway config.** If you leave it,
-swaybar keeps running underneath and you see its text peeking out below SwBr.
-Then add:
+The right hand side is made of **cells**: one command each, run on an interval
+or kept running and read line by line.
 
 ```
-exec_always pkill -x swobar; exec_always ~/bin/swbr
-```
+cell=clock
+clock.cmd=date '+%H:%M'
+clock.interval=20
 
-Your old `status_command` script keeps working — point the config at it:
+cell=vol
+vol.cmd=pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]*%' | head -1
+vol.interval=2
+vol.button1=pavucontrol
 
-```
-status_command=~/bin/sway_bar_status.sh
+bar={workspaces||vol,clock}
 ```
 
-## Cells
+`bar=` is one line that places everything: `||` splits left / center / right,
+`,` separates cells, `(a,b)` glues two together. Keep it last in the config.
 
-The right hand side is a row of cells. One command per cell, its own
-interval, its own colour. No status script in between.
+## Folded
 
-```
-cell=cpu                    # order of the cell= lines = order on the bar
-cpu.cmd=cpu_in_percent
-cpu.fmt=%s%
-cpu.interval=2
-cpu.warn=>70:cecb00         # recolour at or above 70
-cpu.crit=>80:cc0403
-cpu.min_w=52                # never jitter when the number gets shorter
-cpu.sep=0                   # no separator: glue the next cell to this one
-cpu.button1=foot -e htop    # click it
-```
+`space` over the bar folds it to a few pixels. The strip keeps saying
+something.
+
+![swbr, folded](screenshot-slim.png)
 
-| Field | Does |
+| `NAME.slim=` | on the strip |
 | --- | --- |
-| `cmd` | shell command, its output is the text |
-| `interval` | seconds; `0` = once, `-1` = stream every line |
-| `fmt` | `%s` is the output; no printf escaping |
-| `color` `bg` | fixed colours |
-| `warn` `crit` | `>70:cecb00` / `<12:ff2222` on the first number |
-| `min_w` `max_w` `align` | width and placement of the text |
-| `sep` | `0` combines this cell with the next |
-| `hide_empty` | prints nothing = cell and separator disappear |
-| `empty` | text drawn instead of hiding, so the area stays clickable |
-| `gap` | space after the cell; `0` glues it to the next one |
-| `pad` | padding inside the cell |
-| `slim` | folded strip: `auto`, `tick`, `bar`, `clock`, `off` |
-| `slim_min` `slim_max` | value range a gauge maps |
-| `slim_w` | width in the folded strip |
-| `markup` | `<span foreground="#rrggbb">` inside the output |
-| `button1..9` | per-cell click commands |
-| `pos` | `left`, `center` or `right` group |
-| `scroll` | fixed slot, `..` when too long, scrolls on hover |
-
-`swbr_config.example` ships a full set that reproduces a typical swaybar
-script: cmus, volume, docker, mpv, clock, cpu, temperature, battery.
-
-With no cells configured, `status_command` still works the old way.
+| `tick` | a block, on while the cell has output |
+| `bar` | a gauge, `slim_min`–`slim_max` |
+| `clock` | twelve bars, lit up to the hour — two at 02:00, still two at 02:55 |
+| `presence` | a block in the `running` colour, there only while the program is |
+| `media` | two full-height bars while playing, the same two short and dim when paused, nothing when stopped |
+| `auto` | pick from what the cell prints |
+| `off` | nothing |
+
+A cell keeps its own colour when folded, markup included, so a green "playing"
+glyph stays green down there. `slim_color` overrides it, `slim_color2` is the
+second one a clock uses for its minutes. `slim_on=` says which output counts as
+playing for `slim=media` — it has to match what your command prints. Without
+it swbr guesses from the usual glyphs and the words "playing" and "paused",
+which only helps if your player happens to print one of them.
+
+For cmus there is no guessing:
 
-## Messages
+```
+cell=music
+music.source=cmus
+```
 
-Anything on the system can shout at the bar through a fifo
-(`$XDG_RUNTIME_DIR/swbr.fifo`, override with `msg_fifo=`):
+Two cells, if you want the icon separate from the title:
 
-```sh
-swbr --msg "warn: backup running"
-swbr --msg "error: disk full"
-swbr --msg clear
-echo "info: done" > $XDG_RUNTIME_DIR/swbr.fifo
+```
+cell=cmusui
+cmusui.source=cmus
+cmusui.cmus_fmt=%i          # just ▶ or ⏸
+
+cell=cmus
+cmus.source=cmus
+cmus.cmus_fmt=%n            # just Artist — Title
+cmus.slim=off
 ```
 
-`info:` / `warn:` / `error:` pick the colour. `msg_timeout=8` seconds, `0`
-keeps it until cleared, and clicking it dismisses it.
-
-`msg_target=cmus` hands a cell over to the message while one is up — the
-music title becomes the warning, then goes back to being the title. With no
-target the message is drawn centred on its own. Folded, `msg_flash=1` paints
-the whole strip in the message colour, so an error is visible at 4px.
+`cmus_fmt` understands `%i` (the icon, which follows the real state), `%n`
+(artist — title, or just the title), `%a`, `%t` and `%s` (the word playing or
+paused). The default is `%i %n`.
 
-## Layout
+That is the whole config, and `source=cmus` sets the command itself — a
+`music.cmd=` from an older config would leave swbr parsing a script's output
+instead of cmus's own, so setting one switches the cell back to a plain
+command. swbr runs `cmus-remote -Q` and reads the `status` line, so playing and paused are facts, not a guess at what a script
+printed. The bar shows `▶ Artist — Title` or `⏸ Artist — Title`, the icon
+following the actual state, and the cell disappears when cmus is stopped or
+not running. Left click toggles pause, middle skips forward, right goes back;
+`music.cmd=`, `music.interval=` and the binds override any of it.
 
-One line places everything:
+`swbr --probe` prints what it decided for every cell, and the text it decided
+it from:
 
 ```
-bar={workspaces,mode||cmus,volume,(docker,mpv),clock,(cpu,temp),(power,battery)}
+cell music      iv=2  min_w=0  sep=1  '♪ Boards of Canada — Roygbiv'
+            slim=media  playing=yes  slim_on unset, guessing from the text
 ```
 
-- `||` splits groups — one part is left, two are left and right, three are
-  left, centre and right.
-- `,` separates items, `(a,b)` glues them with no separator between.
-- Names are your cell names plus the built-ins `workspaces` and `mode`.
+`signals=0` turns the whole thing off and leaves a plain strip.
 
-It assigns group, order and separators, so keep it last in the config. The
-left group reserves its width first, then the right one is placed against it,
-so nothing ever overlaps.
+## Load per workspace
 
-`min_width=800` makes the bar a floating pill instead of a full-width strip,
-and it **sizes itself to its content**: never narrower than `min_width`,
-never wider than the screen. A cell appearing — charging, a docker list, a
-longer clock — grows the bar instead of being cut off. `min_width=0` goes
-back to spanning the whole output. `align_x=center|left|right` places it.
+A moon in the corner of each workspace button says how much processor time the
+windows on that workspace are using: a sliver when it is idling, whole when something
+is working. It is drawn held back towards the bar's own `dim`, since it is
+secondary to the name. Folded, the workspace slot fills from the bottom
+instead, because a moon that size would be invisible.
 
-## Buttons that stay put
+swbr asks sway which window belongs to which workspace, reads `/proc`, and
+credits every process to the nearest ancestor that owns a window — so a build
+running in a terminal counts towards the workspace that terminal is on. Each
+process is measured by its own cpu time *plus* the time of the children it has
+already reaped, which is what makes a build show up at all: every compiler
+process lives for a moment and is gone long before the next sample, and its
+time only survives in its parent's totals.
+Anything with no window above it, a daemon or the session itself, belongs to
+no workspace and is left out. The colour runs from `running` through
+`slim_warm` into `urgent`.
 
-A cell that prints nothing disappears — fine for docker, wrong for a media
-control you want to click. Two fields fix it:
-
-- `empty=♪` draws a placeholder instead of hiding, so the area and its click
-  bindings survive.
-- `gap=0` glues a cell to the next one; with a `bg` on both, they render as
-  one continuous box.
+```
+ws_cpu=1  ws_cpu_interval=3  ws_cpu_min=4  ws_cpu_full=60
+```
 
-The shipped config uses this for cmus: the title is a scrolling text cell,
-the transport controls are their own cell right next to it. The controls can
-never be truncated away by a long title, and when cmus is not running the
-title vanishes while the button stays and starts it.
+The numbers are written to `$XDG_RUNTIME_DIR/swbr-cpu` so swov can show the
+same thing on its tiles without measuring anything itself — a rate needs two
+samples seconds apart, and swov is only on screen for a moment. `swbr --probe`
+prints what it last measured, and how many windows it found in sway's tree to
+attribute work to — if that count is zero, nothing can ever show.
 
-## Text cells
+## Messages
 
-`NAME.scroll=1` with a `min_w` gives a cell a fixed slot. Longer text is cut
-with `..`, and scrolls through while the pointer sits on it. When the bar
-runs out of room, these are the cells that give up width — nothing else
-moves.
+Other programs can shout at the bar, which shows the text in place of a cell:
 
-## Use
+```sh
+swbr --msg 'warn: battery at 9%'
+swbr --msg clear
+echo 'info: build done' > $XDG_RUNTIME_DIR/swbr.fifo
+```
 
-| Action | Result |
-| --- | --- |
-| Left click a workspace | switch to it |
-| Hover + `space` | fold / unfold the bar |
-| `pkill -USR1 swbr` | fold / unfold without the mouse |
-| Click a message | dismiss it |
-| Mouse buttons | run `button1`..`button9` commands |
-
-## The folded strip
-
-Four pixels tall and still readable:
-
-- **Workspaces** keep a fixed slot each, so slot 3 is always workspace 3 —
-  missing ones stay as faint placeholders and nothing shifts around.
-- **A clock cell** (`slim=clock`) becomes twelve dots: hours up to now are
-  lit, the current hour grows with the minutes.
-- **Anything with a percentage, or with warn/crit thresholds**, becomes a
-  gauge: a visible track filled to its value, in that cell's current colour.
-  Battery red still reads as red. `slim_min`/`slim_max` map any range, so a
-  temperature cell can be a gauge over 40..100 C.
-- **A message** takes the whole strip in its colour.
-
-Per cell: `slim=auto|tick|bar|clock|off`, plus `slim_min`/`slim_max`.
-Height is `collapsed_px` (5 by default).
-
-The strip stays **clickable**: workspace slots still switch workspaces, and a
-cell's button bindings still fire. Give a media cell `slim=tick` (not `off`)
-and `slim_w=26`, and it stays on the strip as a play/pause button you can
-find and hit — the screen edge makes a 5px target easy.
-
-For something you must be able to reach without aiming, use the bar-wide
-`button2=` / `button6=` / `button7=` bindings: they fire anywhere no cell
-sits, folded or not. Start folded with `swbr --slim`.
-
-Elements sit in the same order as the expanded bar, and cells glued in the
-layout stay glued here too. `swbr --probe` prints the whole strip left to
-right with each element's mode, colour and fill, which is the fastest way to
-work out which bar is which.
-
-## Screen modes
-
-Sizes are logical pixels, multiplied by the output scale — the bar looks the
-same on a HiDPI screen. `height=0` derives the height from the font, and
-`ui_scale` scales all text at once. One bar per output, each with its own
-workspaces.
+`msg_target=` says which cell is taken over, `msg_timeout=` for how long.
 
 ## Config
 
-`~/.config/swbr/config`, `key=value`, one per line.
-Every key also works on the command line: `swbr ui_scale=1.2 exclusive=1`.
+`${XDG_CONFIG_HOME:-~/.config}/swbr/config`, `key=value`, `#` comments. Every
+key is also a command line option:
 
 ```sh
-swbr --help          # all keys
-swbr --dump-config   # a config file with every default
-swbr --probe         # outputs, sizes, font metrics, live cell values
-swbr --msg TEXT      # send a message to the running bar
-swbr --slim          # start folded
-swbr --version       # SwBr 0.8.1 (build eaebdc09)  <- md5 of swbr.c
+swbr position=bottom height=32
+swbr --dump-config > ~/.config/swbr/config
+swbr --probe        # outputs, sizes, font metrics and what each cell prints
 ```
 
-See `swbr_config.example` for the commented version.
+`swbr_config.example` lists everything with defaults. The ones worth knowing:
+
+| key | |
+| --- | --- |
+| `position`, `layer`, `height` | where the bar sits and how tall it is |
+| `exclusive` | `1` = reserve the space, `0` = float above the windows |
+| `min_width`, `align_x`, `side_margin` | a floating bar sizes itself to its content |
+| `radius` | corner radius; the corners at the screen edge stay square |
+| `outputs` | which monitors get a bar |
+| `ws_names`, `ws_inset`, `ws_radius` | workspace buttons: numbers or names, pills or blocks |
+| `font`, `font_alt`, `ui_scale`, `text_px` | text |
+| `markup` | the pango subset: `<span foreground=..>`, `<b>`, `&amp;` |
+| `hide_key`, `collapsed_px`, `anim_ms` | folding |
+| `signals`, `slim_ws_slots`, `slim_warm` | what the folded strip shows |
+| `status_command` | i3bar-style status, used when no cells are configured |
+
+### Shared config
+
+Colours and fonts for swbr, swov and appwheel can be set once in
+`${XDG_CONFIG_HOME:-~/.config}/sw/config`, using role names (`surface`,
+`accent`, `hl`, ...) that each program maps onto its own keys. `sw_theme.h`
+lists them all.
+
+Keys written before any section go to all three programs; a `[swbr]` section
+goes to swbr only and takes its own key names as well. The config above is read
+afterwards, so it always wins, and the command line wins over that.
 
 ## Notes
 
-- `hover_keys=1` grabs the keyboard while the pointer is over the bar. That is
-  what lets `space` work without a click first. Keystrokes typed while the
-  pointer rests on the bar go to the bar, not to your window. Set
-  `hover_keys=0` and use `SIGUSR1` if that bothers you.
-- `hide_key` matches the physical key, not the layout.
-- Status markup: `<span foreground=..>`, `<span background=..>`, `<b>`, `<i>`
-  and the XML entities. The i3bar JSON protocol is not supported.
-- No system tray.
-
-## Tested
-
-- Builds clean with `-Wall -Wextra`. `--version` carries the md5 of the
-  source, so you always know which build is running.
-- Config parser: inline comments stripped, `#` inside quoted command text
-  kept, verified against the shipped example.
-- Vertical centering checked at scale 1 and 2: digits sit equally far from
-  both edges.
-- Merged backgrounds verified pixel by pixel: no bare bar shows between two
-  glued cells, so the pair reads as one panel.
-- Media glyphs checked against the font before shipping them: `U+23F8` and
-  the rest of that block are missing from DejaVu, so the config uses glyphs
-  that exist.
-- The folded paint path is now the same code the compositor gets, run
-  headless in tests: the music button lands at 582..608 in its own colour
-  with its binding attached, and a middle click anywhere else falls through
-  to the bar-wide binding.
-- Self-sizing checked across states: 800px idle, 1217px with charging, temp
-  and a docker list, and capped at the 1920px screen when content overflows.
-- Folded strip rendered and sampled pixel by pixel: workspace slots, clock
-  dots, and gauge fills matching their values (47%, 63%, 60%, 88%) against a
-  track that is actually visible.
-- cmus split checked both ways: idle leaves only the button and its click
-  binding, playing keeps the controls at full width beside a truncated title.
-- Focused workspace checked pixel by pixel: solid `cb9b00` from the top row
-  to the bottom row, square corners, and the baseline lift is exactly 2px.
-- Layout DSL parsed and rendered: groups, glued items, hit boxes measured on
-  an 800px bar — workspaces and cells no longer overlap, the last cell lands
-  exactly on the right padding, and a hovered text cell scrolls.
-- Messages tested end to end: fifo round trip, level parsing, cell takeover
-  and hand-back, and the centred-group layout landing on the bar's midpoint.
-- Markup parser, glyph cache, text metrics and the rounded-rect rasterizer
-  run under ASan/UBSan against a real status line: no leaks, no overruns,
-  correct premultiplied output, square top edge, rounded bottom corners.
-- `--dump-config` output re-parses without warnings.
-- Cells tested end to end: config parse, process spawn and reap, thresholds,
-  hidden cells dropping their separators, `sep=0` grouping, `min_w`, and the
-  resulting layout — under ASan/UBSan.
-- `make`, `make debug` and `make clean` run clean.
-- Not yet run against a live compositor — that part is yours.
+- Talks to the sway IPC socket directly: no `swaymsg`, no `jq`.
+- One bar per output, each with its own surface and buffer.
+- Text is rasterised in software into an ARGB32 buffer — no GPU, no SDL.
+- `hover_keys=1` grabs the keyboard only while the pointer is over the bar,
+  which is what makes `hide_key` work without clicking first.
+- A cell with `interval=-1` keeps its command running and takes every line it
+  prints, so a script can push updates the moment they happen.
diff --git a/sw_theme.h b/sw_theme.h
new file mode 100644
index 0000000..63d22c6
--- /dev/null
+++ b/sw_theme.h
@@ -0,0 +1,194 @@
+/* sw_theme.h — one look for swov, appwheel and swbr.
+ *
+ *     ${XDG_CONFIG_HOME:-~/.config}/sw/config
+ *
+ * key=value, '#' comments, same as every other config here. Keys written
+ * before any [section] go to all three programs; keys inside [swov],
+ * [appwheel] or [swbr] go to that one only.
+ *
+ * Outside a section only *role* names are understood (surface, accent, hl,
+ * ...). Each program translates them to its own keys, and roles it has no use
+ * for are dropped. Inside a section every key of that program works as well.
+ *
+ * Order: built-in defaults -> this file -> the program's own config -> the
+ * command line. The specific always wins over the shared.
+ *
+ * Drop-in header, no dependencies. Each program calls sw_shared_apply() with
+ * its own setter, right before it reads its own config file.
+ */
+#ifndef SW_THEME_H
+#define SW_THEME_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+#define SW_MAX_PAIRS 256
+#define SW_KEYLEN     48
+#define SW_VALLEN    256
+#define SW_MAXNATIVE   4
+
+/* ------------------------------------------------------------ role table
+ * NULL = this program has nothing for that role. Several native keys are
+ * separated by commas: swbr paints its focused workspace button with `hl`.
+ */
+typedef struct {
+    const char *role, *swov, *appwheel, *swbr;
+} SwRole;
+
+static const SwRole SW_ROLES[] = {
+    /* role          swov            appwheel   swbr                           */
+    { "bg",          "bg",           "bg",      NULL                           },
+    { "surface",     "tile",         "ring",    "bg"                           },
+    { "surface_alt", "tile_sel",     "ring2",   NULL                           },
+    { "surface_hi",  "tile_hover",   NULL,      "ws_visible_bg"                },
+    { "inset",       "mini_bg",      "center",  NULL                           },
+    { "card",        "card",         NULL,      "ws_bg"                        },
+    { "card_hi",     "card_hover",   NULL,      NULL                           },
+    { "card_focus",  "card_focus",   NULL,      NULL                           },
+    { "text",        "text",         "text",    "text"                         },
+    { "subtext",     "subtext",      NULL,      "ws_fg,ws_visible_fg"          },
+    { "dim",         "dim",          "dim",     "dim"                          },
+    { "hint",        "hint",         NULL,      NULL                           },
+    { "accent",      "accent",       "accent",  "accent,msg_info"              },
+    { "hl",          "hl",           "hl",      "hl,ws_focused_bg,mode_bg"     },
+    { "hltext",      "hltext",       "hltext",  "ws_focused_fg,mode_fg,ws_urgent_fg" },
+    { "current",     "current",      NULL,      NULL                           },
+    { "match",       "match",        NULL,      NULL                           },
+    { "warn",        NULL,           NULL,      "msg_warn"                     },
+    { "urgent",      "urgent",       NULL,      "urgent,ws_urgent_bg,msg_error" },
+    { "ok",          NULL,           NULL,      "running"                      },
+    { "outline",     "outline",      NULL,      "outline,separator_color"      },
+    { "shadow",      "shadow_color", NULL,      NULL                           },
+
+    { "font",          "font",      "font",     "font"     },
+    { "font_bold",     "font_bold", NULL,       NULL       },
+    { "ui_scale",      "ui_scale",  "ui_scale", "ui_scale" },
+    { "anim_ms",       "anim_ms",   "anim_ms",  "anim_ms"  },
+    { "corner_radius", "radius",    NULL,       "radius"   },
+    { "ssaa",          "ssaa",      "ssaa",     NULL       },
+    { "icons",         "icons",     "icons",    NULL       },
+    { "icon_px",       "icon_px",   "icon_px",  NULL       },
+};
+
+/* ------------------------------------------------------------------ path */
+
+static const char *sw_shared_path(void)
+{
+    static char path[512];
+    const char *xdg = getenv("XDG_CONFIG_HOME");
+    const char *home = getenv("HOME");
+    if (xdg && *xdg)       snprintf(path, sizeof path, "%s/sw/config", xdg);
+    else if (home && *home) snprintf(path, sizeof path, "%s/.config/sw/config", home);
+    else return NULL;
+    return path;
+}
+
+/* --------------------------------------------------------------- mapping */
+
+static int sw_ci_eq(const char *a, const char *b)
+{
+    for (; *a && *b; ++a, ++b)
+        if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) return 0;
+    return *a == *b;
+}
+
+static const char *sw_role_for(const SwRole *r, const char *app)
+{
+    if (sw_ci_eq(app, "swov"))     return r->swov;
+    if (sw_ci_eq(app, "appwheel")) return r->appwheel;
+    if (sw_ci_eq(app, "swbr"))     return r->swbr;
+    return NULL;
+}
+
+/* Translate one shared key into the program's own keys.
+ * `scoped` means the key came from a [section], where the program's own key
+ * names are allowed too. Returns how many names were written. */
+static int sw_map(const char *app, const char *key, int scoped,
+                  char out[][SW_KEYLEN], int max)
+{
+    for (size_t i = 0; i < sizeof SW_ROLES / sizeof *SW_ROLES; ++i) {
+        if (!sw_ci_eq(SW_ROLES[i].role, key)) continue;
+        const char *nat = sw_role_for(&SW_ROLES[i], app);
+        if (!nat) return 0;                       /* not this program's business */
+        int n = 0;
+        while (*nat && n < max) {
+            const char *comma = strchr(nat, ',');
+            size_t len = comma ? (size_t)(comma - nat) : strlen(nat);
+            if (len >= SW_KEYLEN) len = SW_KEYLEN - 1;
+            memcpy(out[n], nat, len);
+            out[n][len] = '\0';
+            ++n;
+            if (!comma) break;
+            nat = comma + 1;
+        }
+        return n;
+    }
+    if (!scoped) return 0;                        /* global: roles only */
+    snprintf(out[0], SW_KEYLEN, "%s", key);       /* [section]: pass through */
+    return 1;
+}
+
+/* ---------------------------------------------------------------- reader */
+
+static char *sw_trim(char *s)
+{
+    while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n') ++s;
+    char *e = s + strlen(s);
+    while (e > s && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\r' || e[-1] == '\n')) --e;
+    *e = '\0';
+    return s;
+}
+
+/* Read the shared file and hand every setting that concerns `app` to `set`,
+ * in file order, so a later line overrides an earlier one. Returns 1 if the
+ * file was there. */
+static int sw_shared_apply(const char *app,
+                           void (*set)(void *ud, const char *key, const char *val),
+                           void *ud)
+{
+    const char *path = sw_shared_path();
+    if (!path) return 0;
+    FILE *f = fopen(path, "r");
+    if (!f) return 0;
+
+    char line[1024], section[SW_KEYLEN] = "";
+    int  mine = 1;                                /* before any section: all */
+
+    while (fgets(line, sizeof line, f)) {
+        char *s = sw_trim(line);
+        if (!*s || *s == '#' || *s == ';') continue;
+
+        if (*s == '[') {                          /* [swov] */
+            char *end = strchr(s, ']');
+            if (!end) continue;
+            *end = '\0';
+            snprintf(section, sizeof section, "%s", sw_trim(s + 1));
+            mine = sw_ci_eq(section, app) || sw_ci_eq(section, "all") ||
+                   sw_ci_eq(section, "global");
+            if (sw_ci_eq(section, "all") || sw_ci_eq(section, "global")) section[0] = '\0';
+            continue;
+        }
+        if (!mine) continue;
+
+        char *eq = strchr(s, '=');
+        if (!eq) continue;
+        *eq = '\0';
+        char *k = sw_trim(s), *v = sw_trim(eq + 1);
+        char *hash = strchr(v, '#');              /* trailing comment */
+        if (hash && hash > v && (hash[-1] == ' ' || hash[-1] == '\t')) {
+            *hash = '\0';
+            v = sw_trim(v);
+        }
+        if (!*k) continue;
+
+        char native[SW_MAXNATIVE][SW_KEYLEN];
+        int n = sw_map(app, k, section[0] != '\0', native, SW_MAXNATIVE);
+        for (int i = 0; i < n; ++i) set(ud, native[i], v);
+    }
+    fclose(f);
+    return 1;
+}
+
+#endif /* SW_THEME_H */
diff --git a/swbr.c b/swbr.c
index 1857648..de0d7b3 100644
--- a/swbr.c
+++ b/swbr.c
@@ -46,6 +46,7 @@
 
 #include <ctype.h>
 #include <errno.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <limits.h>
 #include <math.h>
@@ -67,8 +68,10 @@
 #include <time.h>
 #include <unistd.h>
 
+#include "sw_theme.h"
+
 #define APP_ID          "swbr"
-#define SWBR_VERSION  "0.8.1"
+#define SWBR_VERSION  "0.11.0"
 #ifndef SWBR_BUILD                 /* set by the Makefile: md5 of this file */
 #define SWBR_BUILD "unknown"
 #endif
@@ -78,7 +81,8 @@
 #define MAX_BINDS       10
 #define MAX_CELLS       24
 
-enum { SLIM_AUTO, SLIM_TICK, SLIM_BAR, SLIM_CLOCK, SLIM_OFF };
+enum { SLIM_AUTO, SLIM_TICK, SLIM_BAR, SLIM_CLOCK, SLIM_PRESENCE,
+       SLIM_MEDIA, SLIM_OFF };
 
 /* ------------------------------------------------------------------ util */
 
@@ -526,7 +530,8 @@ enum {
     IPC_RUN_COMMAND    = 0,
     IPC_GET_WORKSPACES = 1,
     IPC_SUBSCRIBE      = 2,
-    IPC_GET_OUTPUTS    = 3
+    IPC_GET_OUTPUTS    = 3,
+    IPC_GET_TREE       = 4
 };
 
 static int sway_fd = -1;      /* request socket */
@@ -765,21 +770,268 @@ static void ws_reload(bool full_names)
     jfree(r);
 }
 
+/* ------------------------------------------------------ cpu per workspace
+ *
+ * Who is working, and where. sway knows which pid each window belongs to and
+ * which workspace that window is on; /proc knows how much processor time each
+ * process has used. Put the two together and a workspace has a load.
+ *
+ * A process is credited to the nearest ancestor that owns a window, so a
+ * compiler started in a terminal counts towards the workspace that terminal
+ * is on. Anything with no window above it — daemons, the session itself —
+ * belongs to no workspace and is left out.
+ *
+ * swbr is the one that measures, because a rate needs two samples a few
+ * seconds apart and only a program that stays running has those. It writes
+ * the answer where swov can read it, so swov does not have to measure
+ * anything to draw the same numbers.
+ */
+
+typedef struct { int pid; int ws; } PidWs;      /* window pid -> ws_list index */
+
+static PidWs  cpu_win[512];
+static int    cpu_nwin;
+static double cpu_ws_pct[MAX_WORKSPACES];       /* by ws_list index */
+static char   cpu_ws_name[MAX_WORKSPACES][128];
+static int    cpu_ws_n;
+static double cpu_last_at;
+
+typedef struct {
+    int pid, ppid, ws;
+    unsigned long long own, chld;
+} ProcSample;
+static ProcSample *cpu_prev;
+static int         cpu_prev_n;
+
+typedef struct { int pid, ws; unsigned long long delta; } CpuTop;
+static CpuTop cpu_top[5];      /* the busiest of the last sample, for --probe */
+static int    cpu_top_n;
+
+/* own = this process's own cpu time, chld = the time of the children it has
+ * already reaped. The second one is what makes a build visible: every cc1
+ * lives for a moment and is gone long before the next sample, and its time
+ * lands in make's cutime rather than anywhere we could see it directly. */
+static bool proc_stat_of(int pid, int *ppid,
+                         unsigned long long *own, unsigned long long *chld)
+{
+    char path[64], buf[1024];
+    snprintf(path, sizeof(path), "/proc/%d/stat", pid);
+    FILE *f = fopen(path, "r");
+    if (!f) return false;
+    size_t got = fread(buf, 1, sizeof(buf) - 1, f);
+    fclose(f);
+    buf[got] = 0;
+
+    char *close = strrchr(buf, ')');            /* comm can hold anything */
+    if (!close) return false;
+
+    unsigned long long ut = 0, st = 0;
+    long long cut = 0, cst = 0;
+    int pp = 0;
+    /* after "): state ppid pgrp session tty tpgid flags min_flt cmin_flt
+       maj_flt cmaj_flt utime stime cutime cstime" */
+    if (sscanf(close + 1, " %*c %d %*d %*d %*d %*d %*u %*u %*u %*u %*u %llu %llu %lld %lld",
+               &pp, &ut, &st, &cut, &cst) != 5) return false;
+    if (cut < 0) cut = 0;
+    if (cst < 0) cst = 0;
+    *ppid = pp;
+    *own  = ut + st;
+    if (chld) *chld = (unsigned long long)(cut + cst);
+    return true;
+}
+
+static int cpu_ws_of_pid(int pid)
+{
+    for (int hop = 0; hop < 12 && pid > 1; ++hop) {
+        for (int i = 0; i < cpu_nwin; ++i)
+            if (cpu_win[i].pid == pid) return cpu_win[i].ws;
+        int ppid; unsigned long long own, chld;
+        if (!proc_stat_of(pid, &ppid, &own, &chld)) return -1;
+        pid = ppid;
+    }
+    return -1;
+}
+
+/* every view in sway's tree, as pid -> the workspace it sits on */
+static void cpu_collect_windows(const JV *node, int ws_idx)
+{
+    if (!node || node->type != J_OBJ) return;
+
+    const char *type = jstr(node, "type", "");
+    if (!strcmp(type, "workspace")) {
+        const char *name = jstr(node, "name", "");
+        ws_idx = -1;
+        for (int i = 0; i < ws_count; ++i)
+            if (!strcmp(ws_list[i].name, name)) { ws_idx = i; break; }
+    }
+    int pid = jint(node, "pid", -1);
+    if (pid > 0 && ws_idx >= 0 && cpu_nwin < (int)(sizeof cpu_win / sizeof *cpu_win)) {
+        cpu_win[cpu_nwin].pid = pid;
+        cpu_win[cpu_nwin].ws  = ws_idx;
+        cpu_nwin++;
+    }
+    for (int i = 0; i < node->count; ++i) {
+        JV *kid = node->items[i];
+        if (!kid) continue;
+        if (kid->type == J_ARR)
+            for (int j = 0; j < kid->count; ++j) cpu_collect_windows(kid->items[j], ws_idx);
+        else if (kid->type == J_OBJ)
+            cpu_collect_windows(kid, ws_idx);
+    }
+}
+
+static void cpu_write_cache(void)
+{
+    const char *dir = getenv("XDG_RUNTIME_DIR");
+    char path[512], tmp[540];
+    if (dir && *dir) snprintf(path, sizeof(path), "%s/swbr-cpu", dir);
+    else {
+        const char *home = getenv("HOME");
+        if (!home) return;
+        snprintf(path, sizeof(path), "%s/.cache/swbr-cpu", home);
+    }
+    snprintf(tmp, sizeof(tmp), "%s.new", path);
+
+    FILE *f = fopen(tmp, "w");
+    if (!f) return;
+    for (int i = 0; i < cpu_ws_n; ++i)
+        fprintf(f, "%s\t%.1f\n", cpu_ws_name[i], cpu_ws_pct[i]);
+    fclose(f);
+    if (rename(tmp, path) != 0) unlink(tmp);
+}
+
+/* one sample; the first call only records, the ones after it measure */
+static void cpu_sample(void)
+{
+    JV *tree = sway_query(IPC_GET_TREE);
+    if (!tree) return;
+    cpu_nwin = 0;
+    cpu_collect_windows(tree, -1);
+    jfree(tree);
+
+    DIR *d = opendir("/proc");
+    if (!d) return;
+
+    int cap = 1024, n = 0;
+    ProcSample *now = (ProcSample *)xmalloc((size_t)cap * sizeof *now);
+    struct dirent *e;
+    while ((e = readdir(d)) != NULL) {
+        const char *p = e->d_name;
+        bool digits = *p != 0;
+        for (const char *q = p; *q; ++q) if (*q < '0' || *q > '9') { digits = false; break; }
+        if (!digits) continue;
+        if (n == cap) { cap *= 2; now = (ProcSample *)xrealloc(now, (size_t)cap * sizeof *now); }
+        int pid = atoi(p), ppid; unsigned long long own, chld;
+        if (!proc_stat_of(pid, &ppid, &own, &chld)) continue;
+        now[n].pid = pid; now[n].ppid = ppid;
+        now[n].own = own; now[n].chld = chld; now[n].ws = -1;
+        n++;
+    }
+    closedir(d);
+
+    double at = now_ms() / 1000.0;
+    double dt = at - cpu_last_at;
+
+    if (cpu_prev && dt > 0.2) {
+        long hz = sysconf(_SC_CLK_TCK);
+        if (hz <= 0) hz = 100;
+        long ncpu = sysconf(_SC_NPROCESSORS_ONLN);
+        if (ncpu <= 0) ncpu = 1;
+
+        double used[MAX_WORKSPACES] = { 0 };
+        cpu_top_n = 0;
+        for (int i = 0; i < n; ++i) {
+            const ProcSample *was = NULL;
+            for (int j = 0; j < cpu_prev_n; ++j)          /* same order, mostly */
+                if (cpu_prev[j].pid == now[i].pid) { was = &cpu_prev[j]; break; }
+
+            unsigned long long total = now[i].own + now[i].chld, before = 0;
+            if (was && was->own + was->chld <= total) before = was->own + was->chld;
+            /* no `was` means it was born inside this window, so all of its
+               time belongs to this window */
+            unsigned long long delta = total - before;
+
+            int ws = cpu_ws_of_pid(now[i].pid);
+            now[i].ws = ws;
+            if (!delta) continue;
+            if (ws >= 0 && ws < MAX_WORKSPACES)
+                used[ws] += (double)delta / (double)hz;
+
+            /* keep the five busiest, wherever they went */
+            int slot = cpu_top_n < 5 ? cpu_top_n : -1;
+            if (slot < 0) {
+                unsigned long long worst = delta;
+                for (int j = 0; j < 5; ++j)
+                    if (cpu_top[j].delta < worst) { worst = cpu_top[j].delta; slot = j; }
+            } else {
+                cpu_top_n++;
+            }
+            if (slot >= 0) {
+                cpu_top[slot].pid = now[i].pid;
+                cpu_top[slot].ws = ws;
+                cpu_top[slot].delta = delta;
+            }
+        }
+        /* A process that died since the last sample had its whole lifetime
+         * added to its parent's reaped-children total, and we already counted
+         * the part of it that ran before the last sample. Take that back, or
+         * every exit is counted twice. */
+        for (int j = 0; j < cpu_prev_n; ++j) {
+            bool alive = false;
+            for (int i = 0; i < n; ++i)
+                if (now[i].pid == cpu_prev[j].pid) { alive = true; break; }
+            if (alive) continue;
+            int ws = cpu_prev[j].ws;
+            if (ws >= 0 && ws < MAX_WORKSPACES)
+                used[ws] -= (double)(cpu_prev[j].own + cpu_prev[j].chld) / (double)hz;
+        }
+
+        cpu_ws_n = 0;
+        for (int i = 0; i < ws_count && i < MAX_WORKSPACES; ++i) {
+            if (used[i] < 0) used[i] = 0;
+            double pct = used[i] / dt / (double)ncpu * 100.0;
+            if (pct < 0) pct = 0;
+            if (pct > 100) pct = 100;
+            cpu_ws_pct[i] = pct;
+            str_set(cpu_ws_name[cpu_ws_n], sizeof(cpu_ws_name[0]), ws_list[i].name);
+            cpu_ws_n++;
+        }
+        cpu_write_cache();
+    }
+
+    free(cpu_prev);
+    cpu_prev = now;
+    cpu_prev_n = n;
+    cpu_last_at = at;
+}
+
 /* ---------------------------------------------------------------- config */
 
 /* A cell is one command whose output is drawn on the right hand side.
- * Declared with `cell=NAME`, configured with `NAME.field=value`. */
+ * Declared with `cell=NAME`, configured with `NAME.field=value`.
+ *
+ * `NAME.source=cmus` is the exception: swbr runs cmus-remote itself and reads
+ * the answer, so whether something is playing is a fact rather than a guess
+ * at what a script happened to print. */
+enum { SRC_CMD, SRC_CMUS };
+enum { PS_UNKNOWN = -1, PS_STOPPED = 0, PS_PAUSED = 1, PS_PLAYING = 2 };
 typedef struct {
     char  name[32];
     char  cmd[2048];
     int   interval;              /* seconds; 0 = run once; -1 = keep streaming */
     char  fmt[128];              /* %s is replaced by the output */
     char  empty[128];            /* drawn when the command prints nothing */
+    char  slim_on[64];           /* output containing this = the "on" state */
+    int   source;                /* SRC_CMD, or a player swbr knows itself   */
+    char  src_fmt[128];          /* how a known player is laid out           */
+    int   state;                 /* what that player is doing: PS_*          */
     int   gap;                   /* -1 = cell_gap */
     int   pad;                   /* inner padding, left and right */
     int   slim;                  /* how it shows in the folded strip */
     float slim_min, slim_max;    /* value range a gauge maps, default 0..100 */
     int   slim_w;                /* custom width in the folded strip */
+    Col   slim_color, slim_color2;   /* strip colour, and the minute colour */
+    bool  has_slim_color, has_slim_color2;
     Col   color, bg;
     bool  has_color, has_bg;
     Col   warn_col, crit_col;
@@ -845,7 +1097,11 @@ typedef struct {
     char bind[MAX_BINDS][512];
 
     /* colors */
-    Col bg, text, dim, accent, hl, urgent, outline;
+    Col bg, text, dim, accent, hl, urgent, outline, running, slim_warm;
+    int ws_cpu;                  /* the load dot on a workspace button   */
+    int ws_cpu_interval;         /* seconds between samples              */
+    float ws_cpu_min, ws_cpu_full;  /* below min nothing shows; full = the
+                                       load that paints it urgent        */
     Col ws_bg, ws_fg, ws_focused_bg, ws_focused_fg;
     Col ws_visible_bg, ws_visible_fg, ws_urgent_bg, ws_urgent_fg;
     Col mode_bg, mode_fg;
@@ -911,9 +1167,15 @@ static void config_defaults(Config *c)
     c->accent        = (Col){ 0x89, 0xaf, 0xc4, 0xff };
     c->hl            = (Col){ 0xcb, 0x9b, 0x00, 0xff };
     c->urgent        = (Col){ 0xe0, 0x53, 0x3c, 0xff };
+    c->running       = (Col){ 0x3d, 0xdc, 0x84, 0xff };
+    c->slim_warm     = (Col){ 0xe8, 0x96, 0x3c, 0xff };
     c->outline       = (Col){ 0x0a, 0x0e, 0x14, 0x99 };
     c->ws_bg         = (Col){ 0x1e, 0x27, 0x33, 0x00 };
     c->ws_fg         = (Col){ 0xb3, 0xc0, 0xcd, 0xff };
+    c->ws_cpu          = 1;
+    c->ws_cpu_interval = 3;
+    c->ws_cpu_min      = 4.0f;
+    c->ws_cpu_full     = 60.0f;
     c->ws_focused_bg = (Col){ 0xcb, 0x9b, 0x00, 0xff };
     c->ws_focused_fg = (Col){ 0x14, 0x14, 0x14, 0xff };
     c->ws_visible_bg = (Col){ 0x3c, 0x4a, 0x5c, 0xff };
@@ -971,19 +1233,55 @@ static void cell_set(Config *c, const char *name, const char *k, const char *v)
 {
     Cell *e = cell_add(c, name);
     if (!e) return;
-    if (!strcmp(k, "cmd") || !strcmp(k, "command")) str_set(e->cmd, sizeof(e->cmd), v);
+    if (!strcmp(k, "cmd") || !strcmp(k, "command")) {
+        str_set(e->cmd, sizeof(e->cmd), v);
+        e->source = SRC_CMD;          /* your command prints, swbr shows it */
+    }
     else if (!strcmp(k, "interval")) e->interval = atoi(v);
     else if (!strcmp(k, "fmt") || !strcmp(k, "format")) str_set(e->fmt, sizeof(e->fmt), v);
     else if (!strcmp(k, "empty")) str_set(e->empty, sizeof(e->empty), v);
+    else if (!strcmp(k, "slim_on")) str_set(e->slim_on, sizeof(e->slim_on), v);
+    else if (!strcmp(k, "src_fmt") || !strcmp(k, "cmus_fmt"))
+        str_set(e->src_fmt, sizeof(e->src_fmt), v);
+    else if (!strcmp(k, "source")) {
+        if (!strcasecmp(v, "cmus")) {
+            e->source = SRC_CMUS;
+            /* everything a cmus cell needs, so the config line is enough.
+             * The command is not optional: swbr parses cmus-remote's own
+             * output, so a leftover cmd from a script would leave the cell
+             * with nothing it can read. */
+            str_set(e->cmd, sizeof(e->cmd), "cmus-remote -Q 2>/dev/null");
+            if (!e->interval) e->interval = 2;
+            if (e->slim == SLIM_AUTO) e->slim = SLIM_MEDIA;
+            if (!*e->bind[1])  str_set(e->bind[1], sizeof(e->bind[1]), "cmus-remote -u");
+            if (!*e->bind[2])  str_set(e->bind[2], sizeof(e->bind[2]), "cmus-remote -n");
+            if (!*e->bind[3])  str_set(e->bind[3], sizeof(e->bind[3]), "cmus-remote -r");
+        } else {
+            e->source = SRC_CMD;
+        }
+    }
     else if (!strcmp(k, "gap")) e->gap = atoi(v);
     else if (!strcmp(k, "pad")) e->pad = atoi(v);
     else if (!strcmp(k, "slim_min")) e->slim_min = (float)atof(v);
     else if (!strcmp(k, "slim_max")) e->slim_max = (float)atof(v);
     else if (!strcmp(k, "slim_w")) e->slim_w = atoi(v);
+    else if (!strcmp(k, "slim_color")) {
+        e->slim_color.a = 255;
+        parse_color(v, &e->slim_color);
+        e->has_slim_color = true;
+    }
+    else if (!strcmp(k, "slim_color2")) {
+        e->slim_color2.a = 255;
+        parse_color(v, &e->slim_color2);
+        e->has_slim_color2 = true;
+    }
     else if (!strcmp(k, "slim")) {
         if (!strcasecmp(v, "bar")) e->slim = SLIM_BAR;
         else if (!strcasecmp(v, "clock")) e->slim = SLIM_CLOCK;
         else if (!strcasecmp(v, "tick")) e->slim = SLIM_TICK;
+        else if (!strcasecmp(v, "presence") || !strcasecmp(v, "running") ||
+                 !strcasecmp(v, "app")) e->slim = SLIM_PRESENCE;
+        else if (!strcasecmp(v, "media") || !strcasecmp(v, "play")) e->slim = SLIM_MEDIA;
         else if (!strcasecmp(v, "off") || !strcmp(v, "0")) e->slim = SLIM_OFF;
         else e->slim = SLIM_AUTO;
     }
@@ -1086,9 +1384,15 @@ static void config_set(Config *c, const char *k, const char *v)
     else if (!strcmp(k, "accent")) parse_color(v, &c->accent);
     else if (!strcmp(k, "hl")) parse_color(v, &c->hl);
     else if (!strcmp(k, "urgent")) parse_color(v, &c->urgent);
+    else if (!strcmp(k, "running")) parse_color(v, &c->running);
+    else if (!strcmp(k, "slim_warm")) parse_color(v, &c->slim_warm);
     else if (!strcmp(k, "outline")) parse_color(v, &c->outline);
     else if (!strcmp(k, "ws_bg")) parse_color(v, &c->ws_bg);
     else if (!strcmp(k, "ws_fg")) parse_color(v, &c->ws_fg);
+    else if (!strcmp(k, "ws_cpu")) c->ws_cpu = atoi(v);
+    else if (!strcmp(k, "ws_cpu_interval")) c->ws_cpu_interval = atoi(v);
+    else if (!strcmp(k, "ws_cpu_min")) c->ws_cpu_min = (float)atof(v);
+    else if (!strcmp(k, "ws_cpu_full")) c->ws_cpu_full = (float)atof(v);
     else if (!strcmp(k, "ws_focused_bg")) parse_color(v, &c->ws_focused_bg);
     else if (!strcmp(k, "ws_focused_fg")) parse_color(v, &c->ws_focused_fg);
     else if (!strcmp(k, "ws_visible_bg")) parse_color(v, &c->ws_visible_bg);
@@ -1114,6 +1418,12 @@ static void strip_comment(char *v)
     }
 }
 
+/* the shared ~/.config/sw/config, translated into swbr's own keys */
+static void config_set_shared(void *ud, const char *k, const char *v)
+{
+    config_set((Config *)ud, k, v);
+}
+
 static void config_load(Config *c, const char *path)
 {
     FILE *f = fopen(path, "r");
@@ -1884,8 +2194,22 @@ static Col msg_color(void)
     return msg_level >= 2 ? cfg.msg_error : (msg_level == 1 ? cfg.msg_warn : cfg.msg_info);
 }
 
+static int ctrl_req = 0;          /* 1 toggle, 2 fold, 3 unfold */
+
+static bool msg_control(const char *t)
+{
+    if (*t != ':') return false;
+    t++;
+    if (!strcasecmp(t, "toggle")) ctrl_req = 1;
+    else if (!strcasecmp(t, "fold") || !strcasecmp(t, "slim")) ctrl_req = 2;
+    else if (!strcasecmp(t, "unfold") || !strcasecmp(t, "full")) ctrl_req = 3;
+    else fprintf(stderr, "swbr: unknown control ':%s'\n", t);
+    return true;
+}
+
 static void msg_set(const char *line)
 {
+    if (msg_control(line)) return;
     const char *t = line;
     int lvl = 0;
     if (!strncasecmp(t, "error:", 6))        { lvl = 2; t += 6; }
@@ -1905,6 +2229,61 @@ static void msg_set(const char *line)
     msg_until = cfg.msg_timeout > 0 ? now_ms() + (uint32_t)cfg.msg_timeout * 1000u : 0;
 }
 
+/* Take over from a bar that is already running.
+ *
+ * `pkill swbr` in the sway config does not work reliably: sway forks each
+ * exec_always without waiting for it, so the kill and the new bar race, and
+ * the kill often wins — it terminates the instance that was just started. A
+ * bar that clears the way for itself has nothing to race against.
+ */
+static void replace_running(void)
+{
+    DIR *d = opendir("/proc");
+    if (!d) return;
+
+    pid_t me = getpid(), victims[32];
+    int   n = 0;
+    struct dirent *e;
+    while ((e = readdir(d)) && n < (int)(sizeof victims / sizeof *victims)) {
+        bool digits = e->d_name[0] != 0;
+        for (const char *q = e->d_name; *q; ++q)
+            if (*q < '0' || *q > '9') { digits = false; break; }
+        if (!digits) continue;
+
+        pid_t pid = (pid_t)atoi(e->d_name);
+        if (pid == me || pid <= 1) continue;
+
+        char path[64], comm[64] = "";
+        snprintf(path, sizeof(path), "/proc/%d/comm", (int)pid);
+        FILE *f = fopen(path, "r");
+        if (!f) continue;                       /* gone, or not ours to read */
+        if (fgets(comm, sizeof(comm), f)) {
+            char *nl = strchr(comm, '\n');
+            if (nl) *nl = 0;
+        }
+        fclose(f);
+        if (strcmp(comm, APP_ID)) continue;
+
+        struct stat st;
+        snprintf(path, sizeof(path), "/proc/%d", (int)pid);
+        if (stat(path, &st) != 0 || st.st_uid != getuid()) continue;
+
+        victims[n++] = pid;
+    }
+    closedir(d);
+
+    for (int i = 0; i < n; ++i) kill(victims[i], SIGTERM);
+
+    for (int wait = 0; wait < 40 && n; ++wait) {   /* up to two seconds */
+        usleep(50000);
+        int left = 0;
+        for (int i = 0; i < n; ++i)
+            if (kill(victims[i], 0) == 0) victims[left++] = victims[i];
+        n = left;
+    }
+    for (int i = 0; i < n; ++i) kill(victims[i], SIGKILL);   /* stubborn */
+}
+
 static void msg_open(void)
 {
     msg_resolve_path();
@@ -1990,9 +2369,91 @@ static void cell_spawn(Cell *e)
     e->len = 0;
 }
 
+/* `cmus-remote -Q` prints one key per line:
+ *     status playing
+ *     file /music/x.flac
+ *     tag artist Boards of Canada
+ *     tag title Roygbiv
+ * Nothing at all means cmus is not running. */
+static void cmus_parse(Cell *e)
+{
+    char status[32] = "", artist[256] = "", title[256] = "", file[512] = "";
+
+    for (char *line = e->buf; line && *line; ) {
+        char *nl = strchr(line, '\n');
+        if (nl) *nl = 0;
+        char *v;
+        if (!strncmp(line, "status ", 7))
+            str_set(status, sizeof(status), trim(line + 7));
+        else if (!strncmp(line, "file ", 5))
+            str_set(file, sizeof(file), trim(line + 5));
+        else if (!strncmp(line, "tag artist ", 11))
+            str_set(artist, sizeof(artist), trim(line + 11));
+        else if (!strncmp(line, "tag title ", 10))
+            str_set(title, sizeof(title), trim(line + 10));
+        else if (!strncmp(line, "tag albumartist ", 16) && !*artist)
+            str_set(artist, sizeof(artist), trim(line + 16));
+        (void)v;
+        line = nl ? nl + 1 : NULL;
+    }
+
+    e->state = !*status                      ? PS_UNKNOWN
+             : !strcmp(status, "playing")    ? PS_PLAYING
+             : !strcmp(status, "paused")     ? PS_PAUSED
+                                             : PS_STOPPED;
+
+    if (e->state == PS_UNKNOWN || e->state == PS_STOPPED) { e->out[0] = 0; return; }
+
+    if (!*title && *file) {                              /* fall back to the name */
+        const char *base = strrchr(file, '/');
+        str_set(title, sizeof(title), base ? base + 1 : file);
+        char *dot = strrchr(title, '.');
+        if (dot && dot != title) *dot = 0;
+    }
+
+    /* ▶ while playing, ⏸ while paused — the icon is the state, so it is right
+     * whether or not the player prints one */
+    const char *icon = e->state == PS_PLAYING ? "\xe2\x96\xb6" : "\xe2\x8f\xb8";
+
+    char name[600];
+    if (*artist && *title) snprintf(name, sizeof(name), "%s — %s", artist, title);
+    else                   snprintf(name, sizeof(name), "%s", title);
+
+    /* %i icon  %n artist — title  %a artist  %t title  %s playing|paused
+     * so one cell can be just the icon and another just the name */
+    const char *f = *e->src_fmt ? e->src_fmt : "%i %n";
+    size_t o = 0;
+    for (const char *p = f; *p && o < sizeof(e->out) - 1; ++p) {
+        const char *ins = NULL;
+        if (*p == '%' && p[1]) {
+            switch (p[1]) {
+            case 'i': ins = icon;   break;
+            case 'n': ins = name;   break;
+            case 'a': ins = artist; break;
+            case 't': ins = title;  break;
+            case 's': ins = e->state == PS_PLAYING ? "playing" : "paused"; break;
+            case '%': ins = "%";    break;
+            default:  break;
+            }
+        }
+        if (!ins) { e->out[o++] = *p; continue; }
+        ++p;
+        size_t l = strlen(ins);
+        if (o + l >= sizeof(e->out)) l = sizeof(e->out) - 1 - o;
+        memcpy(e->out + o, ins, l);
+        o += l;
+    }
+    e->out[o] = 0;
+    char *t = trim(e->out);
+    if (t != e->out) memmove(e->out, t, strlen(t) + 1);
+}
+
 static void cell_finish(Cell *e)
 {
     e->buf[e->len] = 0;
+
+    if (e->source == SRC_CMUS) { cmus_parse(e); e->len = 0; return; }
+
     char *t = trim(e->buf);
     for (char *q = t; *q; ++q) if (*q == '\n' || *q == '\r' || *q == '\t') *q = ' ';
     str_set(e->out, sizeof(e->out), t);
@@ -2070,7 +2531,14 @@ static void cell_text(Cell *e, char *out, size_t cap)
     if (!*e->fmt) { str_set(out, cap, e->out); return; }
     const char *pc = strstr(e->fmt, "%s");
     if (!pc) { str_set(out, cap, e->fmt); return; }
-    snprintf(out, cap, "%.*s%s%s", (int)(pc - e->fmt), e->fmt, e->out, pc + 2);
+    size_t head = (size_t)(pc - e->fmt);
+    if (head >= cap) head = cap - 1;
+    memcpy(out, e->fmt, head);
+    out[head] = 0;
+    size_t n = strlen(out);
+    if (n < cap - 1) str_set(out + n, cap - n, e->out);
+    n = strlen(out);
+    if (n < cap - 1) str_set(out + n, cap - n, pc + 2);
 }
 
 /* the first number in the output, ignoring markup tags */
@@ -2301,6 +2769,17 @@ static float baseline_for(Font *f, float y0, float vis, float s)
     return y0 + vis * 0.5f + (f->ascent - f->lineh * 0.5f) + cfg.text_y * s;
 }
 
+static Col col_mix(Col a, Col b, float t)
+{
+    t = clampf(t, 0.0f, 1.0f);
+    Col o;
+    o.r = (uint8_t)((float)a.r + ((float)b.r - (float)a.r) * t);
+    o.g = (uint8_t)((float)a.g + ((float)b.g - (float)a.g) * t);
+    o.b = (uint8_t)((float)a.b + ((float)b.b - (float)a.b) * t);
+    o.a = (uint8_t)((float)a.a + ((float)b.a - (float)a.a) * t);
+    return o;
+}
+
 static Col col_scale_alpha(Col c, float f)
 {
     c.a = (uint8_t)clampi((int)lroundf((float)c.a * clampf(f, 0.0f, 1.0f)), 0, 255);
@@ -2341,29 +2820,117 @@ static float slim_width_of(Cell *e, int mode, float s)
     switch (mode) {
     case SLIM_CLOCK: return 12.0f * 3.0f * s + 11.0f * 2.0f * s;
     case SLIM_BAR:   return 40.0f * s;
+    case SLIM_PRESENCE: return 10.0f * s;
+    case SLIM_MEDIA: return 26.0f * s;
     case SLIM_OFF:   return 0.0f;
     default:         return 12.0f * s;
     }
 }
 
 
+/* The colour a cell shows on the strip: its own slim_color, else the colour
+ * it painted its own text with, so folding never changes what a thing looks
+ * like. A cyan clock stays cyan. */
+static Col slim_color_of(Cell *e)
+{
+    if (e->has_slim_color) return e->slim_color;
+    char txt[1024];
+    Col base;
+    cell_display(e, txt, sizeof(txt), &base);
+    if (e->markup) {
+        Runs rs;
+        int saved = cfg.markup;
+        cfg.markup = 1;
+        markup_parse(txt, &rs, base);
+        cfg.markup = saved;
+        for (int i = 0; i < rs.n; ++i)
+            if (rs.v[i].has_fg) return rs.v[i].fg;
+    }
+    return base;
+}
+
+/* +, an up arrow or a bolt mean charging; -, a down arrow mean draining;
+ * =, a tick mean full. Anything else: no direction shown. */
+static int slim_direction(const char *s)
+{
+    if (strchr(s, '+') || strstr(s, "\xe2\x86\x91") || strstr(s, "\xe2\x9a\xa1")) return 1;
+    if (strchr(s, '=') || strstr(s, "\xe2\x9c\x93")) return 0;
+    if (strchr(s, '-') || strstr(s, "\xe2\x86\x93")) return -1;
+    return 2;
+}
+
+/* 1 = playing, 0 = not, -1 = the cell has said nothing at all yet.
+ *
+ * `slim_on` is the reliable way: give it a word your command prints only while
+ * something is playing. Without it we guess from the usual glyphs, which only
+ * works if your player happens to print one of them. `swbr --probe` prints
+ * what this decided for every cell, next to the text it decided it from. */
+static int media_state(const Cell *e)
+{
+    /* A player swbr talks to itself has already said which it is. Paused is
+     * not playing, and that is the distinction the strip is there to show. */
+    if (e->source != SRC_CMD) {
+        if (e->state == PS_UNKNOWN) return -1;
+        return e->state == PS_PLAYING ? 1 : 0;
+    }
+    if (!e->out[0]) return -1;
+    if (*e->slim_on) return strstr(e->out, e->slim_on) != NULL;
+
+    /* Paused first: a bar showing "⏸ Artist — Title" is not playing, and
+     * looking for the play glyph first would call it playing anyway. */
+    static const char *PAUSED[] = {
+        "\xe2\x8f\xb8",      /* ⏸ */
+        "\xe2\x9d\x9a",      /* ❚ */
+        "paused",
+    };
+    for (size_t i = 0; i < sizeof PAUSED / sizeof *PAUSED; ++i)
+        if (strcasestr(e->out, PAUSED[i])) return 0;
+
+    static const char *PLAYING[] = {
+        "\xe2\x96\xb6",      /* ▶ */
+        "\xf0\x9f\x8e\xb5", /* 🎵 */
+        "\xe2\x99\xaa",      /* ♪ */
+        "playing",
+    };
+    for (size_t i = 0; i < sizeof PLAYING / sizeof *PLAYING; ++i)
+        if (strcasestr(e->out, PLAYING[i])) return 1;
+    return 0;
+}
+
 static void slim_draw_one(Canvas *cv, Cell *e, int mode, float x, float y,
                           float w, float h, float s)
 {
-    Col c = cell_color(e);
+    Col c = slim_color_of(e);
+    if (mode == SLIM_PRESENCE) {
+        /* the cell only exists while its program does, so a block here means
+         * "that one is up" — its own colour, not shared with any gauge */
+        if (!e->has_slim_color) c = cfg.running;
+        fill_rect(cv, x, y, w, h, c);
+        return;
+    }
     if (mode == SLIM_CLOCK) {
         time_t t = time(NULL);
         struct tm tmv;
         localtime_r(&t, &tmv);
-        int hour = tmv.tm_hour % 12;
+        int hour = (tmv.tm_hour + 11) % 12 + 1;    /* 1..12, 0:xx reads as 12 */
         float dot = 3.0f * s, gap = 2.0f * s;
+        /* every dot up to the hour, so you count bars instead of finding the
+         * one lit position: 2 bars at 02:00, still 2 at 02:55. Morning uses
+         * the second colour, afternoon the main one, so 1:00 and 13:00 differ. */
+        Col hc = tmv.tm_hour >= 12 ? c
+               : (e->has_slim_color2 ? e->slim_color2 : cfg.accent);
         for (int i = 0; i < 12; ++i) {
             Col dc = cfg.dim;
-            float dw = dot;
-            if (i < hour) dc = cfg.accent;
-            else if (i == hour) { dc = cfg.hl; dw = dot * (0.35f + 0.65f * (float)tmv.tm_min / 59.0f); }
-            fill_rect(cv, x + (float)i * (dot + gap), y, dw, h, dc);
+            dc.a = 90;
+            if (i < hour) dc = hc;
+            fill_rect(cv, x + (float)i * (dot + gap), y, dot, h, dc);
         }
+        /* the minutes run along the bottom half, in the second colour */
+        Col mc = e->has_slim_color2 ? e->slim_color2 : cfg.accent;
+        mc.a = 200;
+        float span = 12.0f * dot + 11.0f * gap;
+        float half = h * 0.5f;
+        fill_rect(cv, x, y + h - half, span * (float)tmv.tm_min / 60.0f, half, mc);
         return;
     }
     if (mode == SLIM_BAR) {
@@ -2372,16 +2939,94 @@ static void slim_draw_one(Canvas *cv, Cell *e, int mode, float x, float y,
         float lo = e->slim_min, hi = e->slim_max;
         float f = (hi > lo) ? (v - lo) / (hi - lo) : 0.0f;
         f = clampf(f, 0.0f, 1.0f);
+        int dir = slim_direction(e->out);
+        Col fillc = c;                             /* the fill keeps whatever
+                                                    * warn/crit decided */
         Col track = cfg.dim;                       /* visible, or a part filled
                                                     * gauge just looks short */
         track.a = 110;
         fill_rect(cv, x, y, w, h, track);
-        if (have) fill_rect(cv, x, y, w * f, h, c);
+        if (have) fill_rect(cv, x, y, w * f, h, fillc);
+        if (dir == 2) return;
+
+        /* The pips run a colour ramp away from the fill: warm into red while
+         * it drains, warm into green while it fills. Direction and roughly
+         * how hard it is working, in three dots. */
+        Col far = dir > 0 ? cfg.running : (dir == 0 ? cfg.running : cfg.urgent);
+        float edge = x + w * f, pip = 2.0f * s, step = 3.0f * s;
+        for (int i = 0; i < 3; ++i) {
+            /* Start most of the way to the far colour, not at the warm end:
+             * ramping from 0 left the brightest pip orange and the green one
+             * dim, so charging and draining looked much the same. */
+            Col pc = col_mix(cfg.slim_warm, far, 0.45f + (float)i * 0.275f);
+            pc.a = (uint8_t)(245 - i * 35);
+            float px_ = dir > 0 ? edge + (float)i * step
+                      : dir < 0 ? edge - (float)(i + 1) * step
+                      : x + w - (float)(3 - i) * step;
+            if (px_ < x || px_ + pip > x + w) continue;
+            fill_rect(cv, px_, y, pip, h, pc);
+        }
         return;
     }
+    if (mode == SLIM_MEDIA) {
+        /* Playing: two full-height bars, the pause glyph at five pixels, in
+         * the cell's own colour. Not playing: one short stub at a third of
+         * the height, well under half as bright. Same shape at two sizes read
+         * as the same thing, so the two states differ in height as well. */
+        int st = media_state(e);
+        if (st > 0) {                             /* playing: two full bars */
+            float bar = w * 0.38f, gapw = w - 2.0f * bar;
+            fill_rect(cv, x, y, bar, h, c);
+            fill_rect(cv, x + bar + gapw, y, bar, h, c);
+        } else if (st == 0) {                     /* paused: the same two,
+                                                     short and dim */
+            Col d = c;
+            d.a = 110;
+            float bar = w * 0.38f, gapw = w - 2.0f * bar;
+            fill_rect(cv, x, y + h * 0.34f, bar, h * 0.32f, d);
+            fill_rect(cv, x + bar + gapw, y + h * 0.34f, bar, h * 0.32f, d);
+        }
+        return;                                   /* st < 0: nothing to show */
+    }
     fill_rect(cv, x, y, w, h, c);
 }
 
+/* A disc filled from the bottom: empty at rest, full when it is working hard.
+ * `frac` 0..1 is how much of it is lit. */
+static void fill_moon(Canvas *cv, float cx, float cy, float r, float frac, Col c)
+{
+    if (r < 0.5f) return;
+    float top = cy + r - 2.0f * r * clampf(frac, 0.0f, 1.0f);
+    for (float dy = -r; dy <= r; dy += 1.0f) {
+        float y = cy + dy;
+        if (y < top) continue;
+        float half = sqrtf(r * r - dy * dy);
+        fill_rect(cv, cx - half, y, half * 2.0f, 1.0f, c);
+    }
+}
+
+/* How busy, as a colour: quiet green through the accent into urgent. Nothing
+ * at all below ws_cpu_min, so an idle bar stays clean. */
+static bool cpu_load_col(int ws_idx, Col *out, float *frac)
+{
+    if (!cfg.ws_cpu || ws_idx < 0 || ws_idx >= cpu_ws_n) return false;
+    double pct = cpu_ws_pct[ws_idx];
+    if (pct < cfg.ws_cpu_min) return false;
+
+    float f = cfg.ws_cpu_full > cfg.ws_cpu_min
+            ? (float)(pct - cfg.ws_cpu_min) / (cfg.ws_cpu_full - cfg.ws_cpu_min)
+            : 1.0f;
+    f = clampf(f, 0.0f, 1.0f);
+    *frac = f;
+    Col hot = f < 0.5f ? col_mix(cfg.accent, cfg.slim_warm, f * 2.0f)
+                       : col_mix(cfg.slim_warm, cfg.urgent, (f - 0.5f) * 2.0f);
+    /* held back towards the bar's own dim, and never fully opaque: it should
+     * be readable out of the corner of an eye, not competing with the name */
+    *out = col_mix(cfg.dim, hot, 0.6f + 0.4f * f);
+    out->a = (uint8_t)(190 + 65 * f);
+    return true;
+}
+
 static void draw_signals(Canvas *cv, Bar *b, float y, float h, float s, Runs *rs)
 {
     if (!cfg.signals) return;
@@ -2394,12 +3039,13 @@ static void draw_signals(Canvas *cv, Bar *b, float y, float h, float s, Runs *rs
     }
 
     /* one fixed slot per workspace number, so slot 3 is always workspace 3 */
-    int slots = 0;
-    for (int i = 0; i < ws_count; ++i)
-        if (ws_on_bar(b, &ws_list[i]) && ws_list[i].num > slots) slots = ws_list[i].num;
-    if (slots <= 0)
-        for (int i = 0; i < ws_count; ++i) if (ws_on_bar(b, &ws_list[i])) slots++;
-    if (cfg.slim_ws_slots > 0 && slots > cfg.slim_ws_slots) slots = cfg.slim_ws_slots;
+    /* always the full set, so an empty workspace still holds its place and
+     * the lit ones never move */
+    int slots = cfg.slim_ws_slots;
+    if (slots <= 0) {
+        for (int i = 0; i < ws_count; ++i)
+            if (ws_on_bar(b, &ws_list[i]) && ws_list[i].num > slots) slots = ws_list[i].num;
+    }
 
     float tick = 14.0f * s;
     b->hits = 0;
@@ -2415,6 +3061,17 @@ static void draw_signals(Canvas *cv, Bar *b, float y, float h, float s, Runs *rs
             else { c = cfg.dim; c.a = 170; }
         }
         fill_rect(cv, x, y, tick, h, c);
+        if (w) {
+            int wi = (int)(w - ws_list);
+            Col lc; float lf;
+            if (cpu_load_col(wi, &lc, &lf)) {
+                /* the slot is only a few pixels tall, so the load is drawn as
+                 * a lit portion of it rising from the bottom rather than as a
+                 * dot nobody would see */
+                float lh = clampf(h * (0.35f + 0.65f * lf), 1.0f, h);
+                fill_rect(cv, x, y + h - lh, tick, lh, lc);
+            }
+        }
         if (w && b->hits < MAX_WORKSPACES) {      /* still switchable when folded */
             b->hit[b->hits].x0 = x / s;
             b->hit[b->hits].x1 = (x + tick) / s;
@@ -2534,6 +3191,22 @@ static void ws_block_draw(Canvas *cv, Bar *b, Font *f, float x, float y0,
         pill(cv, x, y0, vis, bwid, s, rad, col_scale_alpha(bg, fade));
         text_draw(cv, f, x + (bwid - tw) * 0.5f, base, col_scale_alpha(fg, fade), w->label);
 
+        Col lc; float lf;
+        if (cpu_load_col(i, &lc, &lf)) {
+            /* Top right of the button, big enough to actually see: a dim
+             * whole disc for the outline, then the lit part rising through
+             * it. The pill underneath can be bright orange, so the rim keeps
+             * a dark ring around it. */
+            float r  = clampf(vis * 0.17f, 3.0f * s, 6.0f * s);
+            float mx = x + bwid - r - 3.0f * s, my = y0 + r + 3.0f * s;
+
+            Col ring = cfg.bg; ring.a = 200;
+            fill_moon(cv, mx, my, r + 1.0f * s, 1.0f, col_scale_alpha(ring, fade));
+            Col rim = lc; rim.a = 90;
+            fill_moon(cv, mx, my, r, 1.0f, col_scale_alpha(rim, fade));
+            fill_moon(cv, mx, my, r, 0.2f + 0.8f * lf, col_scale_alpha(lc, fade));
+        }
+
         if (b->hits < MAX_WORKSPACES) {
             b->hit[b->hits].x0 = x / s;
             b->hit[b->hits].x1 = (x + bwid) / s;
@@ -3406,8 +4079,14 @@ static void usage(void)
 "\n"
 "  --config PATH       read this file instead of the default\n"
 "  --dump-config       print a config file with all defaults, then exit\n"
+"  --replace           terminate any swbr already running, then start. Use\n"
+"                      this rather than `pkill swbr` in the sway config: sway\n"
+"                      forks each exec_always without waiting, so a kill on\n"
+"                      one line races the bar started on the next\n"
 "  --probe             print outputs, sizes, font metrics and cell values\n"
 "  --slim              start folded (same as start_collapsed=1)\n"
+"  --toggle            fold or unfold the running bar. Also --fold/--unfold\n"
+"                      bindsym $mod+b exec swbr --toggle\n"
 "  --msg TEXT          send a message to the running bar, then exit\n"
 "                      'warn: ..', 'error: ..', 'info: ..', or 'clear'\n"
 "  --help, --version\n"
@@ -3474,10 +4153,27 @@ static void usage(void)
 "  NAME.sep=1          draw the separator after this cell\n"
 "  NAME.hide_empty=1   no output = the cell and its separator disappear\n"
 "  NAME.empty=TEXT     drawn instead of hiding, so the area stays clickable\n"
+"  NAME.slim_on=TEXT   output containing this counts as the 'on' state for\n"
+"                      slim=media, e.g. the icon your player prints\n"
 "  NAME.gap=           space after this cell; 0 glues it to the next one and\n"
 "                      equal background colours merge into a single box\n"
 "  NAME.pad=0          padding inside the cell, left and right\n"
-"  NAME.slim=auto      folded strip: auto | tick | bar | clock | off\n"
+"  NAME.slim=auto      folded strip: auto | tick | bar | clock | presence\n"
+"                      | media | off. presence = a block in the running\n"
+"                      colour, there only while the program is. media = two\n"
+"                      bars while playing, one dim one when not\n"
+"  NAME.slim_color=    override this cell's colour on the strip. Without it\n"
+"                      the strip reuses the colour the cell's own text has\n"
+"  NAME.source=cmus    swbr runs cmus-remote itself: playing and paused are\n"
+"                      read from it rather than guessed from the text, and\n"
+"                      the command, interval, slim=media and the mouse binds\n"
+"                      are filled in for you\n"
+"  NAME.cmus_fmt=%i %n how it is laid out: %i icon, %n artist — title, %a,\n"
+"                      %t, %s playing|paused. One cell can be just the icon\n"
+"                      and another just the name\n"
+"  NAME.slim_color2=   second colour: the minutes of a clock cell, and its\n"
+"                      hour bars before noon. clock lights every bar up to\n"
+"                      the hour, so 02:00 and 02:55 both show two\n"
 "  NAME.slim_min=0 NAME.slim_max=100   the range a gauge maps\n"
 "  NAME.slim_w=0       width in the folded strip, 0 = the mode's default\n"
 "  NAME.markup=1       parse pango markup in this cell's output\n"
@@ -3506,7 +4202,7 @@ static void usage(void)
 "  signals=1           the folded strip keeps saying something: a fixed slot\n"
 "                      per workspace, twelve dots for a clock cell, a gauge\n"
 "                      for anything with a percentage\n"
-"  slim_ws_slots=10    at most this many workspace slots\n"
+"  slim_ws_slots=10    always show this many workspace slots\n"
 "  start_collapsed=0\n"
 "  SIGUSR1 folds or unfolds every bar:  pkill -USR1 swbr\n"
 "\n"
@@ -3517,7 +4213,10 @@ static void usage(void)
 "colors (#rrggbb or #rrggbbaa)\n"
 "  bg text dim accent hl urgent outline\n"
 "  ws_bg ws_fg ws_focused_bg ws_focused_fg ws_visible_bg ws_visible_fg\n"
-"  ws_urgent_bg ws_urgent_fg mode_bg mode_fg\n", stdout);
+"  ws_urgent_bg ws_urgent_fg mode_bg mode_fg\n"
+"  running             the folded strip's 'this program is up' colour\n"
+"  slim_warm           where a gauge's direction pips start before they ramp\n"
+"                      into running (charging) or urgent (draining)\n", stdout);
 }
 
 static void print_color(const char *k, Col c)
@@ -3576,6 +4275,8 @@ static void dump_config(void)
     print_color("accent", c.accent);
     print_color("hl", c.hl);
     print_color("urgent", c.urgent);
+    print_color("running", c.running);
+    print_color("slim_warm", c.slim_warm);
     print_color("outline", c.outline);
     print_color("ws_bg", c.ws_bg);
     print_color("ws_fg", c.ws_fg);
@@ -3592,6 +4293,7 @@ static void dump_config(void)
 /* ------------------------------------------------------------------ main */
 
 static int  probe = 0;
+static int  do_replace = 0;
 static int  cfg_start_collapsed_arg = 0;
 
 static void probe_report(void)
@@ -3631,10 +4333,64 @@ static void probe_report(void)
                f->px, (double)f->ascent, (double)f->lineh,
                (double)baseline_for(f, 0, (float)b->bh, (float)b->scale), b->bh);
     }
+    if (cfg.ws_cpu) {
+        printf("cpu per workspace, sampled every %ds (min %g%%, full %g%%)\n",
+               cfg.ws_cpu_interval, (double)cfg.ws_cpu_min, (double)cfg.ws_cpu_full);
+        printf("            %d window(s) in sway's tree to attribute work to\n",
+               cpu_nwin);
+        for (int i = 0; i < cpu_nwin && i < 8; ++i)
+            printf("            pid %-7d -> %s\n", cpu_win[i].pid,
+                   cpu_win[i].ws >= 0 && cpu_win[i].ws < ws_count
+                       ? ws_list[cpu_win[i].ws].name : "(no workspace)");
+
+        printf("            busiest processes in that sample:\n");
+        for (int i = 0; i < cpu_top_n; ++i) {
+            char path[64], comm[64] = "?";
+            snprintf(path, sizeof(path), "/proc/%d/comm", cpu_top[i].pid);
+            FILE *cf = fopen(path, "r");
+            if (cf) {
+                if (fgets(comm, sizeof(comm), cf)) {
+                    char *nl = strchr(comm, '\n');
+                    if (nl) *nl = 0;
+                }
+                fclose(cf);
+            }
+            printf("            %-16s pid %-7d -> %s\n", comm, cpu_top[i].pid,
+                   cpu_top[i].ws >= 0 && cpu_top[i].ws < ws_count
+                       ? ws_list[cpu_top[i].ws].name : "no workspace (not under a window)");
+        }
+        if (!cpu_ws_n) printf("            no sample yet\n");
+        for (int i = 0; i < cpu_ws_n; ++i)
+            printf("            %-12s %5.1f%%\n", cpu_ws_name[i], cpu_ws_pct[i]);
+    }
+    static const char *SLIM_NAME[] = { "auto", "tick", "bar", "clock",
+                                       "presence", "media", "off" };
     for (int i = 0; i < cfg.cell_count; ++i) {
         Cell *e = &cfg.cell[i];
         printf("cell %-10s iv=%-3d min_w=%-4d sep=%d  '%s'\n",
                e->name, e->interval, e->min_w, e->sep, e->out);
+
+        int mode = slim_mode(e);
+        printf("            slim=%s", SLIM_NAME[mode]);
+        if (e->slim == SLIM_AUTO && mode != SLIM_AUTO) printf(" (auto)");
+        if (mode == SLIM_MEDIA) {
+            int st = media_state(e);
+            printf("  playing=%s", st > 0 ? "yes" : st == 0 ? "no" : "(no output yet)");
+            if (e->source == SRC_CMUS)
+                printf("  from cmus-remote (%s)",
+                       e->state == PS_PLAYING ? "playing" :
+                       e->state == PS_PAUSED  ? "paused"  :
+                       e->state == PS_STOPPED ? "stopped" : "not running");
+            else if (*e->slim_on)
+                printf("  slim_on='%s'%s", e->slim_on,
+                       strstr(e->out, e->slim_on) ? " found" : " NOT in the output");
+            else printf("  slim_on unset, guessing from the text");
+        } else if (mode == SLIM_BAR) {
+            float v = 0;
+            printf("  value=%s  range %g..%g", cell_number(e->out, &v) ? "yes" : "no",
+                   (double)e->slim_min, (double)e->slim_max);
+        }
+        printf("\n");
     }
     printf("folded strip (%d px), right side left to right:\n", cfg.collapsed_px);
     for (int i = 0; i < layout_n[G_RIGHT]; ++i) {
@@ -3647,9 +4403,18 @@ static void probe_report(void)
         float v = 0;
         bool have = cell_number(e->out, &v);
         const char *mn = m == SLIM_BAR ? "gauge" : m == SLIM_CLOCK ? "clock"
-                       : m == SLIM_OFF ? "off" : "tick";
+                       : m == SLIM_OFF ? "off"
+                       : m == SLIM_PRESENCE ? "up"
+                       : m == SLIM_MEDIA ? "media" : "tick";
         printf("  %-10s %-5s %3.0fpx %02x%02x%02x", e->name, mn,
                (double)slim_width_of(e, m, 1.0f), c.r, c.g, c.b);
+        if (m == SLIM_MEDIA) {
+            bool on = *e->slim_on ? strstr(e->out, e->slim_on) != NULL
+                                  : (strstr(e->out, "\xe2\x9d\x9a") != NULL ||
+                                     strcasestr(e->out, "playing") != NULL);
+            printf("  %s (slim_on='%s')", on ? "PLAYING: two bars" : "not playing: one block",
+                   *e->slim_on ? e->slim_on : "(guessing from the icon)");
+        }
         if (m == SLIM_BAR && have)
             printf("  %.1f of %g..%g = %.0f%% full",
                    (double)v, (double)e->slim_min, (double)e->slim_max,
@@ -3692,24 +4457,32 @@ int main(int argc, char **argv)
             printf("SwBr %s (build %s)\n", SWBR_VERSION, SWBR_BUILD); return 0;
         } else if (!strcmp(argv[i], "--dump-config")) { dump_config(); return 0; }
         else if (!strcmp(argv[i], "--probe")) probe = 1;
+        else if (!strcmp(argv[i], "--replace")) do_replace = 1;
         else if (!strcmp(argv[i], "--slim") || !strcmp(argv[i], "--collapsed"))
             cfg_start_collapsed_arg = 1;
     }
 
+    if (do_replace) replace_running();   /* clear the way before asking for a surface */
+
     config_defaults(&cfg);
-    config_load(&cfg, cfgpath);
+    sw_shared_apply("swbr", config_set_shared, &cfg);   /* shared first */
+    config_load(&cfg, cfgpath);                         /* our own wins  */
 
-    for (int i = 1; i < argc; ++i)                  /* talk to a running bar */
+    for (int i = 1; i < argc; ++i) {               /* talk to a running bar */
         if (!strcmp(argv[i], "--msg") || !strcmp(argv[i], "-m")) {
             if (i + 1 >= argc) die("--msg needs a text ('warn: ..', 'clear')");
             return msg_send(argv[i + 1]);
         }
+        if (!strcmp(argv[i], "--toggle")) return msg_send(":toggle");
+        if (!strcmp(argv[i], "--fold")) return msg_send(":fold");
+        if (!strcmp(argv[i], "--unfold")) return msg_send(":unfold");
+    }
 
     for (int i = 1; i < argc; ++i) {                 /* command line wins */
         const char *a = argv[i];
         if (!strcmp(a, "--config")) { i++; continue; }
         if (!strcmp(a, "--probe") || !strcmp(a, "--slim") ||
-            !strcmp(a, "--collapsed")) continue;
+            !strcmp(a, "--replace") || !strcmp(a, "--collapsed")) continue;
         if (!strcmp(a, "-s") && i + 1 < argc) a = argv[++i];
         else if (!strncmp(a, "--", 2)) a += 2;
         char buf[4096];
@@ -3753,10 +4526,12 @@ int main(int argc, char **argv)
     msg_open();
 
     if (probe) {
+        if (cfg.ws_cpu) cpu_sample();           /* the first of the two */
         for (int i = 0; i < 60; ++i) {          /* give the cells a moment */
             cells_read();
             usleep(20000);
         }
+        if (cfg.ws_cpu) cpu_sample();           /* and the one that measures */
         wl_display_roundtrip(dpy);
         probe_report();
         cells_stop();
@@ -3782,8 +4557,26 @@ int main(int argc, char **argv)
         cells_tick();
         if (cells_read()) redraw = true;
         if (msg_read()) redraw = true;
+        if (ctrl_req) {
+            for (int i = 0; i < output_count; ++i) {
+                Bar *b = outputs[i].bar;
+                if (!b) continue;
+                bar_set_collapsed(b, ctrl_req == 1 ? !b->collapsed : ctrl_req == 2);
+            }
+            ctrl_req = 0;
+            redraw = true;
+        }
         if (msg_expiry_due()) redraw = true;
 
+        if (cfg.ws_cpu && cfg.ws_cpu_interval > 0) {
+            static uint32_t cpu_next;
+            if (!cpu_next || (int32_t)(now_ms() - cpu_next) >= 0) {
+                cpu_sample();
+                cpu_next = now_ms() + (uint32_t)cfg.ws_cpu_interval * 1000u;
+                redraw = true;
+            }
+        }
+
         if (status_fd < 0 && *cfg.status_command && status_next_spawn &&
             (int32_t)(now_ms() - status_next_spawn) >= 0) {
             status_next_spawn = 0;
@@ -3821,7 +4614,8 @@ int main(int argc, char **argv)
 
         int timeout = -1;
         if (animating || scroll_running) timeout = 16;
-        else if (cfg.cell_count || msg_until) timeout = 100;
+        else if (cfg.cell_count || msg_until || (cfg.ws_cpu && cfg.ws_cpu_interval > 0))
+            timeout = 100;
         else if (status_fd < 0 && *cfg.status_command) timeout = 100;
         else {
             bool any_dirty = false;
diff --git a/swbr_config.example b/swbr_config.example
index 2ed7013..f8b1f48 100644
--- a/swbr_config.example
+++ b/swbr_config.example
@@ -80,7 +80,13 @@ scroll_pause=900   # ms held at each end before scrolling
 #   NAME.gap=          space after this cell, overrides cell_gap. 0 glues it
 #                      to the next one: equal bg colours merge into one box
 #   NAME.pad=0         padding inside the cell, left and right
-#   NAME.slim=auto     folded strip: auto | tick | bar | clock | off
+#   NAME.slim=auto     folded strip: auto | tick | bar | clock | presence |
+#                      off. presence = a plain block in the running colour,
+#                      present only while the program is
+#   NAME.slim_color=   override this cell's colour on the strip. Without it
+#                      the strip reuses the colour the cell's own text has,
+#                      so a cyan clock stays cyan when folded
+#   NAME.slim_color2=  second colour: the minutes of a clock cell
 #   NAME.slim_min=0    the value range a gauge maps, so a temperature or a
 #   NAME.slim_max=100  load average can be a gauge as well as a percent
 #   NAME.markup=1      the output may use <span foreground="#rrggbb">..</span>
@@ -132,17 +138,22 @@ cmus.button4=cmus_control prev
 cmus.button5=cmus_control next
 
 cell=cmusui
-cmusui.cmd=pgrep -x cmus >/dev/null || exit 0; case "$(cmus_control status 2>/dev/null)" in playing) printf '❚❚';; paused) printf '▶';; stopped) printf '■';; *) printf '▶';; esac
+cmusui.cmd=pgrep -x cmus >/dev/null || exit 0; case "$(cmus_control status 2>/dev/null)" in playing) printf '<span foreground="#2fbf71">❚❚</span>';; paused) printf '<span foreground="#e8b81a">▶</span>';; *) printf '<span foreground="#5a6b7a">■</span>';; esac
 cmusui.interval=2
 cmusui.empty=♪     # cmus not running: still a button, click it to start
 cmusui.min_w=38    # never shrinks, never truncates
 cmusui.align=center
 cmusui.pad=4
 cmusui.bg=ffffff14
-cmusui.color=e8b81a    # a touch brighter than cb9b00: small glyph on a dark
-                       # panel reads darker than a big filled workspace block
-cmusui.slim=tick   # stays on the folded strip as a button you can click.
-                   # If yours says slim=off you get no music block there.
+cmusui.color=e8b81a    # fallback; the command colours itself per state, and
+                       # the folded strip picks that colour up, so a green
+                       # block means playing and a grey one means stopped
+cmusui.slim=media  # folded: TWO bars while playing, one dim block when not.
+                   # A shape, not another coloured block among gauges.
+cmusui.slim_on=❚   # THIS is what tells it "playing": the output must contain
+                   # this text. It matches the ❚❚ printed above. If you change
+                   # the command to print your own icon, change this too, or
+                   # the strip will always show the idle block.
 cmusui.slim_w=26   # wider than the other ticks, so it is easy to find and hit
 cmusui.button1=pgrep -x cmus >/dev/null && cmus_control toggle || foot -e cmus
 cmusui.button2=cmus_control toggle
@@ -155,6 +166,7 @@ volume.cmd=alsa_control volume get
 volume.interval=2
 volume.button2=alsa_control mute toggle
 volume.slim=bar
+volume.slim_color=a9d8f5   # light baby blue, its own thing on the strip
 
 # --- docker and mpv share one group: docker.sep=0 joins them ---------------
 cell=docker
@@ -162,16 +174,34 @@ docker.cmd=pgrep -x dockerd >/dev/null || exit 0; docker ps --format '{{.Image}}
 docker.interval=10
 docker.color=19cb00
 docker.sep=0
+docker.slim=presence   # folded: a block in the running colour, there only
+                       # while dockerd is, so a glance says it is up
 
 cell=mpv
 mpv.cmd=pgrep -x mpv >/dev/null || exit 0; ws=$(swaymsg -t get_workspaces | jq -r '.[] | select(.representation != null) | select(.representation | test("mpv")) | .name' | paste -sd'|'); [ -n "$ws" ] && printf '<span foreground="#19cb00">mpv </span><span foreground="#3E98E7">[%s]</span>' "$ws" || printf '<span foreground="#cecb00">mpv</span>'
 mpv.interval=3
+mpv.slim=presence      # same for mpv: started something in the background?
+mpv.slim_color=3ea6ff  # its own colour, so you can tell the two apart
+
+# --- cmus, without a script ------------------------------------------------
+# swbr runs cmus-remote itself, so playing and paused are facts. It fills in
+# the command, the interval, slim=media and the mouse binds; override any of
+# them below if you want something else.
+# cell=music
+# music.source=cmus
+# music.cmus_fmt=%i %n   # %i icon, %n artist — title, %a, %t, %s playing|paused
+#                        # two cells work too: one with %i, one with %n
 
 # --- clock -----------------------------------------------------------------
 cell=clock
 clock.cmd=date '+<span foreground="#00ffff">%H:%M</span>  %a %d.%m'
 clock.interval=5
-clock.slim=clock   # twelve dots: hours lit, the current one grows with minutes
+clock.slim=clock   # twelve dots for the hour, in the same cyan the clock
+                   # uses in the full bar (taken from its own markup)
+clock.slim_color2=2f7f9f   # the minutes bar, and the morning hours. Every dot
+                   # up to the hour is lit, so you count bars: 2 at 02:00 and
+                   # still 2 at 02:55. Main colour after noon, this one
+                   # before, so 13:00 and 1:00 differ
 
 # --- cpu load and temperature, joined --------------------------------------
 cell=cpu
@@ -199,11 +229,14 @@ cell=power
 power.cmd=b=/sys/class/power_supply/BAT0; grep -q Discharging $b/status || exit 0; awk -v e="$(cat $b/energy_now)" -v p="$(cat $b/power_now)" 'BEGIN{if(p>0)printf "%.2fW %.1fh", p/1000000, e/p}'
 power.interval=10
 power.color=b3c0cd
+power.slim=off     # watts and hours say nothing as a bar
 power.sep=0
 
+# The marker at the end tells the folded strip which way it is going:
+# + charging, - discharging, = full. In the full bar it reads as ⚡ or nothing.
 cell=battery
-battery.cmd=cat /sys/class/power_supply/BAT0/capacity
-battery.fmt=%s%
+battery.cmd=b=/sys/class/power_supply/BAT0; c=$(cat $b/capacity); case "$(cat $b/status)" in Charging) printf '%s%%+' "$c";; Full) printf '%s%%=' "$c";; *) printf '%s%%-' "$c";; esac
+battery.markup=1
 battery.interval=10
 battery.color=e1eeff
 battery.warn=<30:cecb00
@@ -224,7 +257,9 @@ signals=1          # the folded strip still says something: every workspace
                    # with a percentage become little gauges
 slim_ws_slots=10   # at most this many workspace slots
 start_collapsed=0  # 1 = start folded. Command line: swbr --slim
-# folds or unfolds every bar without the mouse:  pkill -USR1 swbr
+# Without the mouse. Bind either one to a key in your sway config:
+#   bindsym $mod+b exec swbr --toggle      (also --fold / --unfold)
+#   bindsym $mod+b exec pkill -USR1 swbr
 
 # --- mouse: anywhere on the bar, folded or not ------------------------------
 # Cell bindings win where a cell is; these catch everywhere else, which is the
@@ -253,6 +288,17 @@ dim=5a6b7aff
 accent=89afc4ff     # visible-but-not-focused workspace tick
 hl=cb9b00ff         # focused workspace tick in the folded strip
 urgent=e0533cff
+ws_cpu=1           # a dot on each workspace button showing how much
+                   # processor time the windows on it are using. Folded, the
+                   # workspace slot fills from the bottom instead
+ws_cpu_interval=3  # seconds between samples
+ws_cpu_min=4       # below this much load nothing is drawn
+ws_cpu_full=60     # this much load paints it fully hot
+
+running=3ddc84ff   # folded strip: this program is up, and where a charging
+                   # gauge's pips ramp to
+slim_warm=e8963cff # where the direction pips start: orange into red while
+                   # draining, orange into green while charging
 ws_bg=00000000      # inactive button: nothing, the bar shows through
 ws_fg=b3c0cdff
 ws_focused_bg=cb9b00ff   # the appwheel orange