aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-14 16:42:38 +0200
committeruser <user@clank>2026-08-14 16:42:38 +0200
commit93d9ed9863cd509950fb8beae66d40d37288004d (patch)
treede0b21eb503693aa01079129a543d536962538d2
parentsl0ppty: a status line along the bottom (diff)
downloadslosh-93d9ed9863cd509950fb8beae66d40d37288004d.tar.gz
slosh-93d9ed9863cd509950fb8beae66d40d37288004d.tar.xz
slosh-93d9ed9863cd509950fb8beae66d40d37288004d.zip
sl0ppty: the status line counts this tab's panes
The strip along the top counts every pane in the session, which is the right number for a strip that lists every tab. It is the wrong number for "how big is the thing I am working in", and nothing answered that. So the line along the bottom carries its own count, of this tab only. The two disagree as soon as there is more than one tab, which is the point of having both — and the test says so directly rather than checking each in isolation. It sits hard against the right edge with the pane's state inboard of it, which is the order the strip above already uses for its own pair: the number is always there, the mode only sometimes, and a thing that comes and goes should not shove a thing that does not. Both counts now say "1 pane" rather than "1 panes". The top has been ungrammatical since it was written and a test had faithfully pinned it that way; adding a second place to say it made that worth fixing rather than copying. One of my own tests had to go: it asserted the bottom line contained no pane count at all, on the reasoning that the top already had one. That was right when it was written and is exactly what this change reverses, so it now checks the thing that is still true — the bottom line is not a second copy of the tab strip.
-rw-r--r--config/config.kdl5
-rw-r--r--src/app.c24
-rw-r--r--tests/test_config.py46
-rw-r--r--tests/test_finder.py2
4 files changed, 68 insertions, 9 deletions
diff --git a/config/config.kdl b/config/config.kdl
index 2f5109f..cc1cd7c 100644
--- a/config/config.kdl
+++ b/config/config.kdl
@@ -34,8 +34,9 @@ status_bar true
// The line along the bottom: which session and tab you are in, what the
// focused pane calls itself, and whether it is in a state worth knowing about
-// - scrolled back, on an alternate screen, or not started yet. The strip above
-// answers "which tab" and "how many panes", so this one does not repeat it.
+// - scrolled back, on an alternate screen, or not started yet. On the right it
+// counts the panes in *this tab*; the strip above counts the whole session, so
+// the two disagree whenever you have more than one tab.
status_line true
// Focus follows the pointer. It never steals focus mid-chord (while the prefix
diff --git a/src/app.c b/src/app.c
index 7daa7cc..c013ed2 100644
--- a/src/app.c
+++ b/src/app.c
@@ -270,6 +270,8 @@ size_t app_toast_count(app_t *a) {
return a->ntoasts;
}
+static size_t count_leaves(node_t *n); /* defined with the layout */
+
/* 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.
@@ -288,7 +290,24 @@ static void draw_status_line(app_t *a, screen_t *s) {
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. */
+ * the same budgeting rule the tab strip and the pane frame both use.
+ *
+ * The count goes hard against the edge with the state inboard of it, which
+ * is how the strip above orders its own pair: the number is always there,
+ * the mode only sometimes, and a thing that comes and goes should not move
+ * a thing that does not. This count is *this tab's*; the strip above counts
+ * the whole session, which is why they can disagree. */
+ if (a->ntabs && cur(a)->root) {
+ char cnt[32];
+ size_t np = count_leaves(cur(a)->root);
+ snprintf(cnt, sizeof cnt, "%zu pane%s", np, np == 1 ? "" : "s");
+ uint16_t cw = (uint16_t)strlen(cnt);
+ if (right > x + cw + 2) {
+ screen_text(s, (uint16_t)(right - cw), y, cnt, FRAME_IDLE, NO_COLOR, 0);
+ right = (uint16_t)(right - cw - 1);
+ }
+ }
+
char ind[64] = {0};
if (f) {
if (pane_suspended(f->pane)) {
@@ -1864,7 +1883,8 @@ static void draw_tab_strip(app_t *a, screen_t *s) {
* same budgeting rule as the split button and the OSC buttons. */
uint16_t right = (uint16_t)(s->cols - CFG.gap * CFG.gap_aspect);
char info[64];
- snprintf(info, sizeof info, "%zu panes", app_pane_count(a));
+ size_t np = app_pane_count(a);
+ snprintf(info, sizeof info, "%zu pane%s", np, np == 1 ? "" : "s");
uint16_t iw = (uint16_t)strlen(info);
if (right > iw + 4) {
screen_text(s, (uint16_t)(right - iw), y, info, FRAME_IDLE, NO_COLOR, 0);
diff --git a/tests/test_config.py b/tests/test_config.py
index 96e7b57..57c1acd 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -216,10 +216,11 @@ def test_status_line_says_what_you_are_looking_at():
bottom = s.snapshot().text[-2]
check("it names the tab and the focused pane",
"api" in bottom and "agent:main" in bottom, repr(bottom))
- # The top strip already answers "which tab" and "how many panes";
- # the bottom is about the pane, so it must not just repeat that.
- check("and does not repeat the pane count from the top strip",
- "panes" not in bottom, repr(bottom))
+ # The bottom carries a count too, but it is this tab's rather than the
+ # session's (see test_the_two_pane_counts_answer_different_questions).
+ # What it must not do is restate the strip itself.
+ check("and is not a second copy of the tab strip",
+ "+tab" not in bottom, repr(bottom))
ids = [p["id"] for p in s.panes()]
s.api("focus", id=ids[1])
@@ -248,6 +249,42 @@ def test_status_line_reports_scrollback():
repr(s.snapshot().text[-2]))
+def test_the_two_pane_counts_answer_different_questions():
+ """The strip above counts the session; the line below counts this tab."""
+ lay = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
+ lay.write('layout {\n tab name="api" {\n pane\n pane\n }\n'
+ ' tab name="notes" {\n pane\n }\n}\n')
+ lay.close()
+ with Session(SH, cols=80, rows=16, layout=lay.name) as s:
+ s.settle(20)
+ top, bottom = s.snapshot().line(1), s.snapshot().text[-2]
+ check("the strip counts every pane in the session",
+ "3 panes" in top, repr(top))
+ check("the line counts only this tab's",
+ "2 panes" in bottom, repr(bottom))
+
+ tabs = s.tabs()
+ s.api("select-tab", id=tabs[1]["id"])
+ s.settle(20)
+ top, bottom = s.snapshot().line(1), s.snapshot().text[-2]
+ check("the session count does not move with the tab",
+ "3 panes" in top, repr(top))
+ check("the tab count does", "1 pane" in bottom, repr(bottom))
+ check("and says pane, not panes, when there is one",
+ "1 panes" not in bottom, repr(bottom))
+
+ # The count sits hard against the edge; a state that comes and goes
+ # must not shove it around.
+ before = bottom.rstrip()
+ s.api("split", dir="cols")
+ s.settle(20)
+ check("adding a pane to the tab moves its count",
+ "2 panes" in s.snapshot().text[-2], repr(s.snapshot().text[-2]))
+ check("and the session count follows too",
+ "4 panes" in s.snapshot().line(1), repr(s.snapshot().line(1)))
+ os.unlink(lay.name)
+
+
if __name__ == "__main__":
test_geometry()
test_status_bar_off()
@@ -259,4 +296,5 @@ if __name__ == "__main__":
test_status_line_reserves_a_row_at_the_bottom()
test_status_line_says_what_you_are_looking_at()
test_status_line_reports_scrollback()
+ test_the_two_pane_counts_answer_different_questions()
sys.exit(report())
diff --git a/tests/test_finder.py b/tests/test_finder.py
index c84657c..cb421e9 100644
--- a/tests/test_finder.py
+++ b/tests/test_finder.py
@@ -16,7 +16,7 @@ def test_status_bar():
s.settle()
snap = s.snapshot()
bar = snap.line(1)
- check("the bar shows a pane count", "1 panes" in bar, repr(bar))
+ check("the bar shows a pane count", "1 pane" in bar, repr(bar))
check("the bar shows a new-tab button", "+tab" in bar, repr(bar))
x = bar.index("+tab")