aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-22 15:38:08 +0200
committeruser <user@clank>2026-09-22 15:38:08 +0200
commit1cbfb4474ca31c61254be8d5f3ec046967bd1ff3 (patch)
tree3e8184674e330ce24ebef23dd13594fa38f45fa2
parentsidebar: room for the spinner, by default (tab_bar_padding 0 1 0 2) (diff)
downloadslosh-1cbfb4474ca31c61254be8d5f3ec046967bd1ff3.tar.gz
slosh-1cbfb4474ca31c61254be8d5f3ec046967bd1ff3.tar.xz
slosh-1cbfb4474ca31c61254be8d5f3ec046967bd1ff3.zip
theme: the busy spinner is its own colour (tab_status_spinner)
It was drawn in tab_status_busy, the colour of the words it sits beside, which made one decision out of two. The words have to stay readable at a glance and a mark does not: a theme may well want the glyph loud and the text calm -- or the text in the accent and the mark a quiet dot -- and with one entry neither is available without moving both. Unset it follows tab_status_busy, derived after it and from whatever that resolved to, named or derived, so nothing changes for anyone who does not ask and no theme in contrib needs a line. `--dump-theme` writes both, like every other colour the compositor draws.
-rw-r--r--config/config.kdl4
-rw-r--r--docs/panes.md4
-rw-r--r--docs/scripting.md3
-rw-r--r--docs/slosh.57
-rw-r--r--include/app_internal.h1
-rw-r--r--include/config.h6
-rw-r--r--src/app_draw.c4
-rw-r--r--src/config.c6
-rw-r--r--tests/test_tab_bar.py60
-rw-r--r--tests/test_themes.py8
10 files changed, 95 insertions, 8 deletions
diff --git a/config/config.kdl b/config/config.kdl
index 2a1fdac..b46f64d 100644
--- a/config/config.kdl
+++ b/config/config.kdl
@@ -282,7 +282,9 @@ bell_mark "[!]"
// A pane can also say that what its status describes is still *happening*
// (`busy` over OSC 5577, see the scripting chapter). In a sidebar that status
-// then turns the theme's `tab_status_busy` colour and carries this spinner,
+// then turns the theme's `tab_status_busy` colour and carries this spinner --
+// in `tab_status_spinner`, its own entry, so a theme can shout with the mark
+// and keep the words calm; unset it follows the words --
// which is how "running" and "finished" tell themselves apart from another tab
// without reading the words.
//
diff --git a/docs/panes.md b/docs/panes.md
index d760b82..57e3014 100644
--- a/docs/panes.md
+++ b/docs/panes.md
@@ -100,7 +100,9 @@ be able to cost you a tab you can click.
**A pane can say its status is still happening**, and the sidebar says so back:
`busy` over OSC 5577 turns that row `tab_status_busy` and puts a spinner
-(`busy_mark`) in the first column of the indent — so `make test` running and
+(`busy_mark`, in `tab_status_spinner` — its own theme entry, because the words
+have to stay readable and a mark does not, and unset it follows the words) in
+the first column of the indent — so `make test` running and
`make test` finished, which are the same words, stop looking the same from
another tab. Nothing moves when it flips, since the mark takes a column the
padding was already holding blank, and the rest of that padding is the space
diff --git a/docs/scripting.md b/docs/scripting.md
index 9971bc5..8fff400 100644
--- a/docs/scripting.md
+++ b/docs/scripting.md
@@ -115,7 +115,8 @@ keep it current and `clear` it when the work is done.
of its own rather than a field of `status`, because a status is the *whole*
payload after the verb — `status;a;b;c` is the text `a;b;c` — so anything added
to that line would change what every sender already means. In a sidebar a busy
-status gets the spinner (`busy_mark`) and `tab_status_busy`, which is how
+status gets the spinner (`busy_mark`, coloured `tab_status_spinner`) and
+`tab_status_busy`, which is how
"working" and "done" tell themselves apart from another tab without reading the
words. Anything but `0`, `false`, `no` or `off` turns it on; `clear` turns it
off along with the text, and so does the program exiting — a status left behind
diff --git a/docs/slosh.5 b/docs/slosh.5
index 62205cc..3c1a43a 100644
--- a/docs/slosh.5
+++ b/docs/slosh.5
@@ -224,7 +224,12 @@ is the spinner's frames separated by spaces; one frame is a static mark and
costs nothing, several make it turn, and the session keeps a frame clock only
while such a row is on screen. Empty draws no mark and leaves
.B tab_status_busy
-to say it. The mark sits in the first column
+to say it.
+The mark is drawn in
+.BR tab_status_spinner ,
+which follows
+.B tab_status_busy
+unless a theme names it. The mark sits in the first column
.B tab_bar_padding
holds blank, so nothing moves when a program starts or stops working, and
the rest of that padding is the space after it.
diff --git a/include/app_internal.h b/include/app_internal.h
index 1af9163..c699be8 100644
--- a/include/app_internal.h
+++ b/include/app_internal.h
@@ -413,6 +413,7 @@ extern config_t CFG;
#define TAB_STATUS_FG (CFG.tab_status_fg)
#define TAB_STATUS_BG (CFG.tab_status_bg)
#define TAB_STATUS_BUSY (CFG.tab_status_busy)
+#define TAB_STATUS_SPINNER (CFG.tab_status_spinner)
#define TAB_STATUS_STRIPE (CFG.tab_status_stripe)
#define STATUS_C (CFG.status)
#define STATUS_STATE (CFG.status_state)
diff --git a/include/config.h b/include/config.h
index 40a5ace..e5a3ca5 100644
--- a/include/config.h
+++ b/include/config.h
@@ -547,6 +547,12 @@ typedef struct {
* not be something you have to read the words to work out. Derived from the
* theme, like tab_status_fg. */
color_t tab_status_busy;
+ /* The spinner beside such a row, which is its own entry because it is its
+ * own decision: the text has to stay readable, and a mark does not -- so a
+ * theme can shout with the glyph and keep the words calm, or the other way
+ * round. Unset it follows tab_status_busy, which is what it looked like
+ * when the two were one colour. */
+ color_t tab_status_spinner;
/* The other stripe. With wrapped statuses, where one pane's rows end and
* the next one's begin is no longer obvious from the shape -- both are
* just italic text down the same column -- so alternate panes sit on
diff --git a/src/app_draw.c b/src/app_draw.c
index 5676240..b3d8f07 100644
--- a/src/app_draw.c
+++ b/src/app_draw.c
@@ -2469,8 +2469,8 @@ void draw_tab_sidebar(app_t *a, screen_t *s) {
* one column they stand shoulder to shoulder, which is the squashed
* look and now something you ask for rather than something you get. */
if (marked && k == 0)
- screen_text(s, cx, y, spin, hot ? TAB_HOVER : TAB_STATUS_BUSY, band,
- 0);
+ screen_text(s, cx, y, spin, hot ? TAB_HOVER : TAB_STATUS_SPINNER,
+ 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 012964e..60eb711 100644
--- a/src/config.c
+++ b/src/config.c
@@ -697,6 +697,7 @@ static const struct {
{"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_spinner", offsetof(config_t, tab_status_spinner)},
{"tab_status_stripe", offsetof(config_t, tab_status_stripe)},
{"status", offsetof(config_t, status)},
{"status_state", offsetof(config_t, status_state)},
@@ -1143,6 +1144,7 @@ void config_defaults(config_t *c) {
* 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;
+ c->tab_status_spinner = 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. */
@@ -2967,6 +2969,10 @@ static bool load_into(config_t *c, const char *path, int depth, char *err,
/* 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 it, and from it: the mark follows the words it marks unless a
+ * theme says otherwise, whether those words were named or derived. */
+ if (!kdl_child(theme, "tab_status_spinner"))
+ c->tab_status_spinner = c->tab_status_busy;
}
/* 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
diff --git a/tests/test_tab_bar.py b/tests/test_tab_bar.py
index ad523ad..6a1a0a5 100644
--- a/tests/test_tab_bar.py
+++ b/tests/test_tab_bar.py
@@ -648,8 +648,64 @@ def test_the_busy_colour_is_the_themes_own():
_status(s, "make test")
_busy(s)
s.settle(30)
- st = s.snapshot().style_at(CX + 1, Y0 + 1)
+ snap = s.snapshot()
+ st = snap.style_at(CX + 2, Y0 + 1)
check("named, it wins", st["fg"] == "#ff00ff", str(st))
+ check(
+ "and the mark follows the words it marks",
+ (snap.style_at(CX, Y0 + 1) or {}).get("fg") == "#ff00ff",
+ str(snap.style_at(CX, Y0 + 1)),
+ )
+
+
+def test_the_spinner_can_be_coloured_on_its_own():
+ """Its own entry because it is its own decision: the words have to stay
+ readable and a mark does not, so a theme can shout with the glyph and keep
+ the text calm."""
+ themed = _cfg(
+ f'tab_bar_side "left"\ntab_bar_width {W}\n'
+ 'theme { tab_status_busy "#8888ff"\n tab_status_spinner "#ffcc00" }\n'
+ )
+ with Session(SH, cols=90, rows=20, config=themed) as s:
+ s.settle(30)
+ _status(s, "make test")
+ _busy(s)
+ s.settle(30)
+ snap = s.snapshot()
+ check(
+ "the mark takes its own colour",
+ (snap.style_at(CX, Y0 + 1) or {}).get("fg") == "#ffcc00",
+ str(snap.style_at(CX, Y0 + 1)),
+ )
+ check(
+ "...and the words keep theirs",
+ (snap.style_at(CX + 2, Y0 + 1) or {}).get("fg") == "#8888ff",
+ str(snap.style_at(CX + 2, Y0 + 1)),
+ )
+
+
+def test_an_old_theme_gets_a_spinner_colour_anyway():
+ """Neither colour is in any theme in contrib, so both are mixed from what a
+ theme does define -- the same bargain the other status colours make."""
+ old = _cfg(
+ f'tab_bar_side "left"\ntab_bar_width {W}\ntheme {{ tab_hover "#00ff88" }}\n'
+ )
+ with Session(SH, cols=90, rows=20, config=old) as s:
+ s.settle(30)
+ _status(s, "make test")
+ _busy(s)
+ s.settle(30)
+ snap = s.snapshot()
+ check(
+ "the row is the theme's accent",
+ (snap.style_at(CX + 2, Y0 + 1) or {}).get("fg") == "#00ff88",
+ str(snap.style_at(CX + 2, Y0 + 1)),
+ )
+ check(
+ "...and so is the mark",
+ (snap.style_at(CX, Y0 + 1) or {}).get("fg") == "#00ff88",
+ str(snap.style_at(CX, Y0 + 1)),
+ )
def test_status_rows_take_their_own_theme_colours():
@@ -1272,6 +1328,8 @@ if __name__ == "__main__":
test_a_spinner_costs_a_frame_clock_and_a_mark_does_not()
test_no_busy_mark_leaves_the_colour_to_say_it()
test_the_busy_colour_is_the_themes_own()
+ test_the_spinner_can_be_coloured_on_its_own()
+ test_an_old_theme_gets_a_spinner_colour_anyway()
test_status_rows_take_their_own_theme_colours()
test_an_old_theme_gets_coherent_status_colours_anyway()
test_a_monochrome_theme_stays_monochrome()
diff --git a/tests/test_themes.py b/tests/test_themes.py
index d9c67b0..e9f5495 100644
--- a/tests/test_themes.py
+++ b/tests/test_themes.py
@@ -26,7 +26,13 @@ THEMES = sorted(glob.glob(os.path.join(THEMEDIR, "*.kdl")))
# from colours the theme does define (so a theme gets them in its own family
# without a line), and the opt-in ones are unset on purpose, because a band
# behind a row takes a translucent terminal's own background away.
-DERIVED = {"tab_status_fg", "tab_status_stripe", "tab_status_busy", "tab_idle_bg"}
+DERIVED = {
+ "tab_status_fg",
+ "tab_status_stripe",
+ "tab_status_busy",
+ "tab_status_spinner",
+ "tab_idle_bg",
+}
OPT_IN = {"tab_status_bg", "attach_bg"}
# The scrollback search tints: one theme in contrib names them and the rest
# take the compiled-in pair, which reads the same on every palette here.