aboutsummaryrefslogtreecommitdiffstats
path: root/src/app_draw.c
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-22 15:12:02 +0200
committeruser <user@clank>2026-09-22 15:12:02 +0200
commit720aa48e249c0c962e8f73372ceded6092414a6e (patch)
treedb713dc86f1333dab0f509e633077384005562c3 /src/app_draw.c
parentsidebar: air inside it, the way panes have padding (tab_bar_padding) (diff)
downloadslosh-720aa48e249c0c962e8f73372ceded6092414a6e.tar.gz
slosh-720aa48e249c0c962e8f73372ceded6092414a6e.tar.xz
slosh-720aa48e249c0c962e8f73372ceded6092414a6e.zip
osc 5577: `busy`, and a sidebar that shows it (busy_mark)
A status is a line of text, and a line of text cannot say whether it is still true. "building 3/7" reads exactly the same while it builds and ten minutes after it stopped, which is the one thing somebody reading it from another tab wants to know -- and the only workaround available to a program was to write the answer into the words and keep rewriting them. So `busy`: printf '\033]5577;1;status;make test\033\\' printf '\033]5577;1;busy;1\033\\' ... printf '\033]5577;1;busy;0\033\\' A flag of its own rather than a field of `status`, because a status is the *whole* payload after the verb -- `status;a;b;c` has always been the text "a;b;c" -- so anything added to that line would change what every existing sender means, including the five pi extensions D1 promises byte compatibility to. Orthogonal, so a program can leave one line up and toggle the other; anything but 0/false/no/off is on; `clear` takes both; and a pane whose program has gone is never busy, whatever it last claimed, because a spinner turning under a dead tab is a lie you keep looking at. In a sidebar the row turns `tab_status_busy` -- the theme's accent, derived like the other status colours, so no theme needs a line -- and takes a spinner in the column `tab_bar_padding` was already holding blank, so nothing reflows when a program starts or stops working. `busy_mark` is the frames, separated by spaces: one frame is a static mark and costs nothing, several make it turn, and only then does the session keep a frame clock -- via the same `animating` flag an animated shader uses, so the cost lands only while such a row is actually composed, and `""` drops the mark entirely and leaves the colour to say it. The frame is a pure function of the clock, like every other pulse here, so two clients attached to one session show the same one. `panes` now reports `status` and `busy`, which is the scriptable half of the same fact: what a pane says about itself, without reading its screen.
Diffstat (limited to 'src/app_draw.c')
-rw-r--r--src/app_draw.c72
1 files changed, 70 insertions, 2 deletions
diff --git a/src/app_draw.c b/src/app_draw.c
index cf3ff39..8c70771 100644
--- a/src/app_draw.c
+++ b/src/app_draw.c
@@ -2089,6 +2089,7 @@ struct tabstatus {
* of it could ever be shown. */
char text[256];
uint32_t pane;
+ bool busy; /* the program says this is still happening */
} row[16];
size_t n; /* rows kept */
size_t total; /* rows there were, so the cap can say "and more" */
@@ -2107,6 +2108,7 @@ static void tabstatus_cb(node_t *n, void *ud) {
if (ts->n >= sizeof ts->row / sizeof *ts->row) return;
snprintf(ts->row[ts->n].text, sizeof ts->row[0].text, "%s", text);
ts->row[ts->n].pane = n->id;
+ ts->row[ts->n].busy = pane_status_busy(n->pane);
ts->n++;
}
@@ -2139,6 +2141,46 @@ static void mark_cut(char *row, size_t cap, uint16_t width) {
if (len + 4 <= cap) memcpy(row + len, "\u2026", 4);
}
+/* One frame of `busy_mark`, by wall clock, into `out`; false when the mark is
+ * empty. `spins` says whether the mark has more than one frame in it, which is
+ * the only thing that costs the session a frame clock -- a static mark is a
+ * mark, and a mark does not need repainting.
+ *
+ * Time rather than a counter for the same reason the rest of this file derives
+ * rather than remembers: two sidebars drawn from one session (several clients
+ * attached) show the same frame, and a frame dropped under load is a frame
+ * skipped rather than a spinner that limps behind. */
+static bool busy_frame(char *out, size_t cap, bool *spins) {
+ const char *m = CFG.busy_mark;
+ *spins = false;
+ while (*m == ' ') m++;
+ if (!*m) return false;
+ size_t n = 0;
+ const char *starts[16];
+ size_t lens[16];
+ for (const char *p = m; *p && n < 16;) {
+ while (*p == ' ') p++;
+ if (!*p) break;
+ const char *e = p;
+ while (*e && *e != ' ') e++;
+ starts[n] = p;
+ lens[n] = (size_t)(e - p);
+ n++;
+ p = e;
+ }
+ if (!n) return false;
+ *spins = n > 1;
+ size_t i = 0;
+ if (n > 1) {
+ uint16_t step = CFG.busy_ms ? CFG.busy_ms : 120;
+ i = (size_t)((uint64_t)(now_ms_() / step) % n);
+ }
+ size_t len = lens[i] < cap - 1 ? lens[i] : cap - 1;
+ memcpy(out, starts[i], len);
+ out[len] = 0;
+ return true;
+}
+
#define STATUS_WRAP_MAX 8 /* rows one status may ever take */
#define STATUS_ROW_CAP 256
@@ -2377,6 +2419,21 @@ void draw_tab_sidebar(app_t *a, screen_t *s) {
color_t band = TAB_STATUS_BG;
if (lines > 1 && (j % 2)) band = TAB_STATUS_STRIPE;
+ /* A pane that says it is still working: its rows in the busy colour,
+ * and a spinner in the column the padding would otherwise leave blank.
+ * The mark goes on the first row only -- it belongs to the status, not
+ * to each line it wrapped onto -- and the colour carries the rest,
+ * which is also what a sidebar with no padding to spare falls back to.
+ *
+ * This is the one thing a status line cannot say about itself: the text
+ * of a finished `make test` and a running one is the same text. */
+ bool busy = ts.row[j].busy;
+ char spin[16] = "";
+ bool spins = false;
+ bool marked =
+ busy && CFG.tab_pad_left && busy_frame(spin, sizeof spin, &spins);
+ if (marked && spins) a->animating = true;
+
for (size_t k = 0; k < nrows[j] && y < limit; k++, y++) {
char text[STATUS_ROW_CAP + 8];
/* One leading space, which is the label's own -- a status starts in
@@ -2397,8 +2454,19 @@ void draw_tab_sidebar(app_t *a, screen_t *s) {
* would be left with none -- tab_status_fg/bg are there for themes
* that do. The hover brightening doubles as the affordance that the
* row is a door; the hint under the pointer says where it leads. */
- screen_text(s, cx, y, text, hot ? TAB_HOVER : TAB_STATUS_FG, band,
- hot ? ATTR_BOLD : ATTR_ITALIC);
+ screen_text(s, cx, y, text,
+ hot ? TAB_HOVER
+ : busy ? TAB_STATUS_BUSY
+ : TAB_STATUS_FG,
+ band, hot ? ATTR_BOLD : ATTR_ITALIC);
+ /* In the indent, against the text rather than against the frame, and
+ * on the first row only: the mark belongs to the status, not to each
+ * line it wrapped onto. It takes a column the padding was holding
+ * blank, so nothing moves when a program starts or stops working --
+ * a list that reflowed on every `make` would be worse than no mark. */
+ if (marked && k == 0)
+ screen_text(s, (uint16_t)(cx + CFG.tab_pad_left - 1), y, spin,
+ hot ? TAB_HOVER : TAB_STATUS_BUSY, band, 0);
char act[24];
snprintf(act, sizeof act, "find:%u", ts.row[j].pane);
hit_add(&s->hits, cx, y, cw, 1, act);