aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-10-09 12:24:23 +0200
committeruser <user@clank>2026-10-09 12:31:53 +0200
commit3a0825c6890eff566307c0eaad809cdbcf854efd (patch)
treeadd5cc5d6e731bce8dcffe96fa92d64c84052143
parentpanes: the focus a closed tab left behind (diff)
downloadslosh-3a0825c6890eff566307c0eaad809cdbcf854efd.tar.gz
slosh-3a0825c6890eff566307c0eaad809cdbcf854efd.tar.xz
slosh-3a0825c6890eff566307c0eaad809cdbcf854efd.zip
tab bar: a sidebar that fits the terminal, and gives up the edge last
`tab_bar_width` was a reservation: the configured columns came off the screen whatever the screen was, and the only fallback was all the way back to the top row. On a narrow terminal that is chrome displacing the thing it is chrome for, and the jump to the top moves every target you were aiming at. It is a ceiling now. app_tab_bar_cols takes the app, because the answer depends on the terminal and not only on the config: held to the columns left after a pane's collapse width, and to a third of the screen, and narrowed rather than dropped. TAB_BAR_FIT (14) is the narrowest width this code will invent on its own -- a config that asks for less is taken at its word -- and 0 means there is no room for a sidebar at all, which is the one case that still falls back on top. Height counts too, which it did not before: a sidebar spends a row per tab, so a wide but short terminal runs out of list before it runs out of screen, and a tab with no row is not clipped, it is unreachable. tab_bar_min_rows is that floor. Not when the top strip could not seat them either -- moving them would gain nothing, so the side you configured stands. All of it is re-derived per frame, like the layout, so a resize moves the bar and nothing remembers that it did. test_tab_bar also stops depending on where the suite is run from. An unnamed tab borrows the basename of its focused pane's cwd, so eight checks read `tests` under `make test` (which cds there) and `slosh` from the repository root, where they failed. They are about padding, alignment and the index, not about that derivation, so they name the tab first -- by id, since `set-name` takes 0 for the focused *pane* only. 147 checks, green from either directory.
-rw-r--r--docs/panes.md22
-rw-r--r--docs/slosh.514
-rw-r--r--include/app_internal.h9
-rw-r--r--src/app_draw.c2
-rw-r--r--src/app_layout.c89
-rw-r--r--src/config.c6
-rw-r--r--tests/test_tab_bar.py131
7 files changed, 235 insertions, 38 deletions
diff --git a/docs/panes.md b/docs/panes.md
index 57e3014..6a4b586 100644
--- a/docs/panes.md
+++ b/docs/panes.md
@@ -54,7 +54,7 @@ The strip of tabs sits along the top by default. It does not have to:
```kdl
tab_bar_side "left" // or "right"; "top" is the default
-tab_bar_width 18 // columns the sidebar takes
+tab_bar_width 18 // columns the sidebar takes, at most
tab_bar_index "right" // the tab number at the far end; "prefix" is 1:name
tab_bar_padding 0 1 0 2 // air inside it, in cells: top right bottom left
tab_bar_pad 0 // ...or just the top of that, the older name
@@ -78,8 +78,24 @@ the strip runs out of room. Set `newtab_button false` to drop the button
altogether; `new-tab` is still a [chord](keys.md) and a palette entry, and a
pane can no longer be dropped onto it. The pane count and the prefix badge move
to the sidebar's bottom rows; the [status line](config.md) along the bottom
-keeps the full width. A terminal too narrow to give up the columns falls back
-to the top row until it grows.
+keeps the full width.
+
+**The bar fits itself to the terminal, and gives up the edge only when it has
+to.** `tab_bar_width` is a ceiling rather than a reservation: the sidebar is
+held to a third of the screen, so 18 columns is a margin beside a 90-column
+terminal and narrows to 16 beside a 50-column one, with the panes keeping the
+rest and never dropping below the width they collapse at. Only when even a
+legible sidebar (14 columns, or whatever narrower width you asked for) will not
+fit does the strip go back to the top row.
+
+Height counts too: a sidebar spends a
+row per tab, so a wide but short terminal runs out of list before it runs out
+of screen — and a tab with no row is not clipped, it is unreachable. When the
+rows for the labels and the `+` are not there, the strip goes back on top,
+where the same tabs lie along one row. Not when that row could not hold them
+either: with more tabs than the top strip can seat, moving them gains nothing
+and the side you configured stands. All of it is re-derived per frame from the
+terminal's size, so a resize moves the bar and nothing remembers it did.
**The sidebar is framed like a pane**, so it belongs to the chrome rather than
floating beside it. Under [`compact`](config.md#compact) the frame's inner line
diff --git a/docs/slosh.5 b/docs/slosh.5
index 3c1a43a..6226cf9 100644
--- a/docs/slosh.5
+++ b/docs/slosh.5
@@ -129,11 +129,17 @@ line's side margin.
.TP
.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, \fBtab_bar_width\fR
-columns wide, with \fBtab_bar_pad\fR blank rows above the first tab.
+\fBleft\fR or \fBright\fR listing one tab per row, at most
+\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.
+keeps the full width.
+The width is a ceiling, not a reservation: a sidebar narrows to fit a small
+screen \(em never more than a third of it, never into the columns a pane
+collapses at \(em and falls back to \fBtop\fR when what is left would be too
+narrow to read a name in, or when the terminal is too short to give every tab
+a row (unless the top strip could not seat them either, where nothing is
+gained by moving them). Re-derived per frame, so a resize moves the bar.
.TP
.BI tab_bar_padding\ n\ ...
Air inside a sidebar, between its frame and the list, per side and in
diff --git a/include/app_internal.h b/include/app_internal.h
index 40d169a..c6769d2 100644
--- a/include/app_internal.h
+++ b/include/app_internal.h
@@ -489,10 +489,13 @@ void walk_all(app_t *a, leaf_fn fn, void *ud);
uint16_t eff_gap(split_dir_t dir);
rect_t app_tab_area(app_t *a);
/* Where the tab bar lands this frame (TAB_BAR_*), and how many columns a
- * sidebar takes. The side is re-derived per frame: a screen too narrow for
- * the sidebar falls back to the top row. */
+ * sidebar takes. Both are re-derived per frame from the terminal's size: a
+ * sidebar narrows to fit a small screen, and one that can no longer seat
+ * either the panes or its own list of tabs falls back to the top row. The
+ * columns are 0 when there is no room for a sidebar at all; pass a NULL app
+ * to ask only what the config wants. */
int app_tab_bar_side(const app_t *a);
-uint16_t app_tab_bar_cols(void);
+uint16_t app_tab_bar_cols(const app_t *a);
void close_leaf(app_t *a, node_t *leaf);
node_t *first_leaf_of(node_t *n);
void focus_dir(app_t *a, int dx, int dy);
diff --git a/src/app_draw.c b/src/app_draw.c
index b3d8f07..7545c66 100644
--- a/src/app_draw.c
+++ b/src/app_draw.c
@@ -2231,7 +2231,7 @@ static size_t wrap_cells(const char *text, uint16_t width, size_t max,
void draw_tab_sidebar(app_t *a, screen_t *s) {
bool dragging_pane = a->drag.kind == DRAG_TITLE && a->drag.moved;
bool left = app_tab_bar_side(a) == TAB_BAR_LEFT;
- uint16_t sw = app_tab_bar_cols();
+ uint16_t sw = app_tab_bar_cols(a);
uint16_t x = left ? 0 : (uint16_t)(s->cols > sw ? s->cols - sw : 0);
uint16_t top = CFG.compact ? 0 : CFG.gap; /* the strip's own row rule */
uint16_t limit = (uint16_t)(s->rows > (CFG.status_line ? 1 : 0)
diff --git a/src/app_layout.c b/src/app_layout.c
index cf5b411..296727c 100644
--- a/src/app_layout.c
+++ b/src/app_layout.c
@@ -322,26 +322,87 @@ 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)
-/* The sidebar's width, held to a range where it is neither an unreadable
- * sliver nor half the screen. Clamped at use rather than at parse so a config
- * written for a big terminal degrades on a small one instead of erroring. */
-uint16_t app_tab_bar_cols(void) {
+/* The narrowest a sidebar is worth having, and the narrowest this code will
+ * shrink one to on its own. A config that asks for less than TAB_BAR_MIN is
+ * taken at its word -- someone who wrote `tab_bar_width 8` wants the sliver
+ * -- but a width nobody asked for is never invented below TAB_BAR_FIT, where
+ * " 12:name " stops fitting and the list is numbers and ellipses. */
+#define TAB_BAR_MIN 8
+#define TAB_BAR_FIT 14
+#define TAB_BAR_MAX 60
+
+/* The sidebar's width this frame: what the config asked for, held to a range
+ * where it is neither an unreadable sliver nor half the screen, and then to
+ * the room this terminal actually has. Clamped at use rather than at parse,
+ * so a config written for a big terminal degrades on a small one instead of
+ * erroring -- and it degrades by narrowing first, because a sidebar three
+ * columns thinner is still the list you navigate by, while one that jumps to
+ * the top row moves every target you were aiming at.
+ *
+ * Two ceilings, both about the panes rather than about the bar: the columns
+ * left after the width a pane collapses at (plus the gap either side), and a
+ * third of the screen, whatever that arithmetic says. The second is the one
+ * that does the work on a small terminal -- 18 columns is a margin next to a
+ * 100-column screen and a sidecar next to a 50-column one, and the bar is
+ * chrome for the panes either way.
+ *
+ * Zero when what is left is narrower than the bar is worth: the caller reads
+ * that as "no room for a sidebar at all" and puts the strip back on top. */
+uint16_t app_tab_bar_cols(const app_t *a) {
uint16_t w = CFG.tab_bar_width;
- if (w < 8) w = 8;
- if (w > 60) w = 60;
- return w;
+ if (w < TAB_BAR_MIN) w = TAB_BAR_MIN;
+ if (w > TAB_BAR_MAX) w = TAB_BAR_MAX;
+ if (!a) return w;
+
+ uint16_t spare = a->cols > (uint16_t)(CFG.min_pane_cols + 4)
+ ? (uint16_t)(a->cols - CFG.min_pane_cols - 4)
+ : 0;
+ uint16_t third = (uint16_t)(a->cols / 3);
+ if (spare > third) spare = third;
+
+ /* The floor to shrink against: never wider than what was configured, so a
+ * deliberately narrow bar is not "grown" into a fallback it never asked
+ * for, and never narrower than a legible one. */
+ uint16_t least = w < TAB_BAR_FIT ? w : TAB_BAR_FIT;
+ if (spare < least) return 0;
+ return w < spare ? w : spare;
+}
+
+/* The rows a sidebar needs before it starts hiding doors: its own top air,
+ * the frame's two lines, a row per tab, the `+`, and the status line's row
+ * underneath. Everything else in there -- the gaps, the pads, the statuses --
+ * already gives itself up a row at a time when the list is short of room, so
+ * it is the labels and the button that set the floor. */
+static uint16_t tab_bar_min_rows(const app_t *a) {
+ uint16_t need = (uint16_t)(CFG.compact ? 0 : CFG.gap);
+ if (CFG.tab_bar_chrome) need = (uint16_t)(need + 2);
+ if (CFG.newtab_button && CFG.newtab_mark[0]) need++;
+ if (CFG.status_line) need++;
+ return (uint16_t)(need + a->ntabs);
}
/* The side the strip actually lands on this frame. The configured side,
- * except that a screen too narrow to give up the sidebar's columns falls
- * back to the top row: a sidebar that leaves no room for a pane is chrome
- * displacing the thing it is chrome for. Re-derived per frame like the
- * layout itself, so a resize moves the bar without anyone storing state. */
+ * except on a terminal too small to seat it, where it falls back to the top
+ * row: a sidebar that leaves no room for the panes -- or no room for its own
+ * list -- is chrome displacing the thing it is chrome for. Re-derived per
+ * frame like the layout itself, so a resize moves the bar without anyone
+ * storing state.
+ *
+ * Too narrow is app_tab_bar_cols' answer, after it has tried shrinking. Too
+ * short is this function's: the sidebar spends a row per tab, so a wide but
+ * short terminal runs out of list before it runs out of screen, and the tabs
+ * that fall off the bottom are not clipped but unreachable. The strip lays
+ * the same tabs along one row -- but only while that row can hold them, so
+ * the fallback is conditional on the strip actually being the better seat.
+ * Eight columns is about what a tab cell costs at its most squeezed; if the
+ * tabs would not fit there either, nothing is gained by moving them and the
+ * configured side stands. */
int app_tab_bar_side(const app_t *a) {
int side = CFG.tab_bar_side;
if (side == TAB_BAR_TOP || !CFG.status_bar) return TAB_BAR_TOP;
- uint16_t sw = app_tab_bar_cols();
- if (a->cols < sw + CFG.min_pane_cols + 4) return TAB_BAR_TOP;
+ if (!app_tab_bar_cols(a)) return TAB_BAR_TOP;
+ if (a->rows < tab_bar_min_rows(a) && a->ntabs * 8 <= a->cols)
+ return TAB_BAR_TOP;
return side;
}
@@ -429,7 +490,7 @@ static rect_t tab_area(app_t *a) {
* way -- it keeps the full width (LINE_ROWS is subtracted from the height
* alone, never from the sidebar's rows: the sidebar stops above it). */
int side = app_tab_bar_side(a);
- uint16_t sw = side != TAB_BAR_TOP ? app_tab_bar_cols() : 0;
+ uint16_t sw = side != TAB_BAR_TOP ? app_tab_bar_cols(a) : 0;
uint16_t strip = side == TAB_BAR_TOP ? STRIP_ROWS : 0;
uint16_t top = (uint16_t)(gy + strip);
uint16_t lx = (uint16_t)(gx + (side == TAB_BAR_LEFT ? sw : 0));
diff --git a/src/config.c b/src/config.c
index 60eb711..b2f3243 100644
--- a/src/config.c
+++ b/src/config.c
@@ -997,7 +997,9 @@ void config_defaults(config_t *c) {
c->tab_bar_side = TAB_BAR_TOP;
c->tab_bar_index = TAB_INDEX_RIGHT;
/* Wide enough for " 12:a-real-name " plus a bell; narrow enough that a
- * 100-column terminal keeps a working layout beside it. */
+ * 100-column terminal keeps a working layout beside it. A ceiling rather
+ * than a reservation -- app_tab_bar_cols narrows it to fit a smaller
+ * screen, so this is the width you get when there is room for it. */
c->tab_bar_width = 18;
/* None by default: tab_bar_chrome's top line is the separation the air was
* standing in for, and a blank row inside the frame on top of it reads as
@@ -1771,7 +1773,7 @@ char *config_render(const config_t *c) {
"tab_bar_index \"%s\" // the sidebar's number at its far edge, or "
"1:name\n",
c->tab_bar_index == TAB_INDEX_RIGHT ? "right" : "prefix");
- cb_add(&b, "tab_bar_width %u // columns a sidebar takes\n",
+ cb_add(&b, "tab_bar_width %u // columns a sidebar takes, at most\n",
c->tab_bar_width);
if (c->tab_pad_top == c->tab_pad_right &&
c->tab_pad_top == c->tab_pad_bottom && c->tab_pad_top == c->tab_pad_left)
diff --git a/tests/test_tab_bar.py b/tests/test_tab_bar.py
index 6a1a0a5..af5e37e 100644
--- a/tests/test_tab_bar.py
+++ b/tests/test_tab_bar.py
@@ -40,6 +40,22 @@ NT_PAD = 1
SH = ["/bin/sh", "-c", "stty raw -echo; cat"]
+# An unnamed tab borrows the basename of its focused pane's cwd, so every row
+# below would otherwise read differently depending on where the suite was run
+# from -- `tests` under `make test`, `slosh` from the repository root. These
+# checks are about padding, alignment and the index, not about that
+# derivation, so they name the tab and assert against the name they gave it.
+TAB = "tests"
+
+
+def _named(s):
+ """Pin the label, so a row's contents do not depend on the cwd.
+
+ By id, because `set-name` takes 0 for the focused *pane* only -- a tab is
+ named by naming a tab.
+ """
+ s.api("set-name", id=s.tabs()[0]["id"], name=TAB)
+
def _row(snap, y):
"""The sidebar's content on a row, without the frame either side."""
@@ -171,6 +187,7 @@ def test_padding_is_the_air_inside_the_strip():
padded = _cfg(f'tab_bar_side "left"\ntab_bar_width {W}\ntab_bar_padding 1 2\n')
with Session(SH, cols=90, rows=20, config=padded) as s:
s.settle(30)
+ _named(s)
_status(s, "building")
snap = s.snapshot()
check(
@@ -179,7 +196,7 @@ def test_padding_is_the_air_inside_the_strip():
repr(_row(snap, Y0)),
)
label = _row(snap, Y0 + 1)
- check("the label is indented by two", label.startswith(" tests"), repr(label))
+ check("the label is indented by two", label.startswith(f" {TAB}"), repr(label))
check(
"...and the number is held off the other edge",
label.endswith("1 "),
@@ -187,7 +204,7 @@ def test_padding_is_the_air_inside_the_strip():
)
check(
"a status keeps the column its label starts in",
- _row(snap, Y0 + 2).index("building") == label.index("tests"),
+ _row(snap, Y0 + 2).index("building") == label.index(TAB),
repr(_row(snap, Y0 + 2)),
)
@@ -198,11 +215,12 @@ def test_padding_zero_puts_it_against_the_frame():
flush = _cfg(f'tab_bar_side "left"\ntab_bar_width {W}\ntab_bar_padding 0\n')
with Session(SH, cols=90, rows=20, config=flush) as s:
s.settle(30)
+ _named(s)
_status(s, "building")
snap = s.snapshot()
check(
"the name starts in the first column",
- _row(snap, Y0).startswith("tests"),
+ _row(snap, Y0).startswith(TAB),
repr(_row(snap, Y0)),
)
check(
@@ -245,10 +263,11 @@ def test_tab_bar_pad_is_still_the_top_of_it():
)
with Session(SH, cols=90, rows=20, config=both) as s:
s.settle(30)
+ _named(s)
snap = s.snapshot()
check(
"one row of air, not four",
- not _row(snap, Y0).strip() and "tests" in _row(snap, Y0 + 1),
+ not _row(snap, Y0).strip() and TAB in _row(snap, Y0 + 1),
f"{_row(snap, Y0)!r} / {_row(snap, Y0 + 1)!r}",
)
@@ -270,6 +289,7 @@ def test_the_index_sits_at_the_far_end():
it."""
with Session(SH, cols=90, rows=20, config=RIGHT_IDX) as s:
s.settle(30)
+ _named(s)
s.api("new-tab", name="api")
s.settle(30)
rows = [_row(snap := s.snapshot(), Y0), _row(snap, Y0 + 1)]
@@ -280,7 +300,7 @@ def test_the_index_sits_at_the_far_end():
)
check(
"...and every name starts in the same column",
- rows[1].index("api") == rows[0].index("tests"),
+ rows[1].index("api") == rows[0].index(TAB),
str(rows),
)
check("no prefix is left behind", "1:" not in rows[0], str(rows))
@@ -320,9 +340,10 @@ def test_the_top_strip_ignores_it():
top = _cfg('tab_bar_index "right"\n')
with Session(SH, cols=60, rows=10, config=top) as s:
s.settle(30)
+ _named(s)
check(
"the strip still writes 1:name",
- "1:tests" in s.snapshot().screen(),
+ f"1:{TAB}" in s.snapshot().screen(),
s.snapshot().line(1),
)
@@ -353,8 +374,13 @@ def test_the_prefix_is_still_there_for_anyone_who_wants_it():
before the alignment existed."""
with Session(SH, cols=90, rows=20, config=PREFIX_IDX) as s:
s.settle(30)
+ _named(s)
row = _row(s.snapshot(), Y0)
- check("the number is in front of the name", row.strip() == "1:tests", repr(row))
+ check(
+ "the number is in front of the name",
+ row.strip() == f"1:{TAB}",
+ repr(row),
+ )
def test_the_sidebar_aligns_by_default():
@@ -476,6 +502,7 @@ 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)
+ _named(s)
_status(s, "building 3/7")
snap = s.snapshot()
label, status = _row(snap, Y0), _row(snap, Y0 + 1)
@@ -483,8 +510,8 @@ def test_status_rows_are_told_apart_by_the_slant_not_by_an_indent():
# 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("tests"),
- f"{status.index('building')} vs {label.index('tests')}",
+ status.index("building") == label.index(TAB),
+ f"{status.index('building')} vs {label.index(TAB)}",
)
st = snap.style_at(CX + 1, Y0 + 1)
check("it is italic instead", "italic" in st["attrs"], str(st))
@@ -1271,9 +1298,88 @@ def test_the_strip_keeps_its_bare_mark():
)
+def test_a_small_screen_narrows_the_sidebar_first():
+ """Eighteen columns is a margin beside a ninety-column screen and a sidecar
+ beside a fifty-column one, so the width is a ceiling rather than a
+ reservation: the bar hands columns back before it hands back the edge,
+ keeping to a third of the screen so the panes always have the rest."""
+ with Session(SH, cols=50, rows=20, config=LEFT) as s:
+ s.settle(30)
+ snap = s.snapshot()
+ check(
+ "the sidebar kept its edge",
+ snap.hit_at(CX, Y0) == "tab:1",
+ str(snap.hit_at(CX, Y0)),
+ )
+ # A pane's x is its text, which starts after the gap, the bar, and the
+ # pane's own left border: the bar's width is that minus the two.
+ sw = s.pane(0)["x"] - 2
+ check("...by narrowing, not by moving", sw < W, str(sw))
+ check("no more than a third of the screen", sw <= 50 // 3, str(sw))
+ check("and still wide enough to read a name in", sw >= 14, str(sw))
+ s.resize(90, 20)
+ s.until(lambda _: s.pane(0)["x"] - 2 == W)
+ check(
+ "the room comes back and so does the width",
+ s.pane(0)["x"] - 2 == W,
+ str(s.pane(0)["x"] - 2),
+ )
+
+
+def test_a_short_terminal_falls_back_to_top():
+ """The sidebar spends a row per tab, so a wide but short terminal runs out
+ of list before it runs out of screen -- and a tab with no row is not
+ clipped, it is unreachable. The strip lays the same tabs along one row."""
+ with Session(SH, cols=90, rows=10, config=LEFT) as s:
+ s.settle(30)
+ _named(s)
+ check(
+ "one tab fits down the edge",
+ s.snapshot().hit_at(CX, Y0) == "tab:1",
+ str(s.snapshot().hit_at(CX, Y0)),
+ )
+ # Named, like the first one, because the last check needs all seven to
+ # fit *across* the strip: unnamed, they are labelled from the cwd, and
+ # a checkout called something longer than `slosh` pushes the seventh
+ # tab off the row and out of the hit map. The fallback itself is about
+ # rows, so the names do not affect what is being tested.
+ for i in range(6):
+ s.api("new-tab", name=f"t{i}")
+ s.settle(20)
+ s.until(lambda snap: snap.hit_at(5, 1) == "tab:1")
+ snap = s.snapshot()
+ check(
+ "seven of them do not, so the strip goes back on top",
+ snap.hit_at(5, 1) == "tab:1",
+ str(snap.hit_at(5, 1)),
+ )
+ targets = {h["action"] for h in snap.hits}
+ missing = [t["index"] for t in s.tabs() if f"tab:{t['index']}" not in targets]
+ check("and every tab is reachable again", not missing, str(missing))
+
+
+def test_a_strip_that_could_not_hold_them_either_changes_nothing():
+ """The fallback is only worth making while the top row is the better seat.
+ Tabs too many for one row are clipped there as surely as they are cut off
+ the bottom of a sidebar -- and the sidebar at least shows more of them --
+ so the configured side stands."""
+ with Session(SH, cols=60, rows=10, config=LEFT) as s:
+ s.settle(30)
+ for _ in range(9):
+ s.api("new-tab")
+ s.settle(20)
+ snap = s.snapshot()
+ check(
+ "the sidebar keeps the edge it was given",
+ snap.hit_at(CX, Y0) == "tab:1",
+ str(snap.hit_at(CX, Y0)),
+ )
+
+
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.
+ # 40 columns: a third of it is 13, narrower than a sidebar is worth, and
+ # what is left would not seat the pane the bar is chrome for either. So
+ # the strip goes back to the top row rather than shrink any further.
with Session(SH, cols=40, rows=20, config=LEFT) as s:
s.settle(30)
snap = s.snapshot()
@@ -1355,6 +1461,9 @@ if __name__ == "__main__":
test_wrapping_still_says_when_a_status_did_not_fit()
test_a_narrow_sidebar_keeps_the_bare_mark()
test_the_strip_keeps_its_bare_mark()
+ test_a_small_screen_narrows_the_sidebar_first()
+ test_a_short_terminal_falls_back_to_top()
+ test_a_strip_that_could_not_hold_them_either_changes_nothing()
test_narrow_terminal_falls_back_to_top()
test_growing_back_restores_the_sidebar()
sys.exit(report())