aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-10 16:32:10 +0200
committeruser <user@clank>2026-09-10 16:32:10 +0200
commita2d08f4876226446c0052f033e8afea2f2ffbbf1 (patch)
treeac77189b9c928939202adca4a43298730bccd8d2
parentsidebar: the new-tab button says what it is, given a row to say it on (diff)
downloadslosh-a2d08f4876226446c0052f033e8afea2f2ffbbf1.tar.gz
slosh-a2d08f4876226446c0052f033e8afea2f2ffbbf1.tar.xz
slosh-a2d08f4876226446c0052f033e8afea2f2ffbbf1.zip
sidebar: statuses wear italic instead of an indent, and can wear colour
Three columns of indent is a fifth of a sixteen-column sidebar, spent saying 'this belongs to the row above'. The slant says it in none: a status now starts in the same column its tab's name starts in, and is set in italic. That also survives the monochrome themes in contrib, where a colour-only distinction would have had nothing to spend. tab_status_fg / tab_status_bg are there for themes that do have colour to spend. The fg follows tab_count when a theme does not name it -- the same declared/derived split apply_scrolled makes -- so the six themes in contrib get coherent status rows without a line added to any of them. The bg is unset by default, because a band is opt-in and 'none' is what a translucent terminal wants; the row is padded so a band that *is* set covers the row rather than the words on it. Two things fell out. The padding loop existed three times and is now pad_cells. And cb_color wrote #000000 for an unset colour, so quietly pinned attach_bg -- 'gray ink on the terminal's own background', says its comment -- to a black band. Unset colours are now written commented out, the same trick the states block uses for what it wants re-derived rather than pinned.
-rw-r--r--contrib/themes/default.kdl6
-rw-r--r--docs/panes.md6
-rw-r--r--include/app_internal.h2
-rw-r--r--include/config.h6
-rw-r--r--src/app_draw.c50
-rw-r--r--src/config.c23
-rw-r--r--tests/test_dump_config.py11
-rw-r--r--tests/test_tab_bar.py57
-rw-r--r--web/index.html9
9 files changed, 145 insertions, 25 deletions
diff --git a/contrib/themes/default.kdl b/contrib/themes/default.kdl
index 0cf881e..7c1fa23 100644
--- a/contrib/themes/default.kdl
+++ b/contrib/themes/default.kdl
@@ -38,7 +38,7 @@ theme {
header_hover "#7aa2f7"
header_hover_title "#ffffff"
- // the strip along the top
+ // the strip along the top, or the sidebar down the side
tab_active_fg "#141418"
tab_active_bg "#7aa2f7"
tab_active_hover_fg "#ffffff"
@@ -47,6 +47,10 @@ theme {
prefix_fg "#141418"
prefix_bg "#7aa2f7"
tab_count "#45454a"
+ // a sidebar's status rows: italic carries the distinction, so this only
+ // has to be ambient. Unset, it follows tab_count. tab_status_bg is unset
+ // -- a band is opt-in, and none leaves a translucent terminal alone.
+ tab_status_fg "#45454a"
// the line along the bottom
status "#45454a"
diff --git a/docs/panes.md b/docs/panes.md
index ab9bd08..3100b78 100644
--- a/docs/panes.md
+++ b/docs/panes.md
@@ -81,7 +81,11 @@ point — and a pane that died shows how instead (`exited: status 3`). At most
`tab_bar_status` rows per tab (3 by default, 0 turns it off); more than that
and the last row is an ellipsis rather than a status pretending the list is
complete. **Clicking a status row jumps to the pane that said it**, across
-tabs. The top strip has one row and no room for any of this, which is half of
+tabs. A status is set in italic rather than indented under its tab — the slant
+says "this belongs to the row above" without spending columns a narrow sidebar
+has none of, and says it on a monochrome theme too. `tab_status_fg` and
+`tab_status_bg` are yours if you want colour as well; unset, the first follows
+`tab_count` and the second leaves your terminal's own background alone. The top strip has one row and no room for any of this, which is half of
why the sidebar exists.
**An unnamed tab borrows the directory its focused pane is in** — the
diff --git a/include/app_internal.h b/include/app_internal.h
index 11f96b4..94a92cf 100644
--- a/include/app_internal.h
+++ b/include/app_internal.h
@@ -392,6 +392,8 @@ extern config_t CFG;
#define PREFIX_FG (CFG.prefix_fg)
#define PREFIX_BG (CFG.prefix_bg)
#define TAB_COUNT (CFG.tab_count)
+#define TAB_STATUS_FG (CFG.tab_status_fg)
+#define TAB_STATUS_BG (CFG.tab_status_bg)
#define STATUS_C (CFG.status)
#define STATUS_STATE (CFG.status_state)
#define FINDER_FG (CFG.finder_fg)
diff --git a/include/config.h b/include/config.h
index 1849014..f7caeba 100644
--- a/include/config.h
+++ b/include/config.h
@@ -438,6 +438,12 @@ typedef struct {
color_t tab_idle, tab_hover;
color_t prefix_fg, prefix_bg; /* the "C-a" badge */
color_t tab_count; /* how many panes in the session */
+ /* The sidebar's status rows. Their distinction from the labels above them
+ * is carried by the slant (see draw_tab_sidebar), because a monochrome
+ * theme has no colour to spend on it; these are for themes that do. A bg of
+ * none leaves the terminal's own background alone, which is what a
+ * translucent one wants. */
+ color_t tab_status_fg, tab_status_bg;
/* the line along the bottom */
color_t status, status_state;
diff --git a/src/app_draw.c b/src/app_draw.c
index 54b8950..3b329bf 100644
--- a/src/app_draw.c
+++ b/src/app_draw.c
@@ -1783,6 +1783,17 @@ static bool tab_has_bell(tab_t *t) {
return b.found;
}
+/* Spaces out to `w` cells, so a row's fill -- an active tab's, a drop
+ * target's, a themed status band's -- is the row and not the word sitting on
+ * it. Three copies of this loop wanted to exist; this is the one. */
+static void pad_cells(char *text, size_t cap, uint16_t w) {
+ size_t len = strlen(text);
+ while (cells(text) < w && len + 1 < cap) {
+ text[len++] = ' ';
+ text[len] = 0;
+ }
+}
+
/* One tab's cell, wherever the strip lives. Paints the label at (x, y) under
* the shared rules -- active weight, hover colour, the rename editor, the
* bell, the drop target -- and registers the hit. `max_w` is 0 for the top
@@ -1847,11 +1858,7 @@ static uint16_t draw_tab_cell(app_t *a, screen_t *s, size_t i, uint16_t x,
while (len && (label[len] & 0xc0) == 0x80); /* whole UTF-8 sequences */
label[len] = 0;
}
- size_t len = strlen(label);
- while (cells(label) < budget && len + 1 < sizeof label) {
- label[len++] = ' ';
- label[len] = 0;
- }
+ pad_cells(label, sizeof label, budget);
}
bool active = i == a->cur;
@@ -1930,13 +1937,7 @@ static void draw_newtab_button(app_t *a, screen_t *s, uint16_t x, uint16_t y,
snprintf(btn, sizeof btn, " %s ", CFG.newtab_mark);
/* Padded out on a sidebar row for the same reason a tab's label is: the
* hover and the drop fill are the row, not the word sitting on it. */
- if (max_w) {
- size_t len = strlen(btn);
- while (cells(btn) < max_w && len + 1 < sizeof btn) {
- btn[len++] = ' ';
- btn[len] = 0;
- }
- }
+ if (max_w) pad_cells(btn, sizeof btn, max_w);
uint16_t w = screen_text(s, x, y, btn, TAB_IDLE, NO_COLOR, 0);
uint16_t hit_w = max_w ? max_w : w;
if (ptr_on(a, x, y, hit_w, 1))
@@ -2126,18 +2127,29 @@ void draw_tab_sidebar(app_t *a, screen_t *s) {
* one more status pretending the list is complete. */
bool more = ts.total > cap && j == (size_t)(cap - 1);
char text[112];
+ /* One leading space, which is the label's own -- a status starts in the
+ * column its tab's name starts in, rather than indented under it.
+ *
+ * It used to be three. Indentation is a fine way to say "this belongs
+ * to the row above" and a poor way to spend a sixteen-column sidebar,
+ * where three columns is a fifth of everything a status has to say
+ * itself in. The slant below says the same thing in none of them. */
if (more) {
- snprintf(text, sizeof text, " \u2026");
+ snprintf(text, sizeof text, " \u2026");
} else {
- snprintf(text, sizeof text, " %s", ts.row[j].text);
+ snprintf(text, sizeof text, " %s", ts.row[j].text);
fit_status(text, sizeof text, cw);
}
- /* 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. */
+ pad_cells(text, sizeof text, cw); /* so a themed band covers the row */
+ /* Italic, and ambient in colour: these are annotations until pointed
+ * at. The slant is what separates them from the labels, rather than a
+ * colour, because a monochrome theme has no colour to spare and 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. */
bool hot = !more && ptr_on(a, cx, y, cw, 1);
- screen_text(s, cx, y, text, hot ? TAB_HOVER : TAB_COUNT, NO_COLOR,
- hot ? ATTR_BOLD : 0);
+ screen_text(s, cx, y, text, hot ? TAB_HOVER : TAB_STATUS_FG,
+ TAB_STATUS_BG, hot ? ATTR_BOLD : ATTR_ITALIC);
if (!more) {
char act[24];
snprintf(act, sizeof act, "find:%u", ts.row[j].pane);
diff --git a/src/config.c b/src/config.c
index cea9177..88b686f 100644
--- a/src/config.c
+++ b/src/config.c
@@ -640,6 +640,8 @@ static const struct {
{"prefix_fg", offsetof(config_t, prefix_fg)},
{"prefix_bg", offsetof(config_t, prefix_bg)},
{"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)},
{"status", offsetof(config_t, status)},
{"status_state", offsetof(config_t, status_state)},
{"finder_fg", offsetof(config_t, finder_fg)},
@@ -1031,6 +1033,10 @@ void config_defaults(config_t *c) {
c->prefix_fg = ink;
c->prefix_bg = accent;
c->tab_count = dim;
+ /* Ambient, like the count: the slant is what separates a status from the
+ * label above it, so the colour does not have to. */
+ c->tab_status_fg = dim;
+ c->tab_status_bg = (color_t){0}; /* none: the terminal's own background */
c->status = dim;
c->status_state = bright;
@@ -1526,6 +1532,16 @@ static void cb_chord(cfgbuf_t *b, int key, uint16_t mods) {
}
static void cb_color(cfgbuf_t *b, const char *name, color_t c) {
+ /* A colour that is not set is the terminal's own, and that has no spelling
+ * in a theme block -- parse_color takes #rrggbb and nothing else. So it is
+ * written commented out: the same trick the states block uses for what it
+ * wants re-derived rather than pinned. The dump still says the key exists,
+ * and `slosh --dump-config > config.kdl` does not quietly turn "none" into
+ * black, which is what writing #000000 here would have done. */
+ if (!c.set) {
+ cb_add(b, " // %-19s (none: the terminal's own)\n", name);
+ return;
+ }
cb_add(b, " %-22s \"#%02x%02x%02x\"\n", name, c.r, c.g, c.b);
}
@@ -2402,6 +2418,13 @@ static bool load_into(config_t *c, const char *path, int depth, char *err,
complain(c, err, errcap, kdl_child(theme, THEME_COLORS[i].name)->line,
"bad colour for %s: %s", THEME_COLORS[i].name, v);
}
+ /* A theme that restyles the ambient chrome but has never heard of the
+ * sidebar's status rows gets coherent ones anyway: they follow tab_count,
+ * which is the same class of thing. Named explicitly, it wins -- the same
+ * declared/derived split apply_scrolled makes. Without this every theme in
+ * contrib would need a line adding to it to avoid one grey among another
+ * theme's greys. */
+ if (!kdl_child(theme, "tab_status_fg")) c->tab_status_fg = c->tab_count;
}
/* 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_dump_config.py b/tests/test_dump_config.py
index d6b3fd4..67679a1 100644
--- a/tests/test_dump_config.py
+++ b/tests/test_dump_config.py
@@ -106,7 +106,16 @@ def test_every_theme_colour_is_in_it():
src = open(SRC).read()
colours = set(re.findall(r'\{"([a-z_]+)", offsetof\(config_t', src))
text = dump()
- missing = sorted(c for c in colours if f" {c} " not in text)
+ # A colour that is *unset* is the terminal's own, and a theme block has no
+ # spelling for that -- parse_color takes #rrggbb and nothing else. Those
+ # are written commented out, so the dump still names the key while loading
+ # it back does not pin "none" to black. Same tolerance the settings check
+ # above already has.
+ missing = sorted(
+ c
+ for c in colours
+ if not re.search(r"^\s*(// )?%s[ \"]" % re.escape(c), text, re.M)
+ )
check(
f"all {len(colours)} colours are rendered",
not missing,
diff --git a/tests/test_tab_bar.py b/tests/test_tab_bar.py
index ce3ad9b..3fa3154 100644
--- a/tests/test_tab_bar.py
+++ b/tests/test_tab_bar.py
@@ -171,6 +171,60 @@ def test_status_rows_sit_under_their_tab():
)
+def test_status_rows_are_told_apart_by_the_slant_not_by_an_indent():
+ with Session(SH, cols=90, rows=20, config=LEFT) as s:
+ s.settle(30)
+ _status(s, "building 3/7")
+ snap = s.snapshot()
+ label, status = _row(snap, Y0), _row(snap, Y0 + 1)
+ # One leading space each: a status starts in the column its tab's name
+ # starts in. Three columns of indent is a fifth of this sidebar.
+ check(
+ "a status is not indented under its label",
+ status.index("building") == label.index("1:"),
+ f"{status.index('building')} vs {label.index('1:')}",
+ )
+ st = snap.style_at(CX + 1, Y0 + 1)
+ check("it is italic instead", "italic" in st["attrs"], str(st))
+ check(
+ "...and the label above it is not",
+ "italic" not in snap.style_at(CX + 1, Y0)["attrs"],
+ str(snap.style_at(CX + 1, Y0)),
+ )
+
+
+def test_status_rows_take_their_own_theme_colours():
+ themed = _cfg(
+ f'tab_bar_side "left"\ntab_bar_width {W}\n'
+ 'theme { tab_status_fg "#00ff88"\n tab_status_bg "#202030" }\n'
+ )
+ with Session(SH, cols=90, rows=20, config=themed) as s:
+ s.settle(30)
+ _status(s, "building")
+ snap = s.snapshot()
+ st = snap.style_at(CX + 1, Y0 + 1)
+ check("the fg is the themed one", st["fg"] == "#00ff88", str(st))
+ end = snap.style_at(CX + CW - 1, Y0 + 1)
+ check(
+ "and the band covers the row, not just the words",
+ end and end["bg"] == "#202030",
+ str(end),
+ )
+
+
+def test_an_old_theme_gets_coherent_status_colours_anyway():
+ """A theme written before these rows existed names tab_count and nothing
+ about them; deriving from it beats one stock grey among its own greys."""
+ old = _cfg(
+ f'tab_bar_side "left"\ntab_bar_width {W}\ntheme {{ tab_count "#ff8800" }}\n'
+ )
+ with Session(SH, cols=90, rows=20, config=old) as s:
+ s.settle(30)
+ _status(s, "building")
+ st = s.snapshot().style_at(CX + 1, Y0 + 1)
+ check("the status follows tab_count", st["fg"] == "#ff8800", str(st))
+
+
def test_status_cap_spends_its_last_row_on_the_ellipsis():
capped = _cfg(f'tab_bar_side "left"\ntab_bar_width {W}\ntab_bar_status 1\n')
with Session(SH, cols=90, rows=20, config=capped) as s:
@@ -380,6 +434,9 @@ if __name__ == "__main__":
test_right_sidebar_takes_the_other_edge()
test_pad_pushes_the_list_down()
test_status_rows_sit_under_their_tab()
+ test_status_rows_are_told_apart_by_the_slant_not_by_an_indent()
+ test_status_rows_take_their_own_theme_colours()
+ test_an_old_theme_gets_coherent_status_colours_anyway()
test_status_cap_spends_its_last_row_on_the_ellipsis()
test_a_long_status_is_cut_and_says_so()
test_a_dead_pane_reports_its_exit_instead()
diff --git a/web/index.html b/web/index.html
index 9dacd72..e9ac8ea 100644
--- a/web/index.html
+++ b/web/index.html
@@ -237,12 +237,15 @@ Server = https://slosh.foo/arch/$arch</code></pre>
<div class="cmd">
<pre><code><span class="prompt">$</span> git clone https://github.com/sl0p-foo/slosh.git &amp;&amp; cd slosh
<span class="prompt">$</span> make vendor <span class="c"># the vendored terminal core, once</span>
-<span class="prompt">$</span> make <span class="c"># about a second</span>
-<span class="prompt">$</span> sudo install build/slosh /usr/local/bin/</code></pre>
+<span class="prompt">$</span> make all <span class="c"># about a second</span>
+<span class="prompt">$</span> sudo make install <span class="c"># or: make install PREFIX=$HOME/.local</span></code></pre>
</div>
<p class="aside">Needs <a href="https://ziglang.org">zig</a> 0.16 and
nothing else: no cmake, no autotools, no libraries. The result is one
- static binary. Windows cross-compilation is
+ static binary. Bare <code>make</code> prints the target list;
+ <code>make&nbsp;all</code> is the build, and <code>make&nbsp;install</code>
+ puts the binary in <code>$PREFIX/bin</code> along with the manpages and
+ the example configuration. Windows cross-compilation is
<code>make&nbsp;-f&nbsp;Makefile.windows</code>;
<a href="docs/windows.html">the details</a>.</p>
</div>