diff options
| author | user <user@clank> | 2026-09-22 15:12:02 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-22 15:12:02 +0200 |
| commit | 720aa48e249c0c962e8f73372ceded6092414a6e (patch) | |
| tree | db713dc86f1333dab0f509e633077384005562c3 /src | |
| parent | sidebar: air inside it, the way panes have padding (tab_bar_padding) (diff) | |
| download | slosh-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')
| -rw-r--r-- | src/app.c | 7 | ||||
| -rw-r--r-- | src/app_draw.c | 72 | ||||
| -rw-r--r-- | src/config.c | 25 | ||||
| -rw-r--r-- | src/pane.c | 18 |
4 files changed, 120 insertions, 2 deletions
@@ -2155,6 +2155,13 @@ static void panes_cb(node_t *n, void *ud) { json_int(j, "tab_id", (long long)pj->a->tabs[ti].id); const char *t = pane_title(n->pane); json_str(j, "title", t ? t : "", t ? strlen(t) : 0); + /* What the pane said about itself over OSC 5577, which is the one thing in + * here it chose rather than had chosen for it -- and whether it says that + * is still happening, which `alive` does not answer: a pane can be alive + * and idle all day. */ + const char *st = pane_status(n->pane); + json_str(j, "status", st ? st : "", st ? strlen(st) : 0); + json_bool(j, "busy", pane_status_busy(n->pane)); json_obj_close(j); } 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); diff --git a/src/config.c b/src/config.c index 81f23d5..44aee5b 100644 --- a/src/config.c +++ b/src/config.c @@ -696,6 +696,7 @@ static const struct { {"tab_count", offsetof(config_t, tab_count)}, {"tab_status_fg", offsetof(config_t, tab_status_fg)}, {"tab_status_bg", offsetof(config_t, tab_status_bg)}, + {"tab_status_busy", offsetof(config_t, tab_status_busy)}, {"tab_status_stripe", offsetof(config_t, tab_status_stripe)}, {"status", offsetof(config_t, status)}, {"status_state", offsetof(config_t, status_state)}, @@ -937,6 +938,14 @@ void config_defaults(config_t *c) { c->newtab_pad = 1; c->bell_indicator = true; snprintf(c->bell_mark, sizeof c->bell_mark, "[!]"); + /* Braille, because it is one cell wide in every terminal that has the + * glyphs and reads as motion rather than as a character that changed. A + * terminal without them gets a single frame of nothing useful, which is + * what `busy_mark "*"` (or "") is for. */ + snprintf(c->busy_mark, sizeof c->busy_mark, + "\u280b \u2819 \u2839 \u2838 \u283c \u2834 \u2826 \u2827 " + "\u2807 \u280f"); + c->busy_ms = 120; c->keep_dead = KEEP_DEAD_COMMANDS; /* Gentle: an unfocused pane is one you are still reading half the time. * At 60 white text lands on #c3c3c3, which reads as "not this one" without @@ -1126,6 +1135,10 @@ void config_defaults(config_t *c) { * to. The slant already says what it is; this says it is worth reading. */ c->tab_status_fg = blend(dim, accent, 40); c->tab_status_bg = (color_t){0}; /* none: the terminal's own background */ + /* All the way to the accent: a running thing is the one item in a list of + * annotations that is still moving, and the spinner beside it is already + * saying so -- the colour is what makes it findable without looking. */ + c->tab_status_busy = accent; /* Barely off the background: the stripe has to say "different pane", not * "selected". Only drawn when statuses can wrap -- see tab_bar_status_lines * -- so an unwrapped sidebar keeps its transparent rows. */ @@ -1807,6 +1820,10 @@ char *config_render(const config_t *c) { "columns on top\n", c->newtab_pad); cb_qstr(&b, "bell_mark", c->bell_mark, NULL); + cb_qstr(&b, "busy_mark", c->busy_mark, + "// a busy pane's spinner; one frame is a static mark"); + cb_add(&b, "busy_ms %u // how long a spinner frame is held\n", + c->busy_ms); cb_add(&b, "\n// ---- behaviour ----\n"); cb_add(&b, "focus_follows_mouse %s\n", yesno(c->focus_follows_mouse)); @@ -2211,6 +2228,8 @@ static const char *const KNOWN_TOP[] = { "attach_indicator", "bell_indicator", "bell_mark", + "busy_mark", + "busy_ms", "close_mark", "compact", "ctrl_d_exits", @@ -2760,6 +2779,9 @@ static bool load_into(config_t *c, const char *path, int depth, char *err, kdl_arg_bool(kdl_child(root, "bell_indicator"), 0, c->bell_indicator); const char *bm = kdl_arg(kdl_child(root, "bell_mark"), 0, NULL); if (bm) set_mark(c->bell_mark, sizeof c->bell_mark, bm); + const char *busym = kdl_arg(kdl_child(root, "busy_mark"), 0, NULL); + if (busym) snprintf(c->busy_mark, sizeof c->busy_mark, "%s", busym); + c->busy_ms = (uint16_t)kdl_arg_int(kdl_child(root, "busy_ms"), 0, c->busy_ms); const kdl_node_t *mins = kdl_child(root, "min_split"); if (mins) { @@ -2938,6 +2960,9 @@ static bool load_into(config_t *c, const char *path, int depth, char *err, * different row at a glance, not enough to read as a highlight. */ if (!kdl_child(theme, "tab_status_stripe")) c->tab_status_stripe = blend(c->default_bg, c->tab_hover, 12); + /* The busy one is the theme's own accent, which is what it uses for + * "this one" everywhere else. */ + if (!kdl_child(theme, "tab_status_busy")) c->tab_status_busy = c->tab_hover; } /* After the theme, so the wash follows whatever scroll_bg the theme just * chose -- a config that wrote its own `scrolled` chain keeps it, whether @@ -99,6 +99,12 @@ struct pane { osc_scan_t scan; char status[256]; + /* Whether what the status describes is still happening. Its own flag rather + * than a field of the status text, because the text is the *whole* payload + * after the verb (`status;a;b;c` is "a;b;c") and always has been -- adding a + * field to it would change what every existing sender means. Orthogonal, so + * a program can leave one line up and toggle the other. */ + bool busy; pane_button_t buttons[8]; size_t nbuttons; pane_osc_fn osc_cb; @@ -115,6 +121,7 @@ void pane_set_osc_handler(pane_t *p, pane_osc_fn fn, void *ud) { } const char *pane_status(const pane_t *p) { return p->status; } +bool pane_status_busy(const pane_t *p) { return p->busy && p->alive; } size_t pane_buttons(const pane_t *p, const pane_button_t **out) { *out = p->buttons; return p->nbuttons; @@ -176,10 +183,20 @@ static void on_osc5577(const char *verb, const char *payload, void *ud) { if (strcmp(verb, "status") == 0) { snprintf(p->status, sizeof p->status, "%s", payload); p->dirty = true; + } else if (strcmp(verb, "busy") == 0) { + /* "is this still happening?" -- the one thing a line of text cannot say + * about itself. Off for the spellings of no, on for everything else + * including an empty payload, because `busy` with nothing after it is a + * program saying it is busy. */ + bool on = !(strcmp(payload, "0") == 0 || strcmp(payload, "false") == 0 || + strcmp(payload, "no") == 0 || strcmp(payload, "off") == 0); + if (p->busy != on) p->dirty = true; + p->busy = on; } else if (strcmp(verb, "buttons") == 0) { parse_buttons(p, payload); } else if (strcmp(verb, "clear") == 0) { p->status[0] = 0; + p->busy = false; p->nbuttons = 0; p->dirty = true; } else if (strcmp(verb, "hello") == 0) { @@ -690,6 +707,7 @@ bool pane_restart(pane_t *p) { * status line and buttons from a program that is not there describe * nothing, and the new run gets to say its own. */ p->status[0] = 0; + p->busy = false; p->nbuttons = 0; osc_scan_reset(&p->scan); p->exit_known = false; |
