aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-14 16:38:26 +0200
committeruser <user@clank>2026-08-14 16:38:26 +0200
commit6548577400ab6833b9424de056c6da6079ed6ff4 (patch)
treeb2900cbb0e272f18024d95596153da42f6196c24 /src
parentsl0ppty: a collapsed pane is drawn as a pane, closed (diff)
downloadslosh-6548577400ab6833b9424de056c6da6079ed6ff4.tar.gz
slosh-6548577400ab6833b9424de056c6da6079ed6ff4.tar.xz
slosh-6548577400ab6833b9424de056c6da6079ed6ff4.zip
sl0ppty: a status line along the bottom
The strip along the top answers "which tab" and "how many panes" — questions about what you could switch to. Nothing answered "what am I looking at", and three of the states a pane can be in were discoverable only by being surprised by them. So the bottom line is about the focused pane and deliberately repeats nothing from the top: the session it lives in, the tab, what the pane calls itself (its purpose if it declared one, otherwise its title), and on the right, the state worth knowing — scrolled back and how far, on an alternate screen, not started yet, or exited. Right side budgeted first, the same rule the tab strip and the pane frame already use, so a long name cannot push the state off the end. The session name had to be plumbed in: the server knows it because it opened the socket under that name, and the app had no reason to until now. Reserving a row is not free and three tests noticed, which is the correct number to notice. Two were arithmetic — a chrome budget written as rows-5 and a six-row session whose pane no longer had room for a frame at all. The third was more useful: test_tiny_panes_have_no_targets asserted "no border hit that is one row tall and under four wide", which is a description of the shape a collapsed header used to have rather than of the thing being tested, and it started catching the perfectly legitimate side border of a pane that was merely short. It now says what it means — no split targets on a *header* row — and is both stricter and immune to the layout getting a row shorter again.
Diffstat (limited to 'src')
-rw-r--r--src/app.c80
-rw-r--r--src/app.h4
-rw-r--r--src/config.c3
-rw-r--r--src/config.h3
-rw-r--r--src/server.c1
5 files changed, 89 insertions, 2 deletions
diff --git a/src/app.c b/src/app.c
index 09fec75..7daa7cc 100644
--- a/src/app.c
+++ b/src/app.c
@@ -177,6 +177,10 @@ struct app {
int64_t name_click_ms;
uint32_t name_click_id;
const char *const *argv;
+ /* What this session is called. The server knows it because it opened the
+ * socket under that name; the app only knows it because it is worth saying
+ * out loud when several are running. */
+ char session[64];
/* the screen we last composed into: its hit list is what a click resolves
* against, so routing can never consult geometry the user never saw */
const screen_t *painted;
@@ -266,6 +270,72 @@ size_t app_toast_count(app_t *a) {
return a->ntoasts;
}
+/* The line along the bottom: what you are looking at, rather than what you
+ * could switch to. The strip along the top already answers "which tab" and
+ * "how many panes"; repeating that here would spend a row saying it twice.
+ *
+ * So this one is about the focused pane: which session it lives in, which tab,
+ * what it calls itself, and whether it is in a state worth knowing about —
+ * scrolled back, on an alternate screen, or not started yet. Those last three
+ * are the ones you can otherwise only discover by being surprised. */
+static void draw_status_line(app_t *a, screen_t *s) {
+ if (!CFG.status_line || s->rows < 3) return;
+ uint16_t y = (uint16_t)(s->rows - CFG.gap - 1);
+ uint16_t x = (uint16_t)(CFG.gap * CFG.gap_aspect);
+ uint16_t right = (uint16_t)(s->cols > x ? s->cols - x : 0);
+ if (right <= x) return;
+
+ node_t *f = a->ntabs ? cur(a)->focus : NULL;
+
+ /* Right side first, so a long name can never push the state off the end —
+ * the same budgeting rule the tab strip and the pane frame both use. */
+ char ind[64] = {0};
+ if (f) {
+ if (pane_suspended(f->pane)) {
+ snprintf(ind, sizeof ind, "not started");
+ } else if (!pane_alive(f->pane)) {
+ snprintf(ind, sizeof ind, "exited");
+ } else if (pane_scrolled(f->pane)) {
+ uint32_t above = 0, total = 0;
+ pane_scroll_pos(f->pane, &above, &total);
+ snprintf(ind, sizeof ind, "\u25b2%u scrolled", total > above ? total - above : 0);
+ } else if (pane_alt_screen(f->pane)) {
+ snprintf(ind, sizeof ind, "alt screen");
+ }
+ }
+ if (ind[0]) {
+ uint16_t iw = (uint16_t)strlen(ind);
+ if (right > x + iw + 2) {
+ screen_text(s, (uint16_t)(right - iw), y, ind, TITLE_FOCUS, NO_COLOR,
+ ATTR_BOLD);
+ right = (uint16_t)(right - iw - 1);
+ }
+ }
+
+ char line[256];
+ size_t n = 0;
+ if (a->session[0])
+ n += (size_t)snprintf(line + n, sizeof line - n, "%s", a->session);
+ if (a->ntabs) {
+ const tab_t *t = &a->tabs[a->cur];
+ const char *tn = t->name[0] ? t->name : (t->purpose[0] ? t->purpose : NULL);
+ if (tn)
+ n += (size_t)snprintf(line + n, sizeof line - n, "%s%s",
+ n ? " \u00b7 " : "", tn);
+ }
+ if (f) {
+ const char *pt = pane_title(f->pane);
+ const char *pp = f->purpose[0] ? f->purpose : NULL;
+ if (pp)
+ n += (size_t)snprintf(line + n, sizeof line - n, "%s%s", n ? " \u00b7 " : "", pp);
+ else if (pt && *pt)
+ n += (size_t)snprintf(line + n, sizeof line - n, "%s%s", n ? " \u00b7 " : "", pt);
+ }
+ if (!n) return;
+ if (n > (size_t)(right - x)) line[right - x] = 0;
+ screen_text(s, x, y, line, FRAME_IDLE, NO_COLOR, 0);
+}
+
static void draw_toasts(app_t *a, screen_t *s) {
toasts_expire(a);
if (!a->ntoasts) return;
@@ -705,6 +775,7 @@ static void layout_node(node_t *n, rect_t r, layout_ctx_t *ctx) {
}
#define STRIP_ROWS (CFG.status_bar ? 1 : 0)
+#define LINE_ROWS (CFG.status_line ? 1 : 0)
static void layout(app_t *a) {
if (!a->ntabs) return;
@@ -713,7 +784,9 @@ static void layout(app_t *a) {
rect_t r = {.x = gx,
.y = top,
.w = (uint16_t)(a->cols > 2 * gx ? a->cols - 2 * gx : a->cols),
- .h = (uint16_t)(a->rows > top + gy ? a->rows - top - gy : 1)};
+ .h = (uint16_t)(a->rows > top + gy + LINE_ROWS
+ ? a->rows - top - gy - LINE_ROWS
+ : 1)};
node_t *root = cur(a)->root;
if (!root) return;
@@ -1018,6 +1091,10 @@ static node_t *pane_by_id(app_t *a, uint32_t id) {
return b.found;
}
+void app_set_session(app_t *a, const char *name) {
+ snprintf(a->session, sizeof a->session, "%s", name ? name : "");
+}
+
bool app_focus_pane(app_t *a, uint32_t id) {
node_t *n = pane_by_id(a, id);
if (!n) return false;
@@ -2118,6 +2195,7 @@ void app_compose(app_t *a, screen_t *s) {
layout(a);
if (CFG.status_bar) draw_tab_strip(a, s);
draw_node(a, s, cur(a)->root);
+ draw_status_line(a, s);
if (a->finder) draw_finder(a, s); /* painted last, so its hits win */
draw_toasts(a, s); /* and above even that: it is transient */
}
diff --git a/src/app.h b/src/app.h
index 3cb708f..7d0e982 100644
--- a/src/app.h
+++ b/src/app.h
@@ -83,6 +83,10 @@ bool app_apply_layout_text(app_t *a, const char *text, bool replace, char *err,
bool app_apply_layout_file(app_t *a, const char *path, bool replace, char *err,
size_t errcap);
+/* What this session is called, for the status line. The server knows it
+ * because it opened the socket under that name. */
+void app_set_session(app_t *a, const char *name);
+
/* Address panes by id (the control API), not "the focused one". */
bool app_focus_pane(app_t *a, uint32_t id);
bool app_split_pane(app_t *a, uint32_t id, bool rows);
diff --git a/src/config.c b/src/config.c
index f7b1179..e0a40fe 100644
--- a/src/config.c
+++ b/src/config.c
@@ -178,6 +178,7 @@ void config_defaults(config_t *c) {
c->hover_delay_ms = 250;
c->double_click_ms = 400;
c->status_bar = true;
+ c->status_line = true;
c->focus_follows_mouse = true;
c->default_fg = rgb(0xff, 0xff, 0xff);
@@ -294,6 +295,8 @@ bool config_load(config_t *c, const char *path, char *err, size_t errcap) {
c->pad = (uint16_t)kdl_arg_int(kdl_child(root, "padding"), 0, c->pad);
c->rounded = kdl_arg_bool(kdl_child(root, "rounded"), 0, c->rounded);
c->status_bar = kdl_arg_bool(kdl_child(root, "status_bar"), 0, c->status_bar);
+ c->status_line =
+ kdl_arg_bool(kdl_child(root, "status_line"), 0, c->status_line);
c->focus_follows_mouse = kdl_arg_bool(kdl_child(root, "focus_follows_mouse"), 0,
c->focus_follows_mouse);
diff --git a/src/config.h b/src/config.h
index 30b5005..a85a204 100644
--- a/src/config.h
+++ b/src/config.h
@@ -110,7 +110,8 @@ typedef struct {
* already had to avoid by hand. */
shader_t state_shaders[PSTATE_COUNT][SHADE_MAX];
size_t state_n[PSTATE_COUNT];
- bool status_bar;
+ bool status_bar; /* the strip along the top: tabs, prefix, pane count */
+ bool status_line; /* the line along the bottom: what you are looking at */
bool focus_follows_mouse;
/* What a cell's "terminal default" colour resolves to when a shader has to
diff --git a/src/server.c b/src/server.c
index b372821..08c944b 100644
--- a/src/server.c
+++ b/src/server.c
@@ -188,6 +188,7 @@ int server_run(const char *name, const char *const argv[], uint16_t cols,
server_t s = {0};
s.app = app_new(argv, cols, rows);
+ if (s.app) app_set_session(s.app, name);
if (!s.app) return 1;
if (layout) {
char err[256] = {0};