diff options
| author | user <user@clank> | 2026-08-14 14:45:49 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-14 14:45:49 +0200 |
| commit | 037c3550f075ff78e680e5a1f1508d4c292ffcb1 (patch) | |
| tree | 18c085806ac40d1e5ff9b49e6cee668732152fde | |
| parent | sl0ppty: pane states are a table, not three hardcoded knobs (diff) | |
| download | slosh-037c3550f075ff78e680e5a1f1508d4c292ffcb1.tar.gz slosh-037c3550f075ff78e680e5a1f1508d4c292ffcb1.tar.xz slosh-037c3550f075ff78e680e5a1f1508d4c292ffcb1.zip | |
sl0ppty: a floor under splitting, and an arrow saying which way
Two changes to the same affordance.
A border stops being a button below `min_split cols=32 rows=8`. This is a
different question from min_pane, which is where the layout gives up and
collapses a pane: two twenty-five column panes technically fit and are not
worth having, so the number that decides whether to *offer* a split is larger
than the one that decides whether a pane can survive. It is clamped to never
go below min_pane, because refusing to offer what the layout would collapse
anyway is the floor under the floor.
The test is the layout's own arithmetic rather than an approximation of it:
the same floor, the same gap, and the same child count the split would create
— which is one more sibling when the parent already splits that way and two
when it does not. The guide's standing claim is that drawing and the layout
cannot disagree, and a guide promising a split that immediately collapsed
would be exactly that.
The keyboard obeys the same floor, with a toast. A rule only the mouse
followed would mean the same session splits or refuses depending on how you
asked, and whether there is room cannot depend on that. The control API and
layout files are *not* bound by it: a script asking for a pane is declaring
what it wants rather than being offered something, and D6's responsive
collapse is what catches it there. That distinction was not free — app_split_pane
went through the same helper as the keybinding, so the API was silently bound
too until a test noticed, and the interactive path is now its own function.
The guide also gained an arrow on the dashed line, pointing at the half the
new pane will take. The line already said where the boundary lands and left
which side to be inferred from which border you happened to be touching.
Pointers for left and right rather than the matching triangles: U+25C0 and
U+25B6 carry emoji presentation and are widely rendered double-width, while
screen_text books every chrome glyph as exactly one cell — so one that drew as
two would shift the rest of the row. U+25B2 is already the scroll indicator,
so the vertical pair is known good here.
Raising a floor invalidates every test that quietly relied on the old one, and
nine of them did: sessions sized for a world where a sixty-column terminal
could be split into columns. They are sized for the rule now, and the two that
were really about *layout* collapse rather than the affordance now split over
the API, which is the path that still means "do it anyway".
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | config/config.kdl | 17 | ||||
| -rw-r--r-- | src/app.c | 96 | ||||
| -rw-r--r-- | src/config.c | 10 | ||||
| -rw-r--r-- | src/config.h | 6 | ||||
| -rw-r--r-- | tests/test_config.py | 10 | ||||
| -rw-r--r-- | tests/test_drag.py | 2 | ||||
| -rw-r--r-- | tests/test_layout.py | 14 | ||||
| -rw-r--r-- | tests/test_pane_title.py | 4 | ||||
| -rw-r--r-- | tests/test_session.py | 2 | ||||
| -rw-r--r-- | tests/test_split_ux.py | 128 | ||||
| -rw-r--r-- | tests/test_tabs.py | 2 |
12 files changed, 266 insertions, 31 deletions
@@ -54,7 +54,11 @@ sl0ppty cmd '{"cmd":"panes"}' The mouse works throughout. **Click a pane's border to split toward it** — the side you click is the side the new pane appears on, and hovering a border arms -it and shows a dashed line where the split would land. Click a pane to focus +it and shows a dashed line where the split would land, with an arrow (`◄ ► ▲ +▼`) on that line pointing at the half the new pane will take. Below +`min_split` the border stops being a button entirely: no guide, no split, on +the reasoning that two twenty-five column panes technically fit and are not +worth having. Click a pane to focus it, a tab to switch, `+tab` to open one, a collapsed header to expand it, and a pane's own buttons to talk to the program inside it. **Drag a pane's title bar onto another pane to swap them, or drag the gap between two panes to move the diff --git a/config/config.kdl b/config/config.kdl index 7e68c9b..f183c6b 100644 --- a/config/config.kdl +++ b/config/config.kdl @@ -38,6 +38,23 @@ focus_follows_mouse true // widening the terminal always restores exactly the layout you had. min_pane cols=24 rows=6 +// The smallest pane a split is allowed to *produce*. min_pane above is where +// the layout gives up and collapses; this is where splitting stopped being +// worth offering, which is a larger number and a different question — two +// twenty-five column panes technically fit and are not worth having. +// +// Below this the border stops being a button: no guide arms, clicking it does +// nothing, and the keyboard says so with a toast. The control API and layout +// files are deliberately exempt, because a script asking for a pane is saying +// what it wants rather than being offered something, and the responsive +// collapse is what catches it there. +// +// Never used below min_pane: refusing to offer what the layout would collapse +// anyway is the floor under the floor. Raise it if you like fewer, bigger +// panes; a column split needs 2*cols + 2 columns of pane to be offered, a row +// split 2*rows + 1. +min_split cols=32 rows=8 + // Rows per wheel notch, and per scroll-up/scroll-down. scroll_lines 3 @@ -719,10 +719,65 @@ static void split_node(app_t *a, node_t *leaf, split_dir_t dir, bool before) { layout(a); } +/* Would splitting this pane produce two panes worth having? + * + * This is the layout's own arithmetic, not an approximation of it: the same + * floor, the same gap, and the same child count the split would actually + * create — which is one more sibling when the parent already splits this way, + * and two when it does not. Guessing here would let the guide promise a split + * that immediately collapsed, and the guide's whole claim is that drawing and + * the layout cannot disagree. */ +static bool split_fits(node_t *leaf, split_dir_t dir) { + /* Two different questions, and the larger answer wins: min_pane is where the + * layout gives up, min_split is where splitting stopped being worth + * offering. A pane can clear the first and still be too small to be worth + * halving — which is the whole point of having both. */ + uint16_t floor_ = dir == SPLIT_COLS ? CFG.min_split_cols : CFG.min_split_rows; + uint16_t hard = dir == SPLIT_COLS ? MIN_PANE_COLS : MIN_PANE_ROWS; + if (floor_ < hard) floor_ = hard; + uint16_t gap = + dir == SPLIT_COLS ? (uint16_t)(CFG.gap * CFG.gap_aspect) : CFG.gap; + + size_t k; + uint16_t total; + if (leaf->parent && leaf->parent->dir == dir) { + k = leaf->parent->nkids + 1; + total = dir == SPLIT_COLS ? leaf->parent->rect.w : leaf->parent->rect.h; + } else { + k = 2; + total = dir == SPLIT_COLS ? leaf->rect.w : leaf->rect.h; + } + return total >= (uint16_t)(k * floor_ + gap * (k - 1)); +} + +static split_dir_t side_dir(char side) { + return (side == 'l' || side == 'r') ? SPLIT_COLS : SPLIT_ROWS; +} + static void split_focus(app_t *a, split_dir_t dir) { split_node(a, cur(a)->focus, dir, false); } +/* Splitting because a person just asked for it, which is the only case the + * floor applies to. + * + * The keyboard obeys the same floor the border does: a rule only the mouse + * followed would mean the same session splits or refuses depending on how you + * asked, and whether there is room cannot depend on that. The control API and + * layout files go through split_focus() and are deliberately *not* bound by + * it — a script asking for a pane is declaring what it wants, not being + * offered something, and D6's responsive collapse is what catches it there. */ +static void split_focus_ui(app_t *a, split_dir_t dir) { + node_t *n = cur(a)->focus; + if (!n) return; + if (!split_fits(n, dir)) { + app_toast(a, dir == SPLIT_COLS ? "no room to split across" + : "no room to split down"); + return; + } + split_node(a, n, dir, false); +} + static node_t *first_leaf(node_t *n) { while (n && n->kind == NODE_SPLIT) n = n->kids[0]; return n; @@ -1247,11 +1302,24 @@ static void draw_split_guide(app_t *a, screen_t *s, node_t *leaf) { } if (!side) return; + /* Nothing is offered that cannot be delivered: below the floor this pane + * would only collapse, so the border simply stops being a button. */ + if (!split_fits(leaf, side_dir(side))) return; + rect_t r = leaf->rect; if (r.w < 4 || r.h < 4) return; uint16_t x1 = (uint16_t)(r.x + r.w - 1), y1 = (uint16_t)(r.y + r.h - 1); color_t hi = FRAME_FOCUS; + /* An arrow on the new boundary, pointing at the side the new pane will take. + * The dashed line says where, and on its own leaves which half is the new + * one to be inferred from which border you happen to be touching. + * + * Pointers rather than the matching triangles for left and right: U+25C0 and + * U+25B6 carry emoji presentation and terminals widely render them + * double-width, and screen_text books every chrome glyph as one cell — so a + * cell that draws as two would shift the rest of the row. U+25B2 is already + * the scroll indicator, so the vertical pair is known good here. */ if (side == 'l' || side == 'r') { uint16_t bx = side == 'l' ? r.x : x1; uint16_t mid = (uint16_t)(r.x + r.w / 2); @@ -1259,6 +1327,8 @@ static void draw_split_guide(app_t *a, screen_t *s, node_t *leaf) { screen_text(s, bx, y, "\u2503", hi, NO_COLOR, ATTR_BOLD); screen_text(s, mid, y, "\u254e", hi, NO_COLOR, 0); } + screen_text(s, mid, (uint16_t)(r.y + r.h / 2), + side == 'l' ? "\u25c4" : "\u25ba", hi, NO_COLOR, ATTR_BOLD); } else { uint16_t by = side == 't' ? r.y : y1; uint16_t mid = (uint16_t)(r.y + r.h / 2); @@ -1266,6 +1336,8 @@ static void draw_split_guide(app_t *a, screen_t *s, node_t *leaf) { screen_text(s, x, by, "\u2501", hi, NO_COLOR, ATTR_BOLD); screen_text(s, x, mid, "\u254c", hi, NO_COLOR, 0); } + screen_text(s, (uint16_t)(r.x + r.w / 2), mid, + side == 't' ? "\u25b2" : "\u25bc", hi, NO_COLOR, ATTR_BOLD); } } @@ -2176,8 +2248,8 @@ static bool prefix_command(app_t *a, const input_event_t *ev) { return true; } switch (act) { - case ACT_SPLIT_COLS: split_focus(a, SPLIT_COLS); return true; - case ACT_SPLIT_ROWS: split_focus(a, SPLIT_ROWS); return true; + 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_CLOSE_PANE: if (cur(a)->focus) close_leaf(a, cur(a)->focus); return true; @@ -2328,13 +2400,21 @@ void app_event(app_t *a, const input_event_t *ev) { node_t *n = pane_by_id(a, a->drag.src); if (n) { char side = a->drag.side; - bool rows = side == 't' || side == 'b'; bool before = side == 'l' || side == 't'; - split_node(a, n, rows ? SPLIT_ROWS : SPLIT_COLS, before); - app_toast(a, side == 'l' ? "split left" - : side == 'r' ? "split right" - : side == 't' ? "split up" - : "split down"); + split_dir_t dir = side_dir(side); + /* The guide already declined to offer this, so the click that + * the guide would have explained must decline too — otherwise + * the border silently does something it just said it would not. */ + if (!split_fits(n, dir)) { + app_toast(a, dir == SPLIT_COLS ? "no room to split across" + : "no room to split down"); + } else { + split_node(a, n, dir, before); + app_toast(a, side == 'l' ? "split left" + : side == 'r' ? "split right" + : side == 't' ? "split up" + : "split down"); + } } a->drag.kind = DRAG_NONE; a->drag.src = a->drag.target = 0; diff --git a/src/config.c b/src/config.c index fee7d54..56175eb 100644 --- a/src/config.c +++ b/src/config.c @@ -170,6 +170,8 @@ void config_defaults(config_t *c) { c->title_align = ALIGN_CENTER; c->min_pane_cols = 24; c->min_pane_rows = 6; + c->min_split_cols = 32; + c->min_split_rows = 8; c->scroll_lines = 3; c->toast_ms = 2500; c->hover_delay_ms = 250; @@ -301,6 +303,14 @@ bool config_load(config_t *c, const char *path, char *err, size_t errcap) { else c->title_align = ALIGN_CENTER; } + const kdl_node_t *mins = kdl_child(root, "min_split"); + if (mins) { + c->min_split_cols = + (uint16_t)kdl_prop_int(mins, "cols", c->min_split_cols); + c->min_split_rows = + (uint16_t)kdl_prop_int(mins, "rows", c->min_split_rows); + } + const kdl_node_t *minp = kdl_child(root, "min_pane"); if (minp) { c->min_pane_cols = (uint16_t)kdl_prop_int(minp, "cols", c->min_pane_cols); diff --git a/src/config.h b/src/config.h index 098c9bc..cf10d74 100644 --- a/src/config.h +++ b/src/config.h @@ -75,6 +75,12 @@ typedef struct { bool rounded; align_t title_align; uint16_t min_pane_cols, min_pane_rows; + /* The smallest pane a split is allowed to *produce*. min_pane is the point + * below which the layout gives up and collapses a pane; this is the point + * below which splitting stopped being worth offering, which is a larger + * number and a different question. Never used below min_pane: refusing to + * offer something the layout would collapse anyway is the floor. */ + uint16_t min_split_cols, min_split_rows; uint16_t scroll_lines; uint16_t toast_ms; uint16_t hover_delay_ms; /* how long the pointer must rest to arm a guide */ /* how long an announcement stays up */ /* rows per wheel notch */ diff --git a/tests/test_config.py b/tests/test_config.py index 735e223..49ef7be 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -59,7 +59,7 @@ def test_theme(): frame_idle "#333333" } """) - with Session(SH, cols=60, rows=14, config=path) as s: + with Session(SH, cols=76, rows=14, config=path) as s: s.settle() p = s.pane() run = s.snapshot().style_at(p["x"], p["y"]) @@ -109,17 +109,21 @@ def test_keys(): def test_min_pane_drives_collapse(): + # Split over the control API rather than the keyboard. This is about what + # the *layout* does with a split that does not fit, and the keyboard now + # declines to make one — which is a different rule, tested elsewhere. A + # script asking for a pane is declaring what it wants, so it still gets it. roomy = cfg("min_pane cols=10 rows=3\n") tight = cfg("min_pane cols=40 rows=12\n") with Session(SH, cols=70, rows=16, config=roomy) as s: s.settle() - s.key("\\\\") + s.api("split", dir="cols") s.settle() check("a low floor keeps both panes visible", not any(p["hidden"] for p in s.panes()), str(s.panes())) with Session(SH, cols=70, rows=16, config=tight) as s: s.settle() - s.key("\\\\") + s.api("split", dir="cols") s.settle() check("a high floor collapses the same split", any(p["hidden"] for p in s.panes()), str(s.panes())) diff --git a/tests/test_drag.py b/tests/test_drag.py index 1fdd029..b392f3c 100644 --- a/tests/test_drag.py +++ b/tests/test_drag.py @@ -156,7 +156,7 @@ def test_title_drag_reorders(): def test_drag_edge_cases(): - with Session(SH, cols=90, rows=16) as s: + with Session(SH, cols=150, rows=16) as s: s.settle() s.key("\\\\") s.settle() diff --git a/tests/test_layout.py b/tests/test_layout.py index df79877..87339d4 100644 --- a/tests/test_layout.py +++ b/tests/test_layout.py @@ -38,7 +38,9 @@ def check_invariants(name, s, snap=None): def test_split_and_close(): - with Session(SH, cols=60, rows=16) as s: + # Big enough for both splits to clear min_split (32x8 by default): a column + # split needs 2*32+2 columns of pane, a row split 2*8+1 rows. + with Session(SH, cols=80, rows=24) as s: s.settle() check("starts with one pane", len(s.panes()) == 1) @@ -71,7 +73,7 @@ def test_split_and_close(): def test_three_way_split_is_even(): """Splitting the same direction twice must give equal shares, not 1/2+1/4+1/4.""" - with Session(SH, cols=62, rows=10) as s: + with Session(SH, cols=110, rows=10) as s: s.settle() s.key("\\\\") s.settle() @@ -84,7 +86,7 @@ def test_three_way_split_is_even(): def test_focus_movement(): - with Session(SH, cols=60, rows=16) as s: + with Session(SH, cols=76, rows=16) as s: s.settle() s.key("\\\\") s.settle() @@ -120,7 +122,7 @@ def test_frames_and_gap(): pane["content_x"] == pane["x"] + 1 and pane["content_y"] == pane["y"] + 1, str(pane)) - with Session(SH, cols=60, rows=8) as s: + with Session(SH, cols=76, rows=8) as s: s.settle() s.key("\\\\") s.settle() @@ -130,7 +132,7 @@ def test_frames_and_gap(): def test_focus_is_visible(): - with Session(SH, cols=60, rows=10) as s: + with Session(SH, cols=76, rows=10) as s: s.settle() s.key("\\\\") s.settle() @@ -168,7 +170,7 @@ def test_hit_list(): def test_click_focuses_and_forwards(): - with Session(SH, cols=60, rows=12) as s: + with Session(SH, cols=76, rows=12) as s: s.settle() s.key("\\\\") s.settle() diff --git a/tests/test_pane_title.py b/tests/test_pane_title.py index 07ec1ce..d9a368f 100644 --- a/tests/test_pane_title.py +++ b/tests/test_pane_title.py @@ -100,7 +100,7 @@ def test_the_title_text_is_its_own_region(): def test_resting_on_the_title_arms_no_guide(): - with Session(SH, cols=60, rows=14) as s: + with Session(SH, cols=60, rows=26) as s: s.settle() p = s.pane() _, _, mid = title_span(s.snapshot(), p) @@ -121,7 +121,7 @@ def test_resting_on_the_title_arms_no_guide(): def test_clicking_the_title_does_not_split(): - with Session(SH, cols=60, rows=14) as s: + with Session(SH, cols=60, rows=26) as s: s.settle() p = s.pane() _, _, mid = title_span(s.snapshot(), p) diff --git a/tests/test_session.py b/tests/test_session.py index 09edb49..5560eda 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -28,7 +28,7 @@ def check(name, cond, detail=""): print(f"{'ok ' if cond else 'FAIL'} {name}{'' if cond else ' <- ' + detail}") -def attach(name, cols=60, rows=12, inner=None): +def attach(name, cols=80, rows=24, inner=None): """Attach a client on a real pty. Returns (pid, fd).""" pid, fd = pty.fork() if pid == 0: diff --git a/tests/test_split_ux.py b/tests/test_split_ux.py index 17f368a..faa1341 100644 --- a/tests/test_split_ux.py +++ b/tests/test_split_ux.py @@ -5,11 +5,20 @@ The old per-pane `+` was one glyph for a verb that has four directions, and it taxed every frame three columns forever. A border encodes the direction by being on that side, costs nothing when idle, and shows a preview on hover. """ +import os import sys +import tempfile import time from harness import Session, check, report + +def cfg(text): + f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False) + f.write(text) + f.close() + return f.name + SH = ["/bin/sh", "-c", 'printf "\\033]2;shell\\007"; stty raw -echo; cat'] @@ -48,7 +57,7 @@ def test_each_border_splits_toward_itself(): ("bottom", lambda p: (p["x"] + 4, p["y"] + p["h"] - 1), "below"), ("top", lambda p: (p["x"] + 4, p["y"]), "above"), ]: - with Session(SH, cols=70, rows=18) as s: + with Session(SH, cols=80, rows=26) as s: s.settle() before = s.pane() x, y = cell(before) @@ -81,7 +90,7 @@ def test_each_border_splits_toward_itself(): def test_the_guide(): - with Session(SH, cols=60, rows=14) as s: + with Session(SH, cols=80, rows=26) as s: s.settle() p = s.pane() snap = s.snapshot() @@ -118,7 +127,7 @@ def test_guide_follows_the_new_layout(): until the mouse moved and corrected it. It is derived during the paint now, from the rects that paint registered. """ - with Session(SH, cols=54, rows=12) as s: + with Session(SH, cols=120, rows=26) as s: s.settle() p = s.pane() edge_x, edge_y = p["x"] + p["w"] - 1, p["y"] + 3 @@ -146,7 +155,7 @@ def test_guide_follows_the_new_layout(): def test_the_guide_waits_for_a_rest(): """A pointer crossing a border on its way elsewhere must not flash it.""" - with Session(SH, cols=54, rows=12) as s: + with Session(SH, cols=80, rows=26) as s: s.settle() p = s.pane() left, right = p["x"], p["x"] + p["w"] - 1 @@ -179,7 +188,7 @@ def test_the_guide_waits_for_a_rest(): def test_press_skips_the_wait(): - with Session(SH, cols=54, rows=12) as s: + with Session(SH, cols=80, rows=26) as s: s.settle() p = s.pane() x, y = p["x"] + p["w"] - 1, p["y"] + 3 @@ -195,7 +204,7 @@ def test_delay_is_configurable(): f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False) f.write("hover_delay_ms 0\n") f.close() - with Session(SH, cols=54, rows=12, config=f.name) as s: + with Session(SH, cols=80, rows=26, config=f.name) as s: s.settle() p = s.pane() hover(s, p["x"], p["y"] + 3) @@ -206,7 +215,7 @@ def test_delay_is_configurable(): def test_guide_is_per_pane(): - with Session(SH, cols=90, rows=14) as s: + with Session(SH, cols=150, rows=26) as s: s.settle() s.key("\\\\") s.settle() @@ -222,7 +231,7 @@ def test_guide_is_per_pane(): def test_drag_still_moves(): """The top border does double duty: click splits, drag moves.""" - with Session(SH, cols=90, rows=16) as s: + with Session(SH, cols=90, rows=26) as s: s.settle() s.key("\\\\") s.settle() @@ -262,6 +271,104 @@ def test_tiny_panes_have_no_targets(): and h["w"] < 4) for h in snap.hits), str(snap.hits)) +# ---- the floor: a split that is not worth making is not offered ------------ + +ARROWS = {"l": "\u25c4", "r": "\u25ba", "t": "\u25b2", "b": "\u25bc"} + + +def edge_of(pane, side): + if side == "l": + return pane["x"], pane["y"] + 2 + if side == "r": + return pane["x"] + pane["w"] - 1, pane["y"] + 2 + if side == "t": + return pane["x"] + 4, pane["y"] + return pane["x"] + 4, pane["y"] + pane["h"] - 1 + + +def test_a_split_below_the_floor_is_not_offered(): + """min_split is a usability floor, not the physical one: the pane below + would still *fit* by min_pane, and is refused anyway.""" + conf = cfg("min_pane cols=10 rows=3\nmin_split cols=40 rows=14\n") + with Session(SH, cols=80, rows=26, config=conf) as s: + s.settle() + p = s.pane() + for side in ("l", "r", "t", "b"): + x, y = edge_of(p, side) + rest(s, x, y) + snap = s.snapshot() + body = snap.screen() + check(f"resting on the {side} border arms nothing below the floor", + "\u2503" not in body and "\u2501" not in body, repr(body[:80])) + click(s, x, y) + s.settle(80) + check(f"and clicking the {side} border splits nothing", + len(s.panes()) == 1, str(len(s.panes()))) + os.unlink(conf) + + +def test_the_floor_is_configurable_in_both_axes(): + # Wide enough for columns, too short for rows. + conf = cfg("min_split cols=20 rows=40\n") + with Session(SH, cols=90, rows=26, config=conf) as s: + s.settle() + p = s.pane() + x, y = edge_of(p, "b") + click(s, x, y) + s.settle(80) + check("a row split below the row floor is refused", + len(s.panes()) == 1, str(len(s.panes()))) + x, y = edge_of(p, "r") + click(s, x, y) + s.settle(80) + check("while a column split above the column floor still happens", + len(s.panes()) == 2, str(len(s.panes()))) + os.unlink(conf) + + +def test_the_keyboard_obeys_the_same_floor(): + conf = cfg("min_split cols=40 rows=14\n") + with Session(SH, cols=80, rows=26, config=conf) as s: + s.settle() + s.key("\\\\") + s.settle(80) + check("the keyboard will not split below the floor either", + len(s.panes()) == 1, str(len(s.panes()))) + # ...but a script asking for a pane is declaring, not being offered. + s.api("split", dir="cols") + s.settle(80) + check("the control API is not bound by it", len(s.panes()) == 2, + str(len(s.panes()))) + os.unlink(conf) + + +def test_the_guide_says_which_way_it_goes(): + with Session(SH, cols=120, rows=26) as s: + s.settle() + p = s.pane() + for side in ("l", "r", "t", "b"): + x, y = edge_of(p, side) + rest(s, x, y) + body = s.snapshot().screen() + check(f"the {side} guide carries its own arrow", + ARROWS[side] in body, repr(ARROWS[side])) + others = [a for k, a in ARROWS.items() if k != side] + check(f"and only that one", not any(o in body for o in others), + repr(body[:80])) + + +def test_the_arrow_sits_on_the_new_boundary(): + with Session(SH, cols=120, rows=26) as s: + s.settle() + p = s.pane() + x, y = edge_of(p, "r") + rest(s, x, y) + snap = s.snapshot() + pos = snap.find(ARROWS["r"]) + check("the arrow is on the dashed line, not the border", + pos is not None and pos[0] == p["x"] + p["w"] // 2, str(pos)) + + if __name__ == "__main__": test_no_more_plus() test_each_border_splits_toward_itself() @@ -273,4 +380,9 @@ if __name__ == "__main__": test_guide_is_per_pane() test_drag_still_moves() test_tiny_panes_have_no_targets() + test_a_split_below_the_floor_is_not_offered() + test_the_floor_is_configurable_in_both_axes() + test_the_keyboard_obeys_the_same_floor() + test_the_guide_says_which_way_it_goes() + test_the_arrow_sits_on_the_new_boundary() sys.exit(report()) diff --git a/tests/test_tabs.py b/tests/test_tabs.py index c45073e..5407eaf 100644 --- a/tests/test_tabs.py +++ b/tests/test_tabs.py @@ -79,7 +79,7 @@ def test_tab_click(): def test_closing_a_tab(): - with Session(SH, cols=60, rows=14) as s: + with Session(SH, cols=76, rows=14) as s: s.settle() s.key("c") s.settle() |
