aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-14 19:48:30 +0200
committeruser <user@clank>2026-08-14 19:48:30 +0200
commite021e0d05926fbaae2b53edaf256fdd7b31aff1d (patch)
treee841ac08f4b5a241d4013fcfd0a6207eb78e4f43
parentsl0ppty: zoom a pane, and give the frame buttons for it (diff)
downloadslosh-e021e0d05926fbaae2b53edaf256fdd7b31aff1d.tar.gz
slosh-e021e0d05926fbaae2b53edaf256fdd7b31aff1d.tar.xz
slosh-e021e0d05926fbaae2b53edaf256fdd7b31aff1d.zip
sl0ppty: minimise a pane into a strip along the bottom
A pane can be put away: it leaves the layout, becomes one row at the bottom of the tab, and keeps running. The panes it leaves behind take the space back. Clicking its row brings it back. Minimised is intent, so it sits beside the weight on the node rather than with the rect and the collapsed flag — and layout_node has to be careful not to clear it the way it clears those, or every frame would undo it. That is the only sharp edge in the whole thing and it has a comment. Everything else fell out of rules that already existed. A split divides among the children that still want space, so a split with one live child is that child, exactly as a split with one child already was. A minimised leaf returns from layout_node before its rect is touched, which also skips the floor check — a pane that is one row by request is not a pane that failed to fit. And the row itself is drawn by the same code as a collapsed header, because it is the same thing: a pane you can see the name of and click. Restoring is not a verb. A focused pane is never minimised, checked once a frame, so every route that ends with the pane focused restores it — clicking the row, the control API, and anything added later that moves focus without knowing minimising exists. In a tab that has flattened into a list, minimising has no effect at all, which is what was asked for and also what falls out: every pane down there is already a row, so there is no second thing for it to be. The strip is not reserved when it would leave too little above it to be worth looking at; the tab flattens instead, and arrives at the same place. Minimising the last visible pane is refused with a toast. A tab showing nothing has no way back that can be discovered from the tab, and it is not a state worth being able to reach by accident. The frame's third button costs another three columns, which is why they are drawn right to left and stop when they run out: a narrow pane keeps close and zoom, and loses minimise first.
-rw-r--r--README.md6
-rw-r--r--config/config.kdl10
-rw-r--r--src/app.c119
-rw-r--r--src/app.h5
-rw-r--r--src/config.c7
-rw-r--r--src/config.h3
-rw-r--r--tests/test_minimize.py189
7 files changed, 327 insertions, 12 deletions
diff --git a/README.md b/README.md
index ab548b5..51b51c5 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,12 @@ fills the tab until you put it back; the other panes keep running, they are
just not drawn. `×` beside it closes the pane. `pane_buttons false` takes both
buttons off without touching the keybindings.
+**Minimise a pane** with `C-a m` or the `–` button and it leaves the layout for
+a strip along the bottom, one row each, still running — the panes it leaves
+behind take the space back. Click its row to bring it back. Once a tab is too
+small and has flattened into a list, being minimised means nothing: every pane
+is a row down there already.
+
**Select to copy.** Drag over text and release: it is on your clipboard, sent
to your terminal as OSC 52 so it works over ssh. Middle click pastes it back,
the way a primary selection behaves everywhere else. A program in a pane can
diff --git a/config/config.kdl b/config/config.kdl
index 3597549..39e185c 100644
--- a/config/config.kdl
+++ b/config/config.kdl
@@ -40,8 +40,10 @@ title_inset 2
// real choice rather than an oversight.
// ── the frame's own buttons ─────────────────────────────────────────────────
//
-// Zoom and close, in the top-right of every pane's frame. Three columns each,
-// because a one-column target is a thing you miss with a mouse.
+// Minimise, zoom and close, in the top-right of every pane's frame. Three
+// columns each, because a one-column target is a thing you miss with a mouse.
+// They are drawn right to left and stop when they run out of room, so a narrow
+// pane keeps close and zoom and loses minimise first.
//
// The split button was removed from this frame once already, on the grounds
// that one glyph cannot mean four directions and the border can. These are two
@@ -57,6 +59,7 @@ pane_buttons true
zoom_mark "□"
zoom_on_mark "▣"
close_mark "×"
+min_mark "–"
bell_indicator true
@@ -288,7 +291,7 @@ theme {
// Actions: split-cols split-rows close-pane focus-left focus-right focus-up
// focus-down focus-next resize-left resize-right resize-up resize-down
// scroll-up scroll-down scroll-page-up scroll-page-down scroll-top
-// scroll-bottom zoom new-tab next-tab prev-tab select-tab-1..9 finder detach quit
+// scroll-bottom zoom minimize new-tab next-tab prev-tab select-tab-1..9 finder detach quit
// literal-prefix none
keys {
@@ -298,6 +301,7 @@ keys {
bind "-" "split-rows"
bind "x" "close-pane"
bind "z" "zoom"
+ bind "m" "minimize"
bind "h" "focus-left"
bind "l" "focus-right"
diff --git a/src/app.c b/src/app.c
index 284245c..c2b556e 100644
--- a/src/app.c
+++ b/src/app.c
@@ -109,6 +109,12 @@ struct node {
* equal weights, so resizing is not a special case of anything. */
int weight;
+ /* Put away: out of the layout, into a strip along the bottom, still running.
+ * Intent, like the weight beside it and unlike the rect and the collapsed
+ * flag below — layout_node must not clear this, or it would un-minimise
+ * every pane on every frame. */
+ bool minimized;
+
/* leaf */
pane_t *pane;
uint32_t id;
@@ -669,6 +675,26 @@ static size_t collect_leaves(node_t *n, node_t **out, size_t cap, size_t k) {
return k;
}
+/* Does anything under here still want space in the layout? */
+static bool subtree_live(node_t *n) {
+ if (!n) return false;
+ if (n->kind == NODE_LEAF) return !n->minimized;
+ for (size_t i = 0; i < n->nkids; i++)
+ if (subtree_live(n->kids[i])) return true;
+ return false;
+}
+
+static size_t collect_minimized(node_t *n, node_t **out, size_t cap, size_t k) {
+ if (!n || k >= cap) return k;
+ if (n->kind == NODE_LEAF) {
+ if (n->minimized) out[k++] = n;
+ return k;
+ }
+ for (size_t i = 0; i < n->nkids; i++)
+ k = collect_minimized(n->kids[i], out, cap, k);
+ return k;
+}
+
static size_t count_leaves(node_t *n) {
if (!n) return 0;
if (n->kind == NODE_LEAF) return 1;
@@ -780,6 +806,12 @@ static void layout_solo(node_t *n, rect_t r, layout_ctx_t *ctx) {
}
static void layout_node(node_t *n, rect_t r, layout_ctx_t *ctx) {
+ /* Minimised panes are placed by the strip, not by the tree. Returning before
+ * touching the rect leaves the strip's placement standing, and returning
+ * before the floor check is right too: a pane that is one row by request is
+ * not a pane that failed to fit. */
+ if (n->kind == NODE_LEAF && n->minimized) return;
+
n->rect = r;
n->collapsed = false;
if (n->kind == NODE_LEAF) n->hidden = false;
@@ -807,7 +839,18 @@ static void layout_node(node_t *n, rect_t r, layout_ctx_t *ctx) {
return;
}
- size_t k = n->nkids;
+ /* Divide among the children that still want space. A split with one live
+ * child is that child, exactly as a split with one child would be. */
+ node_t *live[64];
+ size_t k = 0;
+ for (size_t i = 0; i < n->nkids && k < 64; i++)
+ if (subtree_live(n->kids[i])) live[k++] = n->kids[i];
+ if (k == 0) return;
+ if (k == 1) {
+ layout_node(live[0], r, ctx);
+ return;
+ }
+
uint16_t gap = n->dir == SPLIT_COLS ? (uint16_t)(CFG.gap * CFG.gap_aspect)
: CFG.gap;
uint16_t total = n->dir == SPLIT_COLS ? r.w : r.h;
@@ -835,13 +878,13 @@ static void layout_node(node_t *n, rect_t r, layout_ctx_t *ctx) {
/* Sizes are proportional to weights; the remainder goes to the widest
* children, largest first, so nothing drifts and nothing rounds to zero. */
long total_weight = 0;
- for (size_t i = 0; i < k; i++) total_weight += n->kids[i]->weight;
+ for (size_t i = 0; i < k; i++) total_weight += live[i]->weight;
if (total_weight <= 0) total_weight = 1;
uint16_t sizes[64];
uint16_t used = 0;
for (size_t i = 0; i < k && i < 64; i++) {
- long want = (long)avail * n->kids[i]->weight / total_weight;
+ long want = (long)avail * live[i]->weight / total_weight;
if (want < 1) want = 1;
sizes[i] = (uint16_t)want;
used = (uint16_t)(used + sizes[i]);
@@ -863,7 +906,7 @@ static void layout_node(node_t *n, rect_t r, layout_ctx_t *ctx) {
rect_t cr = n->dir == SPLIT_COLS
? (rect_t){.x = pos, .y = r.y, .w = size, .h = r.h}
: (rect_t){.x = r.x, .y = pos, .w = r.w, .h = size};
- layout_node(n->kids[i], cr, ctx);
+ layout_node(live[i], cr, ctx);
pos = (uint16_t)(pos + size + gap);
}
}
@@ -887,6 +930,12 @@ static void layout(app_t *a) {
node_t *root = cur(a)->root;
if (!root) return;
+ /* Focusing a minimised pane is how you get it back, and it is checked here
+ * rather than in the several places focus can move: any route that ends with
+ * this pane focused restores it, including ones that do not exist yet. */
+ if (cur(a)->focus && cur(a)->focus->minimized)
+ cur(a)->focus->minimized = false;
+
/* A zoomed pane is the whole tab. Reuses the solo path rather than adding a
* third arrangement: "show this one and nothing else" is a thing the layout
* already knew how to do, it just used to be a last resort rather than a
@@ -912,13 +961,35 @@ static void layout(app_t *a) {
* layout with resizing switched off, rather than a second implementation of
* the same arithmetic — two copies of "does this fit" is exactly how the
* guide and the layout would start disagreeing. */
+ /* Minimised panes come out of the layout and sit in a strip along the
+ * bottom, one row each. The tree is then laid out in what is left, so a
+ * minimised pane costs a row rather than a share. */
+ node_t *mins[64];
+ size_t nmin = collect_minimized(root, mins, 64, 0);
+ rect_t tree_r = r;
+ if (nmin) {
+ /* If the strip would leave too little to be worth looking at, do not
+ * reserve it: the tab flattens instead, and down there a minimised pane is
+ * just another row, which is what it would have become anyway. */
+ if ((uint16_t)(r.h - nmin) >= (uint16_t)(MIN_PANE_ROWS + 2))
+ tree_r.h = (uint16_t)(r.h - nmin);
+ else
+ nmin = 0;
+ }
+
layout_ctx_t probe = {.focus = cur(a)->focus, .apply = false};
- layout_node(root, r, &probe);
+ layout_node(root, tree_r, &probe);
layout_ctx_t real = {.focus = cur(a)->focus, .apply = true};
if (!probe.overflow) {
- layout_node(root, r, &real);
- } else if (r.h >= (uint16_t)(count_leaves(root) + 2)) {
+ layout_node(root, tree_r, &real);
+ /* The strip, under the layout it was taken out of. */
+ for (size_t i = 0; i < nmin; i++)
+ collapse_leaf(mins[i], (rect_t){r.x, (uint16_t)(tree_r.y + tree_r.h + i),
+ r.w, 1});
+ return;
+ }
+ if (r.h >= (uint16_t)(count_leaves(root) + 2)) {
/* One row per pane, plus a body worth opening into: a frame is two rows. */
layout_stack(root, r, &real);
} else {
@@ -1203,6 +1274,34 @@ static node_t *pane_by_id(app_t *a, uint32_t id) {
return b.found;
}
+bool app_minimize(app_t *a, uint32_t id) {
+ node_t *n = id ? pane_by_id(a, id) : cur(a)->focus;
+ if (!n || n->minimized) return false;
+ size_t ti = tab_of(a, n);
+ if (ti == (size_t)-1) return false;
+
+ /* Refused if it would leave nothing on screen. A tab showing no panes at all
+ * has no way back that is discoverable from the tab, and "everything is put
+ * away" is not a state worth being able to reach by accident. */
+ node_t *next = NULL;
+ node_t *leaves[64];
+ size_t nl = collect_leaves(a->tabs[ti].root, leaves, 64, 0);
+ for (size_t i = 0; i < nl; i++)
+ if (leaves[i] != n && !leaves[i]->minimized) {
+ next = leaves[i];
+ break;
+ }
+ if (!next) return false;
+
+ n->minimized = true;
+ a->cur = ti;
+ /* Focus has to leave, or the rule that a focused pane is never minimised
+ * would undo this on the next frame. */
+ if (a->tabs[ti].focus == n) a->tabs[ti].focus = next;
+ layout(a);
+ return true;
+}
+
bool app_toggle_zoom(app_t *a, uint32_t id) {
node_t *n = id ? pane_by_id(a, id) : cur(a)->focus;
if (!n) return false;
@@ -1736,6 +1835,7 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) {
{CFG.close_mark, "close"},
{app_pane_zoomed(a, leaf->id) ? CFG.zoom_on_mark : CFG.zoom_mark,
"zoom"},
+ {CFG.min_mark, "minimize"},
};
uint16_t bx = (uint16_t)(x1 - 1);
for (size_t i = 0; i < sizeof btns / sizeof *btns; i++) {
@@ -2695,6 +2795,8 @@ static void do_action(app_t *a, const char *action, const input_event_t *ev) {
if (strncmp(action, "split:", 6) == 0) {
cur(a)->focus = n;
split_focus(a, SPLIT_COLS);
+ } else if (strncmp(action, "minimize:", 9) == 0) {
+ if (!app_minimize(a, n->id)) app_toast(a, "nothing else to show");
} else if (strncmp(action, "zoom:", 5) == 0) {
app_toggle_zoom(a, n->id);
} else if (strncmp(action, "close:", 6) == 0) {
@@ -2768,6 +2870,9 @@ static bool prefix_command(app_t *a, const input_event_t *ev) {
case ACT_SPLIT_COLS: split_focus_ui(a, SPLIT_COLS); return true;
case ACT_SPLIT_ROWS: split_focus_ui(a, SPLIT_ROWS); return true;
case ACT_ZOOM: app_toggle_zoom(a, 0); return true;
+ case ACT_MINIMIZE:
+ if (!app_minimize(a, 0)) app_toast(a, "nothing else to show");
+ return true;
case ACT_CLOSE_PANE:
if (cur(a)->focus) close_leaf(a, cur(a)->focus);
return true;
diff --git a/src/app.h b/src/app.h
index 58b2c41..7106305 100644
--- a/src/app.h
+++ b/src/app.h
@@ -87,6 +87,11 @@ bool app_apply_layout_file(app_t *a, const char *path, bool replace, char *err,
* because it opened the socket under that name. */
void app_set_session(app_t *a, const char *name);
+/* Put a pane away into the strip along the bottom. 0 means the focused one.
+ * False if it would leave the tab with nothing on screen. Restoring is done by
+ * focusing it, which is what clicking its row does. */
+bool app_minimize(app_t *a, uint32_t id);
+
/* Fill the tab with one pane, or put it back. 0 means the focused one. */
bool app_toggle_zoom(app_t *a, uint32_t id);
bool app_pane_zoomed(app_t *a, uint32_t id);
diff --git a/src/config.c b/src/config.c
index 1cc76af..bc82d7b 100644
--- a/src/config.c
+++ b/src/config.c
@@ -15,7 +15,8 @@ static const struct {
} ACTIONS[] = {
{"split-cols", ACT_SPLIT_COLS}, {"split-rows", ACT_SPLIT_ROWS},
{"close-pane", ACT_CLOSE_PANE},
- {"zoom", ACT_ZOOM}, {"focus-left", ACT_FOCUS_LEFT},
+ {"zoom", ACT_ZOOM},
+ {"minimize", ACT_MINIMIZE}, {"focus-left", ACT_FOCUS_LEFT},
{"focus-right", ACT_FOCUS_RIGHT}, {"focus-up", ACT_FOCUS_UP},
{"focus-down", ACT_FOCUS_DOWN}, {"focus-next", ACT_FOCUS_NEXT},
{"resize-left", ACT_RESIZE_LEFT}, {"resize-right", ACT_RESIZE_RIGHT},
@@ -174,6 +175,7 @@ void config_defaults(config_t *c) {
snprintf(c->zoom_mark, sizeof c->zoom_mark, "\u25a1");
snprintf(c->zoom_on_mark, sizeof c->zoom_on_mark, "\u25a3");
snprintf(c->close_mark, sizeof c->close_mark, "\u00d7");
+ snprintf(c->min_mark, sizeof c->min_mark, "\u2013");
c->bell_indicator = true;
snprintf(c->bell_mark, sizeof c->bell_mark, "\u2022");
c->min_pane_cols = 24;
@@ -270,6 +272,7 @@ void config_defaults(config_t *c) {
bind_add(c, GHOSTTY_KEY_MINUS, 0, ACT_SPLIT_ROWS);
bind_add(c, GHOSTTY_KEY_X, 0, ACT_CLOSE_PANE);
bind_add(c, GHOSTTY_KEY_Z, 0, ACT_ZOOM);
+ bind_add(c, GHOSTTY_KEY_M, 0, ACT_MINIMIZE);
bind_add(c, GHOSTTY_KEY_H, 0, ACT_FOCUS_LEFT);
bind_add(c, GHOSTTY_KEY_L, 0, ACT_FOCUS_RIGHT);
bind_add(c, GHOSTTY_KEY_K, 0, ACT_FOCUS_UP);
@@ -378,6 +381,8 @@ bool config_load(config_t *c, const char *path, char *err, size_t errcap) {
if (zo) snprintf(c->zoom_on_mark, sizeof c->zoom_on_mark, "%s", zo);
const char *cm = kdl_arg(kdl_child(root, "close_mark"), 0, NULL);
if (cm) snprintf(c->close_mark, sizeof c->close_mark, "%s", cm);
+ const char *mm = kdl_arg(kdl_child(root, "min_mark"), 0, NULL);
+ if (mm) snprintf(c->min_mark, sizeof c->min_mark, "%s", mm);
c->bell_indicator =
kdl_arg_bool(kdl_child(root, "bell_indicator"), 0, c->bell_indicator);
const char *bm = kdl_arg(kdl_child(root, "bell_mark"), 0, NULL);
diff --git a/src/config.h b/src/config.h
index 86a57a6..bbd9e0c 100644
--- a/src/config.h
+++ b/src/config.h
@@ -37,6 +37,7 @@ typedef enum {
ACT_SPLIT_ROWS,
ACT_CLOSE_PANE,
ACT_ZOOM,
+ ACT_MINIMIZE,
ACT_FOCUS_LEFT,
ACT_FOCUS_RIGHT,
ACT_FOCUS_UP,
@@ -84,7 +85,7 @@ typedef struct {
/* Buttons in the top-right of a pane's frame: zoom, and close. Off leaves
* the keyboard bindings alone; it is about the affordance, not the verb. */
bool pane_buttons;
- char zoom_mark[16], zoom_on_mark[16], close_mark[16];
+ char zoom_mark[16], zoom_on_mark[16], close_mark[16], min_mark[16];
bool bell_indicator;
/* What to draw. A short string rather than a fixed glyph: the obvious
* choice is an emoji bell, and emoji are drawn two columns wide by many
diff --git a/tests/test_minimize.py b/tests/test_minimize.py
new file mode 100644
index 0000000..9582ca2
--- /dev/null
+++ b/tests/test_minimize.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+"""Putting a pane away.
+
+A minimised pane leaves the layout and sits in a strip along the bottom, one
+row each, still running. It is intent rather than a derived state, so the
+things worth checking are the same ones zoom needed: that it survives the
+layout being recomputed, that there is always a way back, and that it does not
+invent a state with nothing on screen.
+"""
+import os
+import sys
+import tempfile
+
+from harness import Session, check, report
+
+SH = ["/bin/sh", "-c", 'printf "\\033]2;p\\007"; stty raw -echo; cat']
+
+
+def cfg(text):
+ f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
+ f.write(text)
+ f.close()
+ return f.name
+
+
+def here(s):
+ """Panes of the current tab; another tab's are not laid out."""
+ active = [t["index"] for t in s.tabs() if t["active"]][0]
+ return [p for p in s.panes() if p["tab"] == active]
+
+
+def strip(s):
+ return [p for p in here(s) if p["h"] == 1]
+
+
+def open_panes(s):
+ return [p for p in here(s) if p["h"] > 1]
+
+
+def test_minimising_moves_a_pane_to_the_strip():
+ with Session(SH, cols=90, rows=20) as s:
+ s.settle(20)
+ s.api("split", dir="cols")
+ s.settle(20)
+ before = sorted(p["w"] for p in open_panes(s))
+
+ s.send(r"\x01m")
+ s.settle(20)
+ check("one pane is now a single row", len(strip(s)) == 1,
+ str(here(s)))
+ check("and the other still has the tab", len(open_panes(s)) == 1,
+ str(here(s)))
+ check("which is wider than it was, having taken the space back",
+ open_panes(s)[0]["w"] > max(before), str(here(s)))
+
+ row = strip(s)[0]
+ rest = open_panes(s)[0]
+ check("the strip sits under the layout, not in it",
+ row["y"] > rest["y"] + rest["h"] - 1, f"{row} vs {rest}")
+ check("and spans the width", row["w"] == rest["w"], str(here(s)))
+
+
+def test_clicking_the_row_brings_it_back():
+ with Session(SH, cols=90, rows=20) as s:
+ s.settle(20)
+ s.api("split", dir="cols")
+ s.settle(20)
+ layout_before = sorted((p["x"], p["w"]) for p in open_panes(s))
+
+ s.send(r"\x01m")
+ s.settle(20)
+ row = strip(s)[0]
+ s.click(row["x"] + 4, row["y"])
+ s.settle(20)
+ check("clicking the row restores the pane", strip(s) == [], str(here(s)))
+ check("and the layout is the one it left",
+ sorted((p["x"], p["w"]) for p in open_panes(s)) == layout_before,
+ str(here(s)))
+
+
+def test_focusing_it_by_any_route_brings_it_back():
+ """Restoring is not a special verb: a focused pane is never minimised, and
+ that is checked once a frame rather than at each place focus can move."""
+ with Session(SH, cols=90, rows=20) as s:
+ s.settle(20)
+ s.api("split", dir="cols")
+ s.settle(20)
+ s.send(r"\x01m")
+ s.settle(20)
+ gone = strip(s)[0]["id"]
+
+ s.api("focus", id=gone)
+ s.settle(20)
+ check("focusing it over the control API restores it",
+ strip(s) == [], str(here(s)))
+
+
+def test_it_will_not_leave_the_tab_empty():
+ with Session(SH, cols=90, rows=20) as s:
+ s.settle(20)
+ check("one pane to start", len(here(s)) == 1, str(here(s)))
+ s.send(r"\x01m")
+ s.settle(20)
+ check("minimising the only pane is refused", strip(s) == [],
+ str(here(s)))
+ check("and it says why", "nothing else" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-200:]))
+
+ s.api("split", dir="cols")
+ s.settle(20)
+ s.send(r"\x01m")
+ s.settle(20)
+ check("with two, one can go", len(strip(s)) == 1, str(here(s)))
+ s.send(r"\x01m")
+ s.settle(20)
+ check("but the last one standing cannot", len(strip(s)) == 1,
+ str(here(s)))
+
+
+def test_the_button_minimises_that_pane():
+ with Session(SH, cols=90, rows=20) as s:
+ s.settle(20)
+ s.api("split", dir="cols")
+ s.settle(20)
+ target = here(s)[0]["id"]
+ b = [h for h in s.snapshot().hits
+ if h["action"] == f"minimize:{target}"]
+ check("a pane's frame carries a minimise button", len(b) == 1, str(b))
+ if not b:
+ return
+ s.click(b[0]["x"] + 1, b[0]["y"])
+ s.settle(20)
+ check("clicking it puts that pane away",
+ [p["id"] for p in strip(s)] == [target], str(here(s)))
+
+
+def test_it_has_no_say_once_the_tab_is_a_list():
+ """Down there every pane is a row already, so being minimised is not a
+ different thing to be -- which is the whole reason it costs nothing."""
+ with Session(SH, cols=90, rows=20) as s:
+ s.settle(20)
+ s.api("split", dir="cols")
+ s.settle(20)
+ s.send(r"\x01m")
+ s.settle(20)
+ check("one minimised, one open", len(strip(s)) == 1, str(here(s)))
+
+ s.resize(26, 20) # narrow enough that the tab flattens
+ s.settle(20)
+ rows = here(s)
+ check("the tab is a list", any(p["hidden"] for p in rows), str(rows))
+ check("and the minimised pane is just another row in it",
+ len([p for p in rows if p["h"] == 1]) == len(rows) - 1,
+ str(rows))
+
+ s.resize(90, 20)
+ s.settle(20)
+ check("coming back, it is minimised again and not lost",
+ len(strip(s)) == 1 and len(open_panes(s)) == 1, str(here(s)))
+
+
+def test_zoom_hides_the_strip():
+ with Session(SH, cols=90, rows=20) as s:
+ s.settle(20)
+ s.api("split", dir="cols")
+ s.api("split", dir="rows")
+ s.settle(20)
+ s.send(r"\x01m")
+ s.settle(20)
+ check("a strip to start", len(strip(s)) == 1, str(here(s)))
+ s.send(r"\x01z")
+ s.settle(20)
+ check("zooming shows one pane and nothing else, strip included",
+ len([p for p in here(s) if p["w"] > 1]) == 1, str(here(s)))
+ s.send(r"\x01z")
+ s.settle(20)
+ check("and unzooming gives the strip back", len(strip(s)) == 1,
+ str(here(s)))
+
+
+if __name__ == "__main__":
+ test_minimising_moves_a_pane_to_the_strip()
+ test_clicking_the_row_brings_it_back()
+ test_focusing_it_by_any_route_brings_it_back()
+ test_it_will_not_leave_the_tab_empty()
+ test_the_button_minimises_that_pane()
+ test_it_has_no_say_once_the_tab_is_a_list()
+ test_zoom_hides_the_strip()
+ sys.exit(report())