diff options
| author | user <user@clank> | 2026-08-25 14:23:33 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-25 14:23:33 +0200 |
| commit | b744a90859bf248bfbea97f990668301b5fa9537 (patch) | |
| tree | f1b717507bc24161b7ab41def644289f9457470e /tests | |
| parent | placeholder webbie (diff) | |
| download | slosh-b744a90859bf248bfbea97f990668301b5fa9537.tar.gz slosh-b744a90859bf248bfbea97f990668301b5fa9537.tar.xz slosh-b744a90859bf248bfbea97f990668301b5fa9537.zip | |
compact: shared borders instead of gaps (D23)
One knob, `compact true`: panes pack flush against 1-cell divider lines
that meet in real junctions, one frame rings the tab, and a pane's title
rides the line above it -- a boundary costs one cell where the default
look spends four. The lines are strokes: each write says which edges of
its cell it needs, unions with what is there, and never clobbers text,
so ├ ┬ ┼ fall out of paint order and the focus ring recolours without
inventing arms. The divider *is* the gap (eff_gap, one answer for the
layout, split_fits and the resize clamp), so gap hits, hover hints and
corner drags work unchanged. Interior edges give up click-to-split --
one cell cannot hold two verbs -- and a pane's status row overlays its
last content row. Floats, zoom and the D6 flatten keep classic frames
through place_hidden: zero new tree cases.
Underneath it, mode or no mode: a border drag now moves the boundary on
that side, converted on the first motion so the press that never moves
keeps its old meaning -- the handle's click still splits, the rim's is
still deliberately nothing (drag.rim). Which is also the fix for gap 0,
where flush panes leave no gap cell to grab and the resize gesture had
silently vanished with it.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_compact.py | 338 |
1 files changed, 338 insertions, 0 deletions
diff --git a/tests/test_compact.py b/tests/test_compact.py new file mode 100644 index 0000000..4d15131 --- /dev/null +++ b/tests/test_compact.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +"""Compact mode: shared 1-cell borders instead of gaps — and the border-drag +resize that `gap 0` needs either way. + +Panes pack flush against divider lines that meet in real junctions, one frame +rings the tab, and a pane's title rides the line above it. The dividers carry +the same resize drag the gaps did; interior edges give up click-to-split; the +outer frame keeps it. Floats, zoom and the flatten keep the classic frame. +""" + +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 + + +def _lay(text): + f = tempfile.NamedTemporaryFile("w", suffix=".layout", delete=False) + f.write(text) + f.close() + return f.name + + +GAP0 = _cfg("gap 0\npadding 0\n") +COMPACT = _cfg("compact true\nhover_delay_ms 10\n") +GRID = _lay( + 'layout { tab { pane split="rows" { ' + 'pane split="cols" { pane; pane }; ' + 'pane split="cols" { pane; pane } } } }\n' +) + +SH = ["/bin/sh", "-c", 'printf "\\033]2;pane\\007"; stty raw -echo; cat'] + + +def drag(s, x0, y0, x1, y1): + s.send(rf"\e[<0;{x0 + 1};{y0 + 1}M") + s.send(rf"\e[<32;{x1 + 1};{y1 + 1}M") + s.send(rf"\e[<0;{x1 + 1};{y1 + 1}m") + s.settle(40) + + +# ---- gap 0: the border is all there is, so the border drags --------------- + + +def test_gap0_border_drag_moves_the_boundary(): + with Session(SH, cols=80, rows=20, config=GAP0) as s: + s.settle() + s.key("\\\\") + s.settle() + left = min(s.panes(), key=lambda p: p["x"]) + bx = left["x"] + left["w"] - 1 + by = left["y"] + left["h"] // 2 + check( + "flush panes leave no gap, so the cell is the border", + s.snapshot().hit_at(bx, by) == f"border:{left['id']}:r", + s.snapshot().hit_at(bx, by), + ) + before = left["w"] + drag(s, bx, by, bx + 6, by) + after = min(s.panes(), key=lambda p: p["x"])["w"] + check( + "dragging the border moved the boundary", + after == before + 6, + f"{before} -> {after}", + ) + + +def test_gap0_rim_drags_but_never_splits(): + with Session(SH, cols=80, rows=20, config=GAP0) as s: + s.settle() + s.key("\\\\") + s.settle() + snap = s.snapshot() + left = min(s.panes(), key=lambda p: p["x"]) + rim = snap.rim(left["id"], "r") + check("the border still has a rim", rim is not None) + s.click(*rim) + s.settle() + check( + "a rim click still splits nothing", len(s.panes()) == 2, str(len(s.panes())) + ) + before = left["w"] + drag(s, rim[0], rim[1], rim[0] - 5, rim[1]) + after = min(s.panes(), key=lambda p: p["x"])["w"] + check( + "but a rim drag moves the boundary", + after == before - 5, + f"{before} -> {after}", + ) + + +def test_gap0_handle_click_still_splits(): + with Session(SH, cols=120, rows=20, config=GAP0) as s: + s.settle() + s.key("\\\\") + s.settle() + left = min(s.panes(), key=lambda p: p["x"]) + h = s.snapshot().handle(left["id"], "r") + s.click(*h) + s.settle() + check( + "a handle click is still the split it always was", + len(s.panes()) == 3, + str(len(s.panes())), + ) + + +# ---- compact: the look ---------------------------------------------------- + + +def test_compact_boundary_costs_one_cell(): + with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s: + s.settle(60) + ps = sorted(s.panes(), key=lambda p: (p["y"], p["x"])) + tl, tr = ps[0], ps[1] + check( + "one cell between side-by-side contents", + tr["content_x"] - (tl["content_x"] + tl["content_w"]) == 1, + f"{tl} | {tr}", + ) + bl = ps[2] + check( + "one cell between stacked contents", + bl["content_y"] - (tl["content_y"] + tl["content_h"]) == 1, + f"{tl} / {bl}", + ) + check( + "content starts one cell in from the screen edge", + tl["content_x"] == 1 and tl["content_y"] == 2, + str(tl), + ) + + +def test_compact_junctions_are_real_glyphs(): + with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s: + s.settle(60) + snap = s.snapshot() + scr = snap.screen() + for g in "┬┴├┤┼": + check(f"the lines meet in a {g}", g in scr) + corner = [h for h in snap.hits if h["action"].startswith("corner:")] + check("the crossing is a drag target", len(corner) == 1, str(corner)) + c = corner[0] + check( + "and the target sits on the ┼ itself", + snap.line(c["y"])[c["x"]] == "┼", + repr(snap.line(c["y"])), + ) + + +def test_compact_titles_ride_the_shared_line(): + with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s: + s.settle(60) + snap = s.snapshot() + ps = sorted(s.panes(), key=lambda p: (p["y"], p["x"])) + bl = ps[2] # bottom-left: its title line is the interior divider + hit = [h for h in snap.hits if h["action"] == f"panetitle:{bl['id']}"] + check( + "an interior pane's name is on the divider above it", + hit and hit[0]["y"] == bl["y"] - 1, + str(hit), + ) + check( + "and the name is drawn there", + "pane" in snap.line(bl["y"] - 1), + repr(snap.line(bl["y"] - 1)), + ) + + +# ---- compact: the mouse --------------------------------------------------- + + +def test_compact_dividers_drag_like_gaps(): + with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s: + s.settle(60) + snap = s.snapshot() + ps = sorted(s.panes(), key=lambda p: (p["y"], p["x"])) + tl = ps[0] + dx = tl["x"] + tl["w"] # the divider column + dy = tl["y"] + 2 + check( + "the divider is the boundary's own hit", + (snap.hit_at(dx, dy) or "").startswith("edge:"), + snap.hit_at(dx, dy), + ) + before = tl["w"] + drag(s, dx, dy, dx + 5, dy) + after = sorted(s.panes(), key=lambda p: (p["y"], p["x"]))[0]["w"] + check( + "dragging it moves the boundary", + after == before + 5, + f"{before} -> {after}", + ) + + +def test_compact_interior_edges_resize_but_do_not_split(): + with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s: + s.settle(60) + snap = s.snapshot() + ps = sorted(s.panes(), key=lambda p: (p["y"], p["x"])) + bl = ps[2] + divy = bl["y"] - 1 + free = snap.hit_at(bl["x"], divy) + check( + "a divider's free cells stay the resize handle", + (free or "").startswith("edge:"), + free, + ) + interior = [ + h + for h in snap.hits + if h["action"] in (f"border:{bl['id']}:t", f"brim:{bl['id']}:t") + ] + check("an interior edge offers no split", not interior, str(interior)) + + +def test_compact_outer_frame_still_splits(): + with Session(SH, cols=100, rows=22, config=COMPACT) as s: + s.settle() + p = s.pane(0) + snap = s.snapshot() + h = snap.handle(p["id"], "l") + check("the outer frame has a handle", h is not None) + check("on the frame's own column", h[0] == p["x"] - 1, str(h)) + s.click(*h) + s.settle() + check("and clicking it splits", len(s.panes()) == 2, str(len(s.panes()))) + + +def test_compact_hover_hint_spares_the_title(): + with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s: + s.settle(60) + ps = sorted(s.panes(), key=lambda p: (p["y"], p["x"])) + bl = ps[2] + divy = bl["y"] - 1 + s.send(rf"\e[<35;{bl['x'] + 2};{divy + 1}M") + time.sleep(0.05) + s.settle(30) + row = s.snapshot().line(divy) + check("the armed divider goes dotted", "┈" in row, repr(row)) + check("without ruling through the pane's name", " pane " in row, repr(row)) + + +# ---- compact: the frame's colour and everything classic ------------------- + + +def test_compact_focus_ring_wears_the_frame_colour(): + with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s: + s.settle(60) + snap = s.snapshot() + f = s.focused() + ring = snap.style_at(f["x"] - 1, f["y"] + 1) + # another pane's stretch of the frame, for contrast + others = [p for p in s.panes() if p["id"] != f["id"]] + far = max(others, key=lambda p: abs(p["x"] - f["x"]) + abs(p["y"] - f["y"])) + idle = snap.style_at(far["x"] + far["w"], far["y"] + 1) + check("the focused ring is coloured", ring is not None, str(ring)) + check( + "differently from an idle stretch of line", + idle is None or (ring or {}).get("fg") != idle.get("fg"), + f"{ring} vs {idle}", + ) + + +def test_compact_dead_pane_epitaph_overlays_the_last_row(): + cfg = _cfg('compact true\nkeep_dead "all"\n') + with Session(["/bin/sh", "-c", "exit 3"], cols=60, rows=12, config=cfg) as s: + snap = s.until(lambda sn: "re-run" in sn.screen()) + p = s.pane(0) + row = snap.line(p["content_y"] + p["content_h"] - 1) + check( + "the epitaph rides the last content row", + "status 3" in row and "[re-run]" in row, + repr(row), + ) + os.unlink(cfg) + + +def test_compact_float_keeps_the_classic_frame(): + with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s: + s.settle(60) + s.key("f") + s.settle(30) + f = s.focused() + check( + "a float's contents sit inside its own border", + f["content_x"] == f["x"] + 1 and f["content_y"] == f["y"] + 1, + str(f), + ) + s.key("f") + s.settle(30) + + +def test_compact_is_a_config_key(): + import subprocess + + from harness import BIN + + env = dict(os.environ, SLOSH_CONFIG="/nonexistent/slosh.kdl") + out = subprocess.run( + [BIN, "--dump-config"], capture_output=True, text=True, env=env + ).stdout + check( + "dump-config writes the knob", + any(l.split() and l.split()[0] == "compact" for l in out.splitlines()), + out[:200], + ) + r = subprocess.run( + [BIN, "--check", COMPACT], capture_output=True, text=True, env=env + ) + check("and --check accepts compact true", r.returncode == 0, r.stdout + r.stderr) + + +if __name__ == "__main__": + test_gap0_border_drag_moves_the_boundary() + test_gap0_rim_drags_but_never_splits() + test_gap0_handle_click_still_splits() + test_compact_boundary_costs_one_cell() + test_compact_junctions_are_real_glyphs() + test_compact_titles_ride_the_shared_line() + test_compact_dividers_drag_like_gaps() + test_compact_interior_edges_resize_but_do_not_split() + test_compact_outer_frame_still_splits() + test_compact_hover_hint_spares_the_title() + test_compact_focus_ring_wears_the_frame_colour() + test_compact_dead_pane_epitaph_overlays_the_last_row() + test_compact_float_keeps_the_classic_frame() + test_compact_is_a_config_key() + sys.exit(report()) |
