SwBr git · main
SwBr - status bar for sway
C 95.9% Markdown 3.7%git clone https://git.christianimmanuel.de/sway/SwBr.gitwget https://git.christianimmanuel.de/sway/SwBr/archive/SwBr.tar.gzMore fancy slim version And display worspaces from other displays
README.md | 59 +++++++- swbr.c | 389 ++++++++++++++++++++++++++++++++++++++++++---------- swbr_config.example | 43 +++--- 3 files changed, 394 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 179962c..01b498e 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ something. | --- | --- | | `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 | +| `clock` | twelve bars, lit up to the hour — two at 02:00, still two at 02:55, softer before noon | | `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 | @@ -130,13 +130,60 @@ cell music iv=2 min_w=0 sep=1 '♪ Boards of Canada — Roygbiv' `signals=0` turns the whole thing off and leaves a plain strip. +## Battery + +``` +cell=battery +battery.source=battery +``` + +Reads capacity and status out of `/sys/class/power_supply` itself: `83+` while +it is filling, `83-` while it is draining, and no sign at all when it is full, +since nothing is happening. The sign +carries the direction, so there is no percent sign spending width. `slim=bar` +and the 0–100 range are set for you. + +Not every machine calls it `BAT0`, so it takes the first thing with a +`capacity` file, preferring one named `BAT*`. Point it elsewhere with +`battery.bat_path=/sys/class/power_supply/BAT1` — which is also how you pick +one of two — and lay it out with `battery.src_fmt=`: `%c` capacity, `%i` the +sign, `%w` what it is drawing right now in watts, `%h` how long that leaves, +`%s` the word. `battery.src_fmt=%c%i %w %h` gives `83- 21.5W 1.7h`. + +The watts and the hours come from `energy_now`/`power_now` where the kernel +reports those, and from `charge_now`/`current_now`/`voltage_now` where it does +not; whichever pair exists gives both numbers, and they are simply left empty +when neither does. + +None of this takes anything away. Setting `battery.cmd=` switches the cell +back to your own script, as with any other cell; `source=` and `cmd=` are the +two ways of filling a cell, and whichever comes last in the config wins. + +## Other monitors + +`ws_other=1` adds the workspaces from your other screens to the bar, drawn as +short pills sitting on the bar's bottom edge at a little over half height, in +type a fifth smaller and a little see-through. It is on in the shipped config. + +Folded, they take a slot like any other workspace but never this screen's +emphasis — focused on the other monitor is not focused here, so the brightest +they ever get is a soft accent. They stay +readable and clickable, and nothing about them looks like it belongs to this +screen. + ## Load per workspace -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. +A column of dots up the right edge of each workspace button says how much +processor time the windows on that workspace are using: one lit when it is +idling, all of them when something is working. Dots rather than a solid fill, +so the button's own colour still reads through the gaps — six of them on a +30 px bar, three in the folded strip, where the same dots run up the workspace +slot. + +They keep one colour — `accent`, held back towards `dim` — and only tip +towards `urgent` when a workspace is really pinned, so it is the count and not +the hue that carries the load. `hl` is what paints the focused workspace, so +nothing here goes near it. 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 diff --git a/swbr.c b/swbr.c index de0d7b3..47e61f7 100644 --- a/swbr.c +++ b/swbr.c @@ -840,18 +840,50 @@ static bool proc_stat_of(int pid, int *ppid, return true; } -static int cpu_ws_of_pid(int pid) +/* The walk used to stop after twelve hops and re-read /proc at every one. A + * real build is deeper than that — cc1 < gcc < make < make < make < bash < + * bash < helper < helper < bash < python3 < terminal is twelve on its own — + * so the compiler was declared homeless while the make above it was found, + * and the workspace flickered between the truth and nothing. The sample we + * have already read holds every pid and ppid, so the walk is done in memory, + * as deep as it needs, and each answer is remembered. */ +static int *cpu_ws_memo; /* per index in the sample */ + +static int cpu_sample_index(const ProcSample *v, int n, int pid) +{ + int lo = 0, hi = n - 1; + while (lo <= hi) { + int mid = (lo + hi) / 2; + if (v[mid].pid == pid) return mid; + if (v[mid].pid < pid) lo = mid + 1; + else hi = mid - 1; + } + return -1; +} + +static int cpu_ws_of_index(const ProcSample *v, int n, int idx) { - for (int hop = 0; hop < 12 && pid > 1; ++hop) { + int chain[64], depth = 0; + int ws = -1; + + while (idx >= 0 && depth < (int)(sizeof chain / sizeof *chain)) { + if (cpu_ws_memo[idx] != -2) { ws = cpu_ws_memo[idx]; break; } + chain[depth++] = idx; + + int pid = v[idx].pid; + bool found = false; 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; + if (cpu_win[i].pid == pid) { ws = cpu_win[i].ws; found = true; break; } + if (found) break; + + if (v[idx].ppid <= 1) break; + idx = cpu_sample_index(v, n, v[idx].ppid); } - return -1; + for (int i = 0; i < depth; ++i) cpu_ws_memo[chain[i]] = ws; + return ws; } + /* every view in sway's tree, as pid -> the workspace it sits on */ static void cpu_collect_windows(const JV *node, int ws_idx) { @@ -929,6 +961,14 @@ static void cpu_sample(void) } closedir(d); + for (int i = 1; i < n; ++i) { /* insertion sort: /proc is + nearly ordered already */ + ProcSample tmp = now[i]; + int j = i - 1; + while (j >= 0 && now[j].pid > tmp.pid) { now[j + 1] = now[j]; --j; } + now[j + 1] = tmp; + } + double at = now_ms() / 1000.0; double dt = at - cpu_last_at; @@ -940,10 +980,14 @@ static void cpu_sample(void) double used[MAX_WORKSPACES] = { 0 }; cpu_top_n = 0; + + free(cpu_ws_memo); + cpu_ws_memo = (int *)xmalloc((size_t)n * sizeof *cpu_ws_memo); + for (int i = 0; i < n; ++i) cpu_ws_memo[i] = -2; + 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; } + int pj = cpu_sample_index(cpu_prev, cpu_prev_n, now[i].pid); + const ProcSample *was = pj >= 0 ? &cpu_prev[pj] : NULL; unsigned long long total = now[i].own + now[i].chld, before = 0; if (was && was->own + was->chld <= total) before = was->own + was->chld; @@ -951,7 +995,7 @@ static void cpu_sample(void) time belongs to this window */ unsigned long long delta = total - before; - int ws = cpu_ws_of_pid(now[i].pid); + int ws = cpu_ws_of_index(now, n, i); now[i].ws = ws; if (!delta) continue; if (ws >= 0 && ws < MAX_WORKSPACES) @@ -977,10 +1021,7 @@ static void cpu_sample(void) * 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; + if (cpu_sample_index(now, n, cpu_prev[j].pid) >= 0) 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; @@ -1013,7 +1054,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 }; +enum { SRC_CMD, SRC_CMUS, SRC_BATTERY }; enum { PS_UNKNOWN = -1, PS_STOPPED = 0, PS_PAUSED = 1, PS_PLAYING = 2 }; typedef struct { char name[32]; @@ -1024,6 +1065,7 @@ typedef struct { 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 */ + char src_path[256]; /* where to read it from, when that varies */ int state; /* what that player is doing: PS_* */ int gap; /* -1 = cell_gap */ int pad; /* inner padding, left and right */ @@ -1098,6 +1140,7 @@ typedef struct { /* colors */ Col bg, text, dim, accent, hl, urgent, outline, running, slim_warm; + int ws_other; /* show other monitors' workspaces too */ 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 @@ -1172,6 +1215,7 @@ static void config_defaults(Config *c) 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_other = 0; c->ws_cpu = 1; c->ws_cpu_interval = 3; c->ws_cpu_min = 4.0f; @@ -1243,6 +1287,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")) + str_set(e->src_path, sizeof(e->src_path), v); else if (!strcmp(k, "source")) { if (!strcasecmp(v, "cmus")) { e->source = SRC_CMUS; @@ -1256,6 +1302,13 @@ 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, "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"); + 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; } } else { e->source = SRC_CMD; } @@ -1389,6 +1442,7 @@ static void config_set(Config *c, const char *k, const char *v) 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_other")) c->ws_other = 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_min")) c->ws_cpu_min = (float)atof(v); @@ -2448,11 +2502,120 @@ static void cmus_parse(Cell *e) if (t != e->out) memmove(e->out, t, strlen(t) + 1); } +/* 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. */ +/* one number out of a sysfs file, or -1 */ +static double bat_num(const char *base, const char *leaf) +{ + char path[420], buf[64] = ""; + snprintf(path, sizeof(path), "%s/%s", base, leaf); + FILE *f = fopen(path, "r"); + if (!f) return -1.0; + if (!fgets(buf, sizeof(buf), f)) { fclose(f); return -1.0; } + fclose(f); + char *t = trim(buf); + if (!*t) return -1.0; + return atof(t); +} + +static void battery_read(Cell *e) +{ + char base[320] = ""; + + if (*e->src_path) { + /* Whatever you point it at. Not every machine calls it BAT0 — some + * name it CMB0 or macsmc-battery, and a laptop with two has to be + * told which one. */ + str_set(base, sizeof(base), e->src_path); + } else { + DIR *d = opendir("/sys/class/power_supply"); + if (!d) { e->out[0] = 0; return; } + struct dirent *de; + while ((de = readdir(d)) != NULL) { + char probe[400]; + snprintf(probe, sizeof(probe), "/sys/class/power_supply/%s/capacity", + de->d_name); + if (access(probe, R_OK) != 0) continue; /* not a battery */ + snprintf(base, sizeof(base), "/sys/class/power_supply/%s", de->d_name); + if (!strncasecmp(de->d_name, "BAT", 3)) break; /* prefer a BAT* */ + } + closedir(d); + } + if (!*base) { e->out[0] = 0; return; } + + char path[400], cap[32] = "", status[32] = ""; + snprintf(path, sizeof(path), "%s/capacity", base); + FILE *f = fopen(path, "r"); + if (f) { if (fgets(cap, sizeof(cap), f)) {} fclose(f); } + snprintf(path, sizeof(path), "%s/status", base); + f = fopen(path, "r"); + if (f) { if (fgets(status, sizeof(status), f)) {} fclose(f); } + + char *c = trim(cap), *st = trim(status); + if (!*c) { e->out[0] = 0; return; } + + /* What it is doing right now, in watts, and how long that leaves. Some + * kernels report energy and power, others charge and current; either pair + * gives both numbers. */ + double now_u = bat_num(base, "energy_now"), full_u = bat_num(base, "energy_full"); + double rate_u = bat_num(base, "power_now"), volt = bat_num(base, "voltage_now"); + if (now_u < 0) { /* the charge/current pair */ + now_u = bat_num(base, "charge_now"); + full_u = bat_num(base, "charge_full"); + double cur = bat_num(base, "current_now"); + if (cur >= 0 && volt >= 0) rate_u = cur * volt / 1e6; /* to µW */ + if (now_u >= 0 && volt >= 0) { now_u = now_u * volt / 1e6; + full_u = full_u * volt / 1e6; } + } + + char watts[32] = "", hours[32] = ""; + if (rate_u > 0) snprintf(watts, sizeof(watts), "%.1fW", rate_u / 1e6); + + bool full = !strcasecmp(st, "Full"); + bool up = full || !strcasecmp(st, "Charging"); + e->state = up ? PS_PLAYING : PS_PAUSED; /* reused: filling or not */ + + if (rate_u > 0 && now_u >= 0) { + double left_u = up ? (full_u > now_u ? full_u - now_u : 0) : now_u; + double h = left_u / rate_u; + if (h > 0 && h < 100) snprintf(hours, sizeof(hours), "%.1fh", h); + } + + /* %c capacity, %i the sign, %w watts, %h hours left, %s the word. + * Full gets no sign at all: nothing is happening, so nothing is said. */ + const char *sign = full ? "" : up ? "+" : "-"; + const char *bf = *e->src_fmt ? e->src_fmt : "%c%i"; + size_t o = 0; + for (const char *p = bf; *p && o < sizeof(e->out) - 1; ++p) { + const char *ins = NULL; + if (*p == '%' && p[1]) { + switch (p[1]) { + case 'c': ins = c; break; + case 'i': ins = sign; break; + case 'w': ins = watts; break; + case 'h': ins = hours; break; + case 's': ins = st; 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; +} + static void cell_finish(Cell *e) { e->buf[e->len] = 0; - if (e->source == SRC_CMUS) { cmus_parse(e); e->len = 0; return; } + if (e->source == SRC_CMUS) { cmus_parse(e); e->len = 0; return; } + if (e->source == SRC_BATTERY) { battery_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 = ' '; @@ -2799,9 +2962,13 @@ static bool ws_on_bar(Bar *b, Ws *w) static Ws *ws_by_num(Bar *b, int num) { - for (int i = 0; i < ws_count; ++i) - if (ws_list[i].num == num && ws_on_bar(b, &ws_list[i])) return &ws_list[i]; - return NULL; + Ws *other = NULL; + for (int i = 0; i < ws_count; ++i) { + if (ws_list[i].num != num) continue; + if (ws_on_bar(b, &ws_list[i])) return &ws_list[i]; + if (cfg.ws_other) other = &ws_list[i]; /* another screen's */ + } + return other; } static int slim_mode(Cell *e) @@ -2914,11 +3081,15 @@ 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; - /* 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); + /* 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. + * + * The hours are always the cell's own colour — the clock is cyan in + * the bar, so it is cyan down here too. Before noon they are drawn a + * little softer, which still separates 1:00 from 13:00 without + * swapping to a colour that reads as something else entirely. */ + Col hc = c; + if (tmv.tm_hour < 12) hc.a = (uint8_t)(hc.a * 0.62f); for (int i = 0; i < 12; ++i) { Col dc = cfg.dim; dc.a = 90; @@ -2929,8 +3100,8 @@ static void slim_draw_one(Canvas *cv, Cell *e, int mode, float x, float y, 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); + float mh = clampf(h * 0.5f + 1.0f * s, 1.0f, h); /* a pixel more */ + fill_rect(cv, x, y + h - mh, span * (float)tmv.tm_min / 60.0f, mh, mc); return; } if (mode == SLIM_BAR) { @@ -2991,17 +3162,29 @@ static void slim_draw_one(Canvas *cv, Cell *e, int mode, float x, float y, 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) +/* A column of dots rising from the bottom, `frac` of them lit. Dots rather + * than a solid fill so whatever is underneath — a workspace pill, the strip's + * own colour — still reads through the gaps. */ +static void dot_column(Canvas *cv, float x, float y, float w, float h, + float dot, float gap, 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); + /* square-ish, and centred in the slot: a dot as wide as the slot is a + * stripe, and a column of stripes reads as one solid block */ + if (w > dot * 1.4f) { x += (w - dot) * 0.5f; w = dot; } + + int n = (int)((h + gap) / (dot + gap)); + if (n < 2) n = 2; + if (n > 12) n = 12; + + int lit = frac <= 0.0f ? 0 : (int)ceilf(frac * (float)n); + if (lit > n) lit = n; + + Col dim_c = c; + dim_c.a = (uint8_t)(c.a * 0.28f); + + for (int i = 0; i < n; ++i) { + float dy = y + h - (float)(i + 1) * dot - (float)i * gap; + fill_rect(cv, x, dy, w, dot, i < lit ? c : dim_c); } } @@ -3018,12 +3201,14 @@ static bool cpu_load_col(int ws_idx, Col *out, float *frac) : 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); + /* One colour, not a ramp: how much is lit says how busy it is, and the + * hue stays out of the way. Orange was a mistake — `hl` paints the + * focused workspace, so a busy workspace looked like the current one. + * Only a workspace that is really pinned tips towards urgent. */ + Col hot = f < 0.85f ? cfg.accent + : col_mix(cfg.accent, cfg.urgent, (f - 0.85f) / 0.15f); + *out = col_mix(cfg.dim, hot, 0.7f + 0.3f * f); + out->a = (uint8_t)(200 + 55 * f); return true; } @@ -3044,7 +3229,8 @@ static void draw_signals(Canvas *cv, Bar *b, float y, float h, float s, Runs *rs 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; + if ((ws_on_bar(b, &ws_list[i]) || cfg.ws_other) && + ws_list[i].num > slots) slots = ws_list[i].num; } float tick = 14.0f * s; @@ -3054,22 +3240,28 @@ static void draw_signals(Canvas *cv, Bar *b, float y, float h, float s, Runs *rs Ws *w = ws_by_num(b, i); Col c = cfg.dim; c.a = 60; /* empty slot: a faint place holder */ + bool mine = w ? ws_on_bar(b, w) : true; if (w) { if (w->urgent) c = cfg.urgent; else if (w->focused) c = cfg.hl; else if (w->visible) c = cfg.accent; else { c = cfg.dim; c.a = 170; } + + /* Another screen's workspace is shown, but never with this + * screen's emphasis: focused there is not focused here. */ + if (!mine) { + if (w->focused || w->visible) { c = cfg.accent; c.a = 120; } + else { c = cfg.dim; c.a = 110; } + } } 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); + /* 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, 1.0f * s, 1.0f * s, lf, lc); } } if (w && b->hits < MAX_WORKSPACES) { /* still switchable when folded */ @@ -3131,6 +3323,14 @@ static void draw_signals(Canvas *cv, Bar *b, float y, float h, float s, Runs *rs * drawn through the same two-pass code. With ws_inset > 0 they float inside * the bar as their own pills instead of being as tall as the bar itself. */ +/* A workspace from another screen is drawn smaller, so it has to be measured + * smaller too, or the block comes out wider than what is in it. */ +static Font *ws_font(Bar *b, Ws *w, Font *f) +{ + if (ws_on_bar(b, w)) return f; + return font_get((int)((float)f->px * 0.78f + 0.5f)); +} + static float ws_button_w(Font *f, Ws *w, float s) { return fmaxf(text_width(f, w->label) + 2.0f * (float)cfg.ws_pad * s, @@ -3142,8 +3342,10 @@ static float ws_block_w(Font *f, Bar *b, float s) float w = 0; int n = 0; for (int i = 0; i < ws_count; ++i) { - if (!ws_on_bar(b, &ws_list[i])) continue; - w += ws_button_w(f, &ws_list[i], s); + /* the same set the drawing walks, or the block is measured shorter + * than it is drawn and the next cell lands on top of it */ + if (!ws_on_bar(b, &ws_list[i]) && !cfg.ws_other) continue; + w += ws_button_w(ws_font(b, &ws_list[i], f), &ws_list[i], s); n++; } if (n > 1) w += (float)(n - 1) * (float)cfg.ws_gap * s; @@ -3179,32 +3381,46 @@ static void ws_block_draw(Canvas *cv, Bar *b, Font *f, float x, float y0, float base = baseline_for(f, y0, vis, s); for (int i = 0; i < ws_count; ++i) { Ws *w = &ws_list[i]; - if (!ws_on_bar(b, w)) continue; - float bwid = ws_button_w(f, w, s); - float tw = text_width(f, w->label); + bool mine = ws_on_bar(b, w); + if (!mine && !cfg.ws_other) continue; + + Font *wf = ws_font(b, w, f); + float bwid = ws_button_w(wf, w, s); + float tw = text_width(wf, w->label); Col bg = cfg.ws_bg, fg = cfg.ws_fg; if (w->urgent) { bg = cfg.ws_urgent_bg; fg = cfg.ws_urgent_fg; } else if (w->focused) { bg = cfg.ws_focused_bg; fg = cfg.ws_focused_fg; } else if (w->visible) { bg = cfg.ws_visible_bg; fg = cfg.ws_visible_fg; } - 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); + /* A workspace on another monitor is drawn as a short pill sitting on + * the baseline: still readable, still clickable, and obviously not + * one of this screen's. */ + float bh = mine ? vis : vis * 0.55f; + float by = mine ? y0 : y0 + (vis - bh); + float bfade = mine ? fade : fade * 0.75f; + + pill(cv, x, by, bh, bwid, s, mine ? rad : rad * 0.6f, + col_scale_alpha(bg, bfade)); + if (mine) + text_draw(cv, f, x + (bwid - tw) * 0.5f, base, + col_scale_alpha(fg, fade), w->label); + else { + /* smaller type, lifted clear of the bottom edge, and a little + see-through: it belongs to another screen */ + float base2 = baseline_for(wf, by, bh, s) - 1.5f * s; + text_draw(cv, wf, x + (bwid - tw) * 0.5f, base2, + col_scale_alpha(fg, bfade * 0.72f), 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)); + /* Up the right edge of the button, the same dots as the folded + * strip so the two read as one idea. */ + float dw = clampf(2.0f * s, 1.0f, 3.0f); + float pad = 3.0f * s; + dot_column(cv, x + bwid - dw - pad, by + pad, dw, bh - 2.0f * pad, + 2.0f * s, 2.0f * s, lf, col_scale_alpha(lc, bfade)); } if (b->hits < MAX_WORKSPACES) { @@ -4164,6 +4380,12 @@ 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=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" +" particular one, NAME.src_fmt= lays it out (%c capacity,\n" +" %i sign, %s the word). NAME.cmd= goes back to your own\n" +" command for any of this\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" @@ -4357,7 +4579,36 @@ static void probe_report(void) } 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)"); + ? ws_list[cpu_top[i].ws].name : "no workspace"); + + /* Where the trail leads, and where it stops. If a build shows no + * workspace, this says which process broke the chain — a + * multiplexer, a container or a service does not descend from any + * window, so nothing can attribute it. */ + printf(" "); + int pid = cpu_top[i].pid; + for (int hop = 0; hop < 8 && pid > 1; ++hop) { + char c2[64] = "?"; + snprintf(path, sizeof(path), "/proc/%d/comm", pid); + FILE *g = fopen(path, "r"); + if (g) { + if (fgets(c2, sizeof(c2), g)) { + char *nl = strchr(c2, '\n'); + if (nl) *nl = 0; + } + fclose(g); + } + bool is_win = false; + for (int w = 0; w < cpu_nwin; ++w) + if (cpu_win[w].pid == pid) { is_win = true; break; } + printf("%s%d:%s", hop ? " < " : "", pid, c2); + if (is_win) { printf(" [window]"); break; } + + int ppid; unsigned long long o, c3; + if (!proc_stat_of(pid, &ppid, &o, &c3)) { printf(" (gone)"); break; } + pid = ppid; + } + printf("\n"); } if (!cpu_ws_n) printf(" no sample yet\n"); for (int i = 0; i < cpu_ws_n; ++i) diff --git a/swbr_config.example b/swbr_config.example index f8b1f48..d9ba20d 100644 --- a/swbr_config.example +++ b/swbr_config.example @@ -122,7 +122,9 @@ msg_error=e0533c # Note: the U+23Fx media glyphs (pause, stop) are missing from most mono # fonts, so the icon is derived here from glyphs that actually exist. cell=cmus -cmus.cmd=pgrep -x cmus >/dev/null || exit 0; cmus_control title-short | tr -d '<>&' +cmus.source=cmus # swbr runs cmus-remote itself: no script, and playing vs + # paused is read from cmus rather than guessed +cmus.cmus_fmt=%n # just Artist — Title; %i is the icon, %a %t %s also work cmus.interval=2 cmus.scroll=1 # text cell: cut with ".." and scroll it while hovered cmus.min_w=210 # fixed slot, so nothing else on the bar ever shifts @@ -138,22 +140,18 @@ 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 '<span foreground="#2fbf71">❚❚</span>';; paused) printf '<span foreground="#e8b81a">▶</span>';; *) printf '<span foreground="#5a6b7a">■</span>';; esac +cmusui.source=cmus # the same source, laid out as just the icon +cmusui.cmus_fmt=%i # ▶ while playing, ⏸ while paused, following the real state 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 # 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.color=e8b81a +cmusui.slim=media # folded: TWO full bars while playing, the same two short + # and dim when paused, nothing when cmus is stopped. + # source=cmus knows which, so slim_on is not needed. 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 @@ -183,15 +181,6 @@ 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' @@ -235,8 +224,15 @@ 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=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.source=battery # reads /sys itself: 83+ filling, 83- draining, no + # sign when full +# battery.bat_path=/sys/class/power_supply/BAT1 # if yours is not BAT0, or + # you have two. Left out, swbr takes the first thing with a + # capacity file, preferring one called BAT* +# battery.src_fmt=%c%i %w %h # %c capacity, %i sign, %w watts now, %h hours + # left, %s the word. This one reads "83- 21.5W 1.7h" +# battery.cmd=... # setting a command switches the cell back to your own + # script; source= and cmd= are the two ways, never both battery.interval=10 battery.color=e1eeff battery.warn=<30:cecb00 @@ -288,6 +284,9 @@ dim=5a6b7aff accent=89afc4ff # visible-but-not-focused workspace tick hl=cb9b00ff # focused workspace tick in the folded strip urgent=e0533cff +ws_other=1 # 1 = also show the workspaces of your other monitors, as + # short pills on the baseline so they are clearly not this + # screen's 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