aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-10 15:41:16 +0200
committeruser <user@clank>2026-09-10 15:41:16 +0200
commit881c9b4a5c7352daf4861bff5902936e9746f9f5 (patch)
treed57b4eb8ceaf3675236c2c31924eb89d900d8dc7
parenttabs: the strip can stand on its side (tab_bar_side left/right) (diff)
downloadslosh-881c9b4a5c7352daf4861bff5902936e9746f9f5.tar.gz
slosh-881c9b4a5c7352daf4861bff5902936e9746f9f5.tar.xz
slosh-881c9b4a5c7352daf4861bff5902936e9746f9f5.zip
sidebar: a row of air on top (tab_bar_pad), indicators under the labels' inset
One blank row above the first tab by default: a list starting in the very corner reads 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. The pad is where the drawing starts, not a layout fact -- the panes' rect is identical at pad 0 and pad 5, so tuning it repaints without reflowing anyone. The bottom indicators (pane count, prefix badge) move in one cell to sit under the labels' own leading space; flush at the edge column they read as misaligned on the left and collide with the compact ring's corner on the right.
-rw-r--r--docs/slosh.55
-rw-r--r--include/config.h5
-rw-r--r--src/app_draw.c19
-rw-r--r--src/config.c9
-rw-r--r--tests/test_tab_bar.py57
5 files changed, 71 insertions, 24 deletions
diff --git a/docs/slosh.5 b/docs/slosh.5
index 0986934..c605ab4 100644
--- a/docs/slosh.5
+++ b/docs/slosh.5
@@ -127,9 +127,10 @@ a split is not offered in the first place.
The tab strip along the top, the status line along the bottom, and the
line's side margin.
.TP
-.BR tab_bar_side\ \fIside\fR ,\ tab_bar_width\ \fIn\fR
+.BR tab_bar_side\ \fIside\fR ,\ tab_bar_width\ \fIn\fR ,\ tab_bar_pad\ \fIn\fR
Where the tab strip lives: \fBtop\fR (the default), or a sidebar on the
-\fBleft\fR or \fBright\fR listing one tab per row, \fIn\fR columns wide.
+\fBleft\fR or \fBright\fR listing one tab per row, \fBtab_bar_width\fR
+columns wide, with \fBtab_bar_pad\fR blank rows above the first tab.
The strip's indicators move to the sidebar's bottom rows; the status line
keeps the full width. A terminal too narrow to give up the columns falls
back to \fBtop\fR until it grows.
diff --git a/include/config.h b/include/config.h
index 1392288..70596ec 100644
--- a/include/config.h
+++ b/include/config.h
@@ -321,6 +321,11 @@ typedef struct {
* untouched and keeps the full width either way. */
int tab_bar_side; /* TAB_BAR_* */
uint16_t tab_bar_width; /* columns a sidebar takes; ignored for top */
+ /* Rows a sidebar leaves blank above its first tab. Air for anyone who finds
+ * a list starting in the very corner cramped -- or room for a terminal's
+ * own window buttons when the multiplexer fills a borderless window.
+ * Ignored for top, which has exactly one row and no air to give. */
+ uint16_t tab_bar_pad;
bool status_line; /* the line along the bottom: what you are looking at */
/* How far the strip and the line are held off the left and right edges.
* Separate from `gap`, which is the space around the *panes*: the two happen
diff --git a/src/app_draw.c b/src/app_draw.c
index 372b2f5..2cf22d0 100644
--- a/src/app_draw.c
+++ b/src/app_draw.c
@@ -1960,25 +1960,32 @@ void draw_tab_sidebar(app_t *a, screen_t *s) {
uint16_t x = app_tab_bar_side(a) == TAB_BAR_LEFT
? 0
: (uint16_t)(s->cols > sw ? s->cols - sw : 0);
- uint16_t y = CFG.compact ? 0 : CFG.gap; /* same rule as the strip's row */
+ /* The strip's row rule, plus the configured air above the list. The pad is
+ * where the drawing starts, not a layout fact: the panes' rect is the same
+ * with pad 0 and pad 5, so tuning it repaints without reflowing anyone. */
+ uint16_t y = (uint16_t)((CFG.compact ? 0 : CFG.gap) + CFG.tab_bar_pad);
uint16_t limit = (uint16_t)(s->rows > (CFG.status_line ? 1 : 0)
? s->rows - (CFG.status_line ? 1 : 0)
: 0);
- /* Bottom rows first: what the horizontal strip keeps on its right. */
+ /* Bottom rows first: what the horizontal strip keeps on its right. Indented
+ * one cell to sit under the labels' own leading space -- at x exactly, the
+ * count reads flush against the screen edge on the left and against the
+ * compact ring's corner on the right. */
char info[64];
size_t np = app_pane_count(a);
snprintf(info, sizeof info, "%zu pane%s", np, np == 1 ? "" : "s");
- if (limit > y && (uint16_t)strlen(info) <= sw) {
+ if (limit > y && (uint16_t)(strlen(info) + 1) <= sw) {
limit--;
- screen_text(s, x, limit, info, TAB_COUNT, NO_COLOR, 0);
+ screen_text(s, (uint16_t)(x + 1), limit, info, TAB_COUNT, NO_COLOR, 0);
}
if (a->prefix) { /* the prefix is a mode: say so, and say which key */
char pfx[24];
config_chord_name(CFG.prefix_key, CFG.prefix_mods, pfx, sizeof pfx);
- if (limit > y && cells(pfx) <= sw) {
+ if (limit > y && (uint16_t)(cells(pfx) + 1) <= sw) {
limit--;
- screen_text(s, x, limit, pfx, PREFIX_FG, PREFIX_BG, ATTR_BOLD);
+ screen_text(s, (uint16_t)(x + 1), limit, pfx, PREFIX_FG, PREFIX_BG,
+ ATTR_BOLD);
}
}
diff --git a/src/config.c b/src/config.c
index 31ca0e1..d800696 100644
--- a/src/config.c
+++ b/src/config.c
@@ -923,6 +923,10 @@ void config_defaults(config_t *c) {
/* Wide enough for " 12:a-real-name " plus a bell; narrow enough that a
* 100-column terminal keeps a working layout beside it. */
c->tab_bar_width = 18;
+ /* One row of air by default: a list that starts in the very corner reads
+ * 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;
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
@@ -1625,6 +1629,8 @@ char *config_render(const config_t *c) {
: "top");
cb_add(&b, "tab_bar_width %u // columns a sidebar takes\n",
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, "status_line %s // the line along the bottom\n",
yesno(c->status_line));
cb_add(&b, "status_pad %u\n", c->status_pad);
@@ -1933,6 +1939,7 @@ static const char *const KNOWN_TOP[] = {
"status_bar",
"status_line",
"status_pad",
+ "tab_bar_pad",
"tab_bar_side",
"tab_bar_width",
"theme",
@@ -2145,6 +2152,8 @@ static bool load_into(config_t *c, const char *path, int depth, char *err,
}
c->tab_bar_width = (uint16_t)kdl_arg_int(kdl_child(root, "tab_bar_width"), 0,
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->status_line =
kdl_arg_bool(kdl_child(root, "status_line"), 0, c->status_line);
c->status_pad =
diff --git a/tests/test_tab_bar.py b/tests/test_tab_bar.py
index 5f466e1..842d421 100644
--- a/tests/test_tab_bar.py
+++ b/tests/test_tab_bar.py
@@ -33,21 +33,21 @@ def test_left_sidebar_reserves_columns():
with Session(SH, cols=90, rows=20, config=LEFT) as s:
s.settle(30)
snap = s.snapshot()
- # gap 1 (the default): the sidebar's first row is row 1, like the strip.
+ # gap 1 + tab_bar_pad 1 (the defaults): the first tab is row 2.
check(
"the first tab is a row in the sidebar",
- snap.hit_at(1, 1) == "tab:1",
- str(snap.hit_at(1, 1)),
+ snap.hit_at(1, 2) == "tab:1",
+ str(snap.hit_at(1, 2)),
)
check(
"the whole row is the target, not just the label",
- snap.hit_at(W - 1, 1) == "tab:1",
- str(snap.hit_at(W - 1, 1)),
+ snap.hit_at(W - 1, 2) == "tab:1",
+ str(snap.hit_at(W - 1, 2)),
)
check(
"the + sits on the row after the last tab",
- snap.hit_at(1, 2) == "newtab",
- str(snap.hit_at(1, 2)),
+ snap.hit_at(1, 3) == "newtab",
+ str(snap.hit_at(1, 3)),
)
p = s.pane(0)
check("panes give up the sidebar's columns", p["x"] >= W, str(p["x"]))
@@ -62,17 +62,17 @@ def test_left_sidebar_reserves_columns():
def test_sidebar_rows_click_like_the_strip():
with Session(SH, cols=90, rows=20, config=LEFT) as s:
s.settle(30)
- s.click(1, 2) # the + row: a new tab, which becomes the active one
+ s.click(1, 3) # the + row: a new tab, which becomes the active one
s.until(lambda _: len(s.tabs()) == 2)
check("the + row makes a tab", len(s.tabs()) == 2, str(len(s.tabs())))
check("...and it is the one you are in", s.tabs()[1]["active"], str(s.tabs()))
snap = s.snapshot()
check(
"the new tab is the next row down",
- snap.hit_at(1, 2) == "tab:2",
- str(snap.hit_at(1, 2)),
+ snap.hit_at(1, 3) == "tab:2",
+ str(snap.hit_at(1, 3)),
)
- s.click(1, 1) # the first tab's row
+ s.click(1, 2) # the first tab's row
s.until(lambda _: s.tabs()[0]["active"])
check("clicking a row selects that tab", s.tabs()[0]["active"], str(s.tabs()))
@@ -84,8 +84,8 @@ def test_right_sidebar_takes_the_other_edge():
x0 = 90 - W
check(
"the first tab is a row on the right edge",
- snap.hit_at(x0 + 1, 1) == "tab:1",
- str(snap.hit_at(x0 + 1, 1)),
+ snap.hit_at(x0 + 1, 2) == "tab:1",
+ str(snap.hit_at(x0 + 1, 2)),
)
p = s.pane(0)
check(
@@ -95,6 +95,30 @@ def test_right_sidebar_takes_the_other_edge():
)
+def test_pad_pushes_the_list_down():
+ pad = _cfg(f'tab_bar_side "left"\ntab_bar_width {W}\ntab_bar_pad 3\n')
+ with Session(SH, cols=90, rows=20, config=pad) as s:
+ s.settle(30)
+ snap = s.snapshot()
+ # gap 1 + pad 3: the first tab is on row 4, and the rows above are air.
+ check(
+ "the pad rows are not targets",
+ snap.hit_at(1, 1) is None and snap.hit_at(1, 3) is None,
+ f"{snap.hit_at(1, 1)} / {snap.hit_at(1, 3)}",
+ )
+ check(
+ "the first tab starts below the pad",
+ snap.hit_at(1, 4) == "tab:1",
+ str(snap.hit_at(1, 4)),
+ )
+ p = s.pane(0)
+ check(
+ "the pad is paint, not layout: panes keep their rows",
+ p["y"] <= 2,
+ str(p["y"]),
+ )
+
+
def test_narrow_terminal_falls_back_to_top():
# 40 < width + min_pane cols + 4: the sidebar would leave no room for the
# pane it is chrome for, so the strip goes back to the top row.
@@ -114,11 +138,11 @@ def test_growing_back_restores_the_sidebar():
with Session(SH, cols=40, rows=20, config=LEFT) as s:
s.settle(30)
s.resize(90, 20)
- s.until(lambda snap: snap.hit_at(1, 1) == "tab:1")
+ s.until(lambda snap: snap.hit_at(1, 2) == "tab:1")
check(
"a resize moves the bar without anyone storing state",
- s.snapshot().hit_at(1, 1) == "tab:1",
- str(s.snapshot().hit_at(1, 1)),
+ s.snapshot().hit_at(1, 2) == "tab:1",
+ str(s.snapshot().hit_at(1, 2)),
)
p = s.pane(0)
check("and the panes give the columns back", p["x"] >= W, str(p["x"]))
@@ -128,6 +152,7 @@ if __name__ == "__main__":
test_left_sidebar_reserves_columns()
test_sidebar_rows_click_like_the_strip()
test_right_sidebar_takes_the_other_edge()
+ test_pad_pushes_the_list_down()
test_narrow_terminal_falls_back_to_top()
test_growing_back_restores_the_sidebar()
sys.exit(report())