aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-10 15:42:17 +0200
committeruser <user@clank>2026-09-10 15:42:17 +0200
commitecf609e3f903e05690ef21bb4f7dbd08e6d8e91d (patch)
treeddf7b66b4e5938f9205e4a901899e23fb3ea4537 /src
parentsidebar: a row of air on top (tab_bar_pad), indicators under the labels' inset (diff)
downloadslosh-ecf609e3f903e05690ef21bb4f7dbd08e6d8e91d.tar.gz
slosh-ecf609e3f903e05690ef21bb4f7dbd08e6d8e91d.tar.xz
slosh-ecf609e3f903e05690ef21bb4f7dbd08e6d8e91d.zip
sidebar: what each tab's panes are saying, said under the tab (tab_bar_status)
Panes already announce a one-line status over OSC 5577, and it rendered in exactly two places -- both inside the pane's own frame, which is the one place you are not looking when you most want it. The sidebar has the vertical room the top strip never had: under each tab, up to tab_bar_status rows of what the panes inside said, in tree order, dim like the pane count. A dead pane's row says how it died instead (exit_words), the same substitution its own frame makes. The cap's last row is spent on an ellipsis rather than one more status pretending the list is complete, and a status cut to the sidebar's width ends in one too -- 'building 3' is a different fact than 'building 3/7'. Each row is a find: hit, the verb that already means 'go to this pane', so clicking a status jumps to the pane that said it, across tabs. Freshness costs nothing new: an OSC status marks the pane dirty, the same path the bell indicator rides out of background tabs.
Diffstat (limited to 'src')
-rw-r--r--src/app_draw.c80
-rw-r--r--src/config.c9
2 files changed, 88 insertions, 1 deletions
diff --git a/src/app_draw.c b/src/app_draw.c
index 2cf22d0..5d358ad 100644
--- a/src/app_draw.c
+++ b/src/app_draw.c
@@ -1954,6 +1954,52 @@ void draw_tab_strip(app_t *a, screen_t *s) {
* budgeting rule the horizontal strip applies from the right, so a long tab
* list can never eat them. The status line below keeps the full width; the
* sidebar stops above it. */
+/* The status rows a tab contributes to the sidebar: what each pane inside
+ * said through OSC 5577, or -- for a pane that is dead rather than suspended
+ * -- how it died, which is the same substitution the pane's own frame makes
+ * (draw_pane_status): a program that is gone cannot mean whatever it last
+ * asked us to show. Collected in tree order, pane id kept so the row can be
+ * a `find:` target: clicking a status jumps to the pane that said it. */
+struct tabstatus {
+ struct {
+ char text[96];
+ uint32_t pane;
+ } row[16];
+ size_t n; /* rows kept */
+ size_t total; /* rows there were, so the cap can say "and more" */
+};
+
+static void tabstatus_cb(node_t *n, void *ud) {
+ struct tabstatus *ts = ud;
+ if (!n->pane) return;
+ char text[96] = {0};
+ if (!pane_alive(n->pane) && !pane_suspended(n->pane))
+ exit_words(n->pane, text, sizeof text);
+ else
+ snprintf(text, sizeof text, "%s", pane_status(n->pane));
+ if (!text[0]) return;
+ ts->total++;
+ 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->n++;
+}
+
+/* Cut `text` to `budget` cells at a UTF-8 boundary, saying so: the last cell
+ * becomes an ellipsis, because a status cut silently reads as complete --
+ * "building 3" is a different fact than "building 3/7". */
+static void fit_status(char *text, size_t cap, uint16_t budget) {
+ if (cells(text) <= budget) return;
+ while (text[0] && cells(text) > (uint16_t)(budget > 0 ? budget - 1 : 0)) {
+ size_t len = strlen(text);
+ do len--;
+ while (len && (text[len] & 0xc0) == 0x80);
+ text[len] = 0;
+ }
+ size_t len = strlen(text);
+ if (len + 4 <= cap) memcpy(text + len, "\u2026", 4);
+}
+
void draw_tab_sidebar(app_t *a, screen_t *s) {
bool dragging_pane = a->drag.kind == DRAG_TITLE && a->drag.moved;
uint16_t sw = app_tab_bar_cols();
@@ -1989,8 +2035,40 @@ void draw_tab_sidebar(app_t *a, screen_t *s) {
}
}
- for (size_t i = 0; i < a->ntabs && y < limit; i++, y++)
+ uint16_t cap = CFG.tab_bar_status;
+ if (cap > 16) cap = 16; /* the collector keeps no more than that */
+ for (size_t i = 0; i < a->ntabs && y < limit; i++) {
draw_tab_cell(a, s, i, x, y, sw, dragging_pane);
+ y++;
+ if (!cap) continue;
+
+ struct tabstatus ts = {0};
+ walk(a->tabs[i].root, tabstatus_cb, &ts);
+ size_t show = ts.total > cap ? cap : ts.n;
+ for (size_t j = 0; j < show && y < limit; j++, y++) {
+ /* The cap's last row is spent on saying there was more, rather than on
+ * one more status pretending the list is complete. */
+ bool more = ts.total > cap && j == (size_t)(cap - 1);
+ char text[112];
+ if (more) {
+ snprintf(text, sizeof text, " \u2026");
+ } else {
+ snprintf(text, sizeof text, " %s", ts.row[j].text);
+ fit_status(text, sizeof text, sw);
+ }
+ /* Dim like the pane count: these are ambience until pointed at. The
+ * hover brightening doubles as the affordance that the row is a door;
+ * the hint under the pointer says where it leads. */
+ bool hot = !more && ptr_on(a, x, y, sw, 1);
+ screen_text(s, x, y, text, hot ? TAB_HOVER : TAB_COUNT, NO_COLOR,
+ hot ? ATTR_BOLD : 0);
+ if (!more) {
+ char act[24];
+ snprintf(act, sizeof act, "find:%u", ts.row[j].pane);
+ hit_add(&s->hits, x, y, sw, 1, act);
+ }
+ }
+ }
if (cells(CFG.newtab_mark) && y < limit)
draw_newtab_button(a, s, x, y, dragging_pane);
diff --git a/src/config.c b/src/config.c
index d800696..48442e0 100644
--- a/src/config.c
+++ b/src/config.c
@@ -927,6 +927,9 @@ void config_defaults(config_t *c) {
* as cramped, and in compact mode the row lines the first tab up with the
* pane content inside the ring rather than with the ring's top line. */
c->tab_bar_pad = 1;
+ /* Three is enough for "the build, the tests, and one more thing" without a
+ * chatty tab pushing the list off the bottom. */
+ c->tab_bar_status = 3;
c->status_line = true;
/* Deliberately wider than the panes' own margin (gap * gap_aspect = 2), so
* the strip and the line read as chrome sitting outside the layout rather
@@ -1631,6 +1634,9 @@ char *config_render(const config_t *c) {
c->tab_bar_width);
cb_add(&b, "tab_bar_pad %u // blank rows above a sidebar's tabs\n",
c->tab_bar_pad);
+ cb_add(&b,
+ "tab_bar_status %u // pane status rows under each tab, 0 = off\n",
+ c->tab_bar_status);
cb_add(&b, "status_line %s // the line along the bottom\n",
yesno(c->status_line));
cb_add(&b, "status_pad %u\n", c->status_pad);
@@ -1941,6 +1947,7 @@ static const char *const KNOWN_TOP[] = {
"status_pad",
"tab_bar_pad",
"tab_bar_side",
+ "tab_bar_status",
"tab_bar_width",
"theme",
"title_align",
@@ -2154,6 +2161,8 @@ static bool load_into(config_t *c, const char *path, int depth, char *err,
c->tab_bar_width);
c->tab_bar_pad =
(uint16_t)kdl_arg_int(kdl_child(root, "tab_bar_pad"), 0, c->tab_bar_pad);
+ c->tab_bar_status = (uint16_t)kdl_arg_int(kdl_child(root, "tab_bar_status"),
+ 0, c->tab_bar_status);
c->status_line =
kdl_arg_bool(kdl_child(root, "status_line"), 0, c->status_line);
c->status_pad =