aboutsummaryrefslogtreecommitdiffstats
path: root/src
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 /src
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.
Diffstat (limited to 'src')
-rw-r--r--src/app_draw.c2
-rw-r--r--src/app_layout.c89
-rw-r--r--src/config.c6
3 files changed, 80 insertions, 17 deletions
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)