Nimbin[12]?Sway / SwBr / commits / adc4476

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

Bigger slim mode bars

Christian Immanuel · 2026-08-30 01:08 · adc4476c78da4bc76893c6112596624a8a82d8c1

 config.example      |   9 ++-
 screenshot-slim.png | Bin 5833 -> 12652 bytes
 screenshot.png      | Bin 26473 -> 27876 bytes
 swbr.c              | 190 +++++++++++++++++++++++++++++++++++++++++++++++-----
 4 files changed, 183 insertions(+), 16 deletions(-)

diff --git a/config.example b/config.example
index c941391..45cdd7e 100644
--- a/config.example
+++ b/config.example
@@ -176,7 +176,10 @@ 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.source=window  # asks sway which workspaces have an mpv window, no jq and
+                   # no swaymsg. %n the name, %w the workspaces, %c how many
+mpv.program=mpv    # matched against app_id, the X11 class, then the title
+mpv.src_fmt=<span foreground="#19cb00">%n </span><span foreground="#3E98E7">[%w]</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
@@ -249,6 +252,10 @@ hover_keys=1       # grab the keyboard while the pointer is over the bar.
                    # That is what makes hide_key work without clicking first.
 collapsed_px=5
 anim_ms=120
+slim_align=1       # folded: each mark takes its cell's whole place, same
+                   # left edge and same width, so toggling changes the height
+                   # of the bar and nothing else.
+                   # 0 = pack them against the right edge instead
 signals=1          # the folded strip still says something: every workspace
                    # keeps a fixed slot on the left (slot 3 is always
                    # workspace 3), the clock becomes twelve dots, and cells
diff --git a/screenshot-slim.png b/screenshot-slim.png
index 1fab7e4..1006efa 100644
Binary files a/screenshot-slim.png and b/screenshot-slim.png differ
diff --git a/screenshot.png b/screenshot.png
index cce4b75..27aea6c 100644
Binary files a/screenshot.png and b/screenshot.png differ
diff --git a/swbr.c b/swbr.c
index 0e14216..04af404 100644
--- a/swbr.c
+++ b/swbr.c
@@ -1058,7 +1058,7 @@ static void cpu_sample(void)
  * `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, SRC_BATTERY };
+enum { SRC_CMD, SRC_CMUS, SRC_BATTERY, SRC_WINDOW };
 enum { PS_UNKNOWN = -1, PS_STOPPED = 0, PS_PAUSED = 1, PS_PLAYING = 2 };
 typedef struct {
     char  name[32];
@@ -1148,6 +1148,8 @@ typedef struct {
     /* colors */
     Col bg, text, dim, accent, hl, urgent, outline, running, slim_warm;
     int ws_other;                /* show other monitors' workspaces too  */
+    int slim_align;              /* folded marks sit under their full-bar
+                                    positions, rather than packed right   */
     int ws_cpu;                  /* the load dot on a workspace button   */
     int ws_cpu_interval;         /* seconds between samples              */
     float ws_cpu_idle;           /* under this, nothing is happening at all */
@@ -1223,6 +1225,7 @@ static void config_defaults(Config *c)
     c->ws_bg         = (Col){ 0x1e, 0x27, 0x33, 0x00 };
     c->ws_fg         = (Col){ 0xb3, 0xc0, 0xcd, 0xff };
     c->ws_other        = 0;
+    c->slim_align      = 1;
     c->ws_cpu          = 1;
     c->ws_cpu_interval = 3;
     c->ws_cpu_idle     = 0.01f;   /* one percent of one core */
@@ -1295,7 +1298,8 @@ static void cell_set(Config *c, const char *name, const char *k, const char *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, "src_path") || !strcmp(k, "bat_path"))
+    else if (!strcmp(k, "src_path") || !strcmp(k, "bat_path") ||
+             !strcmp(k, "program"))
         str_set(e->src_path, sizeof(e->src_path), v);
     else if (!strcmp(k, "hover")) str_set(e->hover_fmt, sizeof(e->hover_fmt), v);
     else if (!strcmp(k, "hover_slim")) e->hover_slim = atoi(v);
@@ -1312,10 +1316,18 @@ static void cell_set(Config *c, const char *name, const char *k, const char *v)
             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 if (!strcasecmp(v, "window") || !strcasecmp(v, "program")) {
+            e->source = SRC_WINDOW;
+            e->cmd[0] = 0;                       /* it asks sway, not a shell */
+            if (!e->interval) e->interval = 3;
+            if (e->slim == SLIM_AUTO) e->slim = SLIM_PRESENCE;
+            if (!*e->src_path) str_set(e->src_path, sizeof(e->src_path), e->name);
         } else if (!strcasecmp(v, "battery") || !strcasecmp(v, "bat")) {
             e->source = SRC_BATTERY;
-            /* it reads /sys itself; the command only paces it */
-            str_set(e->cmd, sizeof(e->cmd), "true");
+            /* no command: spawning /bin/true every twenty seconds just to
+             * pace a pair of file reads is silly. cells_tick calls the
+             * reader straight. */
+            e->cmd[0] = 0;
             if (!e->interval) e->interval = 20;
             if (e->slim == SLIM_AUTO) e->slim = SLIM_BAR;
             if (e->slim_max <= e->slim_min) { e->slim_min = 0; e->slim_max = 100; }
@@ -1453,6 +1465,7 @@ static void config_set(Config *c, const char *k, const char *v)
     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_other")) c->ws_other = atoi(v);
+    else if (!strcmp(k, "slim_align")) c->slim_align = atoi(v);
     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_idle")) c->ws_cpu_idle = (float)atof(v);
@@ -2521,6 +2534,77 @@ static void cmus_parse(Cell *e)
     if (t != e->out) memmove(e->out, t, strlen(t) + 1);
 }
 
+/* a leaf with a pid is a window; containers and workspaces are not */
+static bool node_is_view(const JV *n)
+{
+    if (!n || n->type != J_OBJ) return false;
+    if (jint(n, "pid", -1) <= 0) return false;
+    const JV *kids = jget(n, "nodes");
+    if (kids && kids->type == J_ARR && kids->count > 0) return false;
+    return true;
+}
+
+/* Which workspaces a program has a window on. sway knows; this is the same
+ * question the old shell one-liner asked of swaymsg and jq, without either. */
+static void window_scan(const JV *node, const char *want, const char *ws,
+                        char *out, size_t cap, int *found)
+{
+    if (!node || node->type != J_OBJ) return;
+
+    const char *type = jstr(node, "type", "");
+    if (!strcmp(type, "workspace")) ws = jstr(node, "name", "");
+
+    if (node_is_view(node)) {
+        const char *id  = jstr(node, "app_id", "");
+        const char *cls = jstr(jget(node, "window_properties"), "class", "");
+        const char *nm  = jstr(node, "name", "");
+        if (strcasestr(id, want) || strcasestr(cls, want) || strcasestr(nm, want)) {
+            if (ws && *ws) {
+                char one[80];
+                snprintf(one, sizeof(one), "%s%s", *out ? "|" : "", ws);
+                if (strlen(out) + strlen(one) < cap - 1 && !strstr(out, ws))
+                    strcat(out, one);
+            }
+            (*found)++;
+        }
+    }
+    for (int i = 0; i < node->count; ++i) {
+        const JV *kid = node->items[i];
+        if (!kid) continue;
+        if (kid->type == J_ARR)
+            for (int j = 0; j < kid->count; ++j)
+                window_scan(kid->items[j], want, ws, out, cap, found);
+        else if (kid->type == J_OBJ)
+            window_scan(kid, want, ws, out, cap, found);
+    }
+}
+
+static void window_read(Cell *e)
+{
+    e->out[0] = 0;
+    e->state = PS_STOPPED;
+    if (!*e->src_path) return;                   /* nothing to look for */
+
+    JV *tree = sway_query(IPC_GET_TREE);
+    if (!tree) return;
+
+    char list[256] = "";
+    int  found = 0;
+    window_scan(tree, e->src_path, NULL, list, sizeof(list), &found);
+    jfree(tree);
+
+    if (!found) return;                          /* not running: no cell */
+    e->state = PS_PLAYING;
+
+    char cnt[16];
+    snprintf(cnt, sizeof(cnt), "%d", found);
+    const char *vals[] = { e->name, list, cnt };
+    expand_fmt(e->out, sizeof(e->out), *e->src_fmt ? e->src_fmt : "%n [%w]",
+               "nwc", vals);
+    if (*e->hover_fmt)
+        expand_fmt(e->out_hover, sizeof(e->out_hover), e->hover_fmt, "nwc", vals);
+}
+
 /* capacity and status under /sys/class/power_supply: "83+" while it is
  * filling, "83-" while it is not. The sign says the direction, the number says the
  * charge, and neither needs a script. */
@@ -2617,6 +2701,7 @@ static void cell_finish(Cell *e)
 
     if (e->source == SRC_CMUS)    { cmus_parse(e);  e->len = 0; return; }
     if (e->source == SRC_BATTERY) { battery_read(e); e->len = 0; return; }
+    if (e->source == SRC_WINDOW)  { window_read(e);  e->len = 0; return; }
 
     char *t = trim(e->buf);
     for (char *q = t; *q; ++q) if (*q == '\n' || *q == '\r' || *q == '\t') *q = ' ';
@@ -2669,14 +2754,25 @@ static bool cells_read(void)
     return got;
 }
 
+static bool cells_dirty;         /* something changed without a process */
+
 static void cells_tick(void)
 {
     uint32_t t = now_ms();
     for (int i = 0; i < cfg.cell_count; ++i) {
         Cell *e = &cfg.cell[i];
-        if (e->pid > 0 || !*e->cmd) continue;
+        if (e->pid > 0) continue;
         if (e->interval == 0 && e->next_run) continue;       /* run once */
         if (e->next_run && (int32_t)(t - e->next_run) < 0) continue;
+
+        if ((e->source == SRC_BATTERY || e->source == SRC_WINDOW) && !*e->cmd) {
+            if (e->source == SRC_WINDOW) window_read(e);   /* no process */
+            else                         battery_read(e);
+            e->next_run = t + (uint32_t)(e->interval > 0 ? e->interval * 1000 : 5000);
+            cells_dirty = true;
+            continue;
+        }
+        if (!*e->cmd) continue;
         e->next_run = t + 1;
         cell_spawn(e);
     }
@@ -2747,7 +2843,7 @@ static bool cell_is_msg(Cell *e)
 static bool cell_visible(Cell *e)
 {
     if (cell_is_msg(e)) return true;
-    if (!*e->cmd && !*e->fmt && !*e->empty) return false;
+    if (!*e->cmd && !*e->fmt && !*e->empty && e->source == SRC_CMD) return false;
     if (*e->out) return true;
     return *e->empty || !e->hide_empty;      /* a placeholder stays clickable */
 }
@@ -2818,6 +2914,10 @@ struct Bar {
     struct { float x0, x1; char name[128]; } hit[MAX_WORKSPACES];
     int hits;
     bool hover_open;            /* opened by a hover, folds back on leave */
+    float cellx0[MAX_CELLS], cellx1[MAX_CELLS];  /* where each cell sat in the
+                                                    full bar, so the folded
+                                                    strip can line up with it */
+    float wsx0[MAX_WORKSPACES], wsx1[MAX_WORKSPACES];   /* and each workspace */
     struct { float x0, x1; int cell; } chit[MAX_CELLS];
     int chits;
 };
@@ -3086,8 +3186,9 @@ static void slim_draw_one(Canvas *cv, Cell *e, int mode, float x, float y,
     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;
+         * "that one is up". It keeps the colour it has in the full bar —
+         * painting it green regardless meant the same cell was two different
+         * colours depending on whether the bar was folded. */
         fill_rect(cv, x, y, w, h, c);
         return;
     }
@@ -3097,6 +3198,11 @@ static void slim_draw_one(Canvas *cv, Cell *e, int mode, float x, float y,
         localtime_r(&t, &tmv);
         int hour = (tmv.tm_hour + 11) % 12 + 1;    /* 1..12, 0:xx reads as 12 */
         float dot = 3.0f * s, gap = 2.0f * s;
+        if (w > 12.0f * (dot + gap)) {             /* fill the width it was given */
+            float pitch = w / 12.0f;
+            dot = pitch * 0.62f;
+            gap = pitch - dot;
+        }
         /* Every bar up to the hour, so you count them instead of finding the
          * one lit position: 2 at 02:00, still 2 at 02:55.
          *
@@ -3273,8 +3379,27 @@ static void draw_signals(Canvas *cv, Bar *b, float y, float h, float s, Runs *rs
     float tick = 14.0f * s;
     b->hits = 0;
     b->chits = 0;
+    /* Lined up, a workspace keeps the place its button has in the full bar,
+     * so folding does not shuffle them either. Numbers with no workspace have
+     * no button to line up with, so they are simply not there. */
+    bool ws_aligned = cfg.slim_align != 0;
+    if (ws_aligned) {
+        bool any = false;
+        for (int i = 0; i < ws_count && i < MAX_WORKSPACES; ++i)
+            if (b->wsx1[i] > b->wsx0[i]) { any = true; break; }
+        ws_aligned = any;
+    }
+
     for (int i = 1; i <= slots; ++i) {
         Ws *w = ws_by_num(b, i);
+        float sx = x, sw = tick;
+        if (ws_aligned) {
+            if (!w) continue;                      /* nothing to sit under */
+            int wi = (int)(w - ws_list);
+            if (wi < 0 || wi >= MAX_WORKSPACES || b->wsx1[wi] <= b->wsx0[wi]) continue;
+            sx = b->wsx0[wi] * s;
+            sw = (b->wsx1[wi] - b->wsx0[wi]) * s - gap * 0.5f;
+        }
         Col c = cfg.dim;
         c.a = 60;                                  /* empty slot: a faint place holder */
         bool mine = w ? ws_on_bar(b, w) : true;
@@ -3291,24 +3416,24 @@ static void draw_signals(Canvas *cv, Bar *b, float y, float h, float s, Runs *rs
                 else                          { c = cfg.dim;    c.a = 110; }
             }
         }
-        fill_rect(cv, x, y, tick, h, c);
+        fill_rect(cv, sx, y, sw, h, c);
         if (w) {
             int wi = (int)(w - ws_list);
             Col lc; float lf; bool faint;
             if (cpu_load_col(wi, &lc, &lf, &faint)) {
                 /* Three or four dots up the slot. Filling it solid hid which
                  * workspace it was, which is the thing the slot is for. */
-                dot_column(cv, x, y, tick, h, 2.0f * s, 1.0f * s,
+                dot_column(cv, sx, y, sw, h, 2.0f * s, 1.0f * s,
                            faint ? 0.001f : lf, 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;
+            b->hit[b->hits].x0 = sx / s;
+            b->hit[b->hits].x1 = (sx + sw) / s;
             str_set(b->hit[b->hits].name, sizeof(b->hit[b->hits].name), w->name);
             b->hits++;
         }
-        x += tick + gap;
+        if (!ws_aligned) x += tick + gap;
     }
 
     if (!cfg.cell_count) {                         /* status_command fallback */
@@ -3342,9 +3467,32 @@ static void draw_signals(Canvas *cv, Bar *b, float y, float h, float s, Runs *rs
     }
     float rx = (float)cv->w - 4.0f * s - total;
     if (rx < x + gap) rx = x + gap;
+
+    /* Folded, a cell can sit under the middle of where it is in the full bar
+     * instead of being packed against the right edge. Toggling then does not
+     * move anything, so the clock is where the clock was and you can hit it
+     * without looking. Falls back to packing until the full bar has been
+     * drawn once, and wherever two marks would overlap. */
+    /* Aligned, a mark takes its cell's whole place — same left edge, same
+       width — so folding changes the height of the bar and nothing else. */
+    float px[MAX_CELLS], pw[MAX_CELLS];
+    bool  aligned = cfg.slim_align != 0;
+    if (aligned) {
+        for (int k = 0; k < n && aligned; ++k) {
+            float c0 = b->cellx0[idx[k]] * s, c1 = b->cellx1[idx[k]] * s;
+            if (c1 <= c0) { aligned = false; break; }        /* never measured */
+            if (c0 < x + gap) c0 = x + gap;
+            if (c1 > (float)cv->w - 3.0f * s) c1 = (float)cv->w - 3.0f * s;
+            if (c1 - c0 < 4.0f * s) { aligned = false; break; }
+            px[k] = c0;
+            pw[k] = c1 - c0 - gap * 0.5f;
+        }
+    }
+
     for (int k = 0; k < n; ++k) {
-        float w = slim_width_of(&cfg.cell[idx[k]], mode[k], s);
-        if (k) rx += join[k] ? 1.0f * s : gap;
+        float w = aligned ? pw[k] : slim_width_of(&cfg.cell[idx[k]], mode[k], s);
+        if (aligned) rx = px[k];
+        else if (k)  rx += join[k] ? 1.0f * s : gap;
         if (rx + w > (float)cv->w - 3.0f * s) break;
         slim_draw_one(cv, &cfg.cell[idx[k]], mode[k], rx, y, w, h, s);
         if (b->chits < MAX_CELLS) {               /* buttons keep working */
@@ -3468,6 +3616,10 @@ static void ws_block_draw(Canvas *cv, Bar *b, Font *f, float x, float y0,
             str_set(b->hit[b->hits].name, sizeof(b->hit[b->hits].name), w->name);
             b->hits++;
         }
+        if (i < MAX_WORKSPACES) {                 /* for the folded strip */
+            b->wsx0[i] = x / s;
+            b->wsx1[i] = (x + bwid) / s;
+        }
         x += bwid + (float)cfg.ws_gap * s;
     }
 }
@@ -3708,6 +3860,10 @@ static float draw_group_(Canvas *cv, Bar *b, Font *f, int g, float y0, float vis
             }
             canvas_clip(cv, ox0, cv->cy0, ox1, cv->cy1);
 
+            if (it->cell >= 0 && it->cell < MAX_CELLS) {   /* for the strip */
+                b->cellx0[it->cell] = x / s;
+                b->cellx1[it->cell] = (x + bw[k]) / s;
+            }
             if (b->chits < MAX_CELLS) {
                 b->chit[b->chits].x0 = x / s;
                 b->chit[b->chits].x1 = (x + bw[k]) / s;
@@ -4453,6 +4609,9 @@ static void usage(void)
 "                      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=window  asks sway which workspaces have a window of\n"
+"                      NAME.program= and prints them: mpv [3|5]. %n the\n"
+"                      name, %w the workspaces, %c the count\n"
 "  NAME.source=battery reads capacity and status out of /sys itself and\n"
 "                      prints 83+ while it is filling, 83- while it is not,\n"
 "                      83= when it is full. NAME.bat_path= points it at a\n"
@@ -4887,6 +5046,7 @@ int main(int argc, char **argv)
         if (status_read()) redraw = true;
         cells_tick();
         if (cells_read()) redraw = true;
+        if (cells_dirty) { cells_dirty = false; redraw = true; }
         if (msg_read()) redraw = true;
         if (ctrl_req) {
             for (int i = 0; i < output_count; ++i) {