diff options
| author | user <user@clank> | 2026-08-15 23:06:32 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-15 23:06:32 +0200 |
| commit | f6281fbec93d92bfc7e9164492e436d0236d9179 (patch) | |
| tree | c78b435dec7f62c948e2e8144f29f47ccab2fa7d /tests/test_equalize.py | |
| parent | sl0ppty: a command palette, on a key you choose (diff) | |
| download | slosh-f6281fbec93d92bfc7e9164492e436d0236d9179.tar.gz slosh-f6281fbec93d92bfc7e9164492e436d0236d9179.tar.xz slosh-f6281fbec93d92bfc7e9164492e436d0236d9179.zip | |
sl0ppty: shaders on the chrome, and two keys for the arrangement
Five things, in one commit because they land in the same few files --
the action table, the shader pass, config.kdl -- and splitting them by
hunk would buy a tidier log at the price of commits that do not build.
Each is verified end to end; the suite is 46 files green.
Chrome shaders. `where="chrome"` on any entry in `shaders` or in a
`states` block runs that pass over a pane's frame -- border, padding,
title, buttons -- instead of its contents. Nothing else changes: same
registry, same `amount`, same expressions, same chain order, one new
word, because a frame is another rect and so it is another pass. It runs
over the frame's whole rect with the contents punched out as a hole,
rather than four passes over four sides: an effect has to be able to
travel round a border, and four passes would each count x/y from zero
and restart at every corner. The hole keeps the two honest -- a skipped
cell is never visited, so a full-strength chrome tint cannot reach your
text. It is also the only kind of pass that can colour a pane a small
window has collapsed to a single row (D6), which is the state where a
colour is worth the most.
`channel="fg"|"bg"|"both"` came out of looking at the first border
flash: `tint` mixes both of a cell's colours, and a frame's background
is the terminal's own default, so the pass materialised it to our guess
at that default and mixed towards the colour -- a dark rectangle behind
the glyphs, left there until the pane changed state. The mask is
enforced by the pass, not the shader: the other colour is snapshotted
before materialising and put back after, so an unset colour goes back to
unset, it works for a built-in and a plugin alike, and no shader grows
an fg-only twin of itself.
A `bell` pane state and a `since` variable, because "flash the border
when a pane rings" needed both and neither existed. `since` is
milliseconds since the pane entered its current state, stamped in the
one function that decides the state -- a transition is the one thing a
frame cannot tell you, so it is remembered where it cannot disagree with
the state it dates. `bell` ranks above the ambient states and below
`dead`: a pane that rang while you were elsewhere is news, and a pane
that rang and then exited is a pane that exited. It ends when you look
at the pane, because that is what answering a bell is.
Animation needed a clock and did not have one: a shader reading `t` only
advanced when something else caused a frame, so a pulse froze the moment
you stopped typing. `app_next_deadline_ms` now reports `anim_ms` while a
chain that reads the clock ran over the last frame -- derived from what
was applied, so a pulse hung off `dead` costs a clock while a pane is
dead and nothing for the rest of the session. A `deadline` verb makes
that observable; the server's poll loop already lived on the number.
contrib/chrome has seven presets and the perimeter trick they are built
on: on a frame the coordinate you want is distance along the ring, which
the language can work out, and `min(d, P - d)` is what makes an effect
cross the corner it started from. Hand-written, unlike contrib/shaders,
because the toy previews contents and a frame has no preview to give --
so tests/test_chrome_presets.py checks each one the only way left: load
it into a real session, assert the frame moves and the contents do not.
`equalize` (C-a =) puts every share back to even: each split's children
weighted by how many visible panes are behind them, so one pane beside a
stack of three is a quarter of the width and "even" means the same thing
at every depth. Exactly equal cell counts are not something a tree can
always express, so the action promises the even division it can.
`rotate-layout` (C-a Space) turns the whole tab a quarter turn
clockwise. Every split changes axis and a row split's children reverse,
because the pane on top of a stack is the pane on the right once you
turn it; get that asymmetry wrong and you have a mirror image, which
looks almost right until four turns fail to come back. They do come
back, exactly, which is why the inverse is three more turns rather than
a second mirrored routine with its own chance of being wrong -- and why
a key as easy as space is the right one rather than a hazard. A turn is
refused when it would not fit: a share of the height is not always a
share the width can afford, and a node that cannot give its children
their floor takes the whole tab down to a list. So the turn is tried,
the layout is asked what it made of it, and one that flattened the tab
is put back with a word. Same rule `split_fits` applies to splitting.
Keys: the palette moves from `shift+p` to `p`, and cycling tabs from
`n`/`p` to tab/shift+tab. `p` is pressed far more often than "the tab
before this one" and was the only shifted letter in the defaults; tab is
what every other tabbed thing cycles with, and `\e[Z` decodes as
shift+tab on a plain terminal, so this costs nothing on a client without
the kitty protocol. `n` is free again. The actions are untouched: two
lines of config put the old binds back.
The shader ABI goes to 3: `shade_ctx_t` gained `state_ms` and struct
shader gained `channels`, both the pass's business rather than a
shader's. Plugins need a rebuild, which is what the version is for.
Diffstat (limited to 'tests/test_equalize.py')
| -rwxr-xr-x | tests/test_equalize.py | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/tests/test_equalize.py b/tests/test_equalize.py new file mode 100755 index 0000000..675d468 --- /dev/null +++ b/tests/test_equalize.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""`C-a =`: every visible pane gets an even share of the rows and columns. + +The interesting part is not the flat case -- three columns nudged apart and put +back is arithmetic -- but what "even" means once the tree has depth. A split is +divided between the *panes behind* each child, so one pane beside a stack of +two is a third of the width and not half of it. Weighting per child would make +"even" mean something different at every depth, which is the layout this action +exists to get away from. +""" +import sys + +from harness import Session, check, report + +SH = ["/bin/sh", "-c", "stty raw -echo; cat"] + + +def by_pos(s): + """Panes on screen, left to right then top to bottom. A minimised pane gets + no rect at all — the strip draws it — so it is not one of these.""" + return sorted((p for p in s.panes() if p["w"] > 1), + key=lambda p: (p["x"], p["y"])) + + +def widths(s): + return [p["w"] for p in by_pos(s)] + + +def test_a_nudged_row_of_columns_goes_back_to_even(): + with Session(SH, cols=120, rows=24) as s: + s.settle() + s.key("\\\\") + s.key("\\\\") + s.settle() + even = widths(s) + check("three columns start out even", max(even) - min(even) <= 1, + str(even)) + + for _ in range(4): + s.key("L") # shift+l: move the boundary right + s.settle() + nudged = widths(s) + check("nudging the boundary makes them uneven", + max(nudged) - min(nudged) > 2, str(nudged)) + + s.key("=") + s.settle() + back = widths(s) + check("equalize puts them back to even", + max(back) - min(back) <= 1, str(back)) + check("and it kept every pane", len(back) == 3, str(back)) + + +def test_it_divides_a_split_between_the_panes_behind_it(): + """A pane beside a stack of two: a third of the width, not half of it.""" + with Session(SH, cols=120, rows=30) as s: + s.settle() + s.key("\\\\") # left | right + s.key("-") # right becomes right-top / right-bottom + s.settle() + left, top, bottom = by_pos(s) + check("the split starts out halved per child", + abs(left["w"] - top["w"]) <= 1, + f"{left['w']} vs {top['w']}") + + s.key("=") + s.settle() + left, top, bottom = by_pos(s) + check("the stack of two gets twice the lone pane's width", + abs(top["w"] - 2 * left["w"]) <= 2, + f"{left['w']} vs {top['w']}") + check("the two panes sharing that width still match", + top["w"] == bottom["w"], f"{top['w']} vs {bottom['w']}") + check("and they still split their column in half", + abs(top["h"] - bottom["h"]) <= 1, + f"{top['h']} vs {bottom['h']}") + check("the lone pane still spans the tab", + left["h"] > top["h"], f"{left['h']} vs {top['h']}") + + +def test_a_minimised_pane_is_not_counted(): + """It is out of the layout and costs a row in the strip, not a share: its + share would otherwise be handed to a pane nobody can see.""" + with Session(SH, cols=120, rows=30) as s: + s.settle() + s.key("\\\\") + s.key("-") + s.settle() + top = by_pos(s)[1] + s.api("focus", id=top["id"]) + s.key("m") + s.settle() + check("one pane is away in the strip", len(widths(s)) == 2, + str(widths(s))) + + s.key("=") + s.settle() + seen = widths(s) + check("the two on screen share the tab evenly", + max(seen) - min(seen) <= 1, str(seen)) + + +def test_one_pane_has_nothing_to_even_out(): + with Session(SH, cols=80, rows=24) as s: + s.settle() + before = s.pane() + s.key("=") + s.settle() + snap = s.snapshot() + check("it says so rather than doing nothing quietly", + "nothing to even out" in snap.screen(), repr(snap.screen()[-200:])) + after = s.pane() + check("and the pane is untouched", + (after["x"], after["y"], after["w"], after["h"]) == + (before["x"], before["y"], before["w"], before["h"]), + str((before, after))) + + +def test_it_is_in_the_palette_and_the_cheatsheet(): + # Room for the whole sheet: it is sized from the screen, and a sheet + # clipped at the bottom would make this a test of the box. + with Session(SH, cols=110, rows=40) as s: + s.settle() + s.key("?") + s.settle() + sheet = s.snapshot().screen() + check("the cheatsheet lists it under size, with its key", + "= even out every split" in sheet, repr(sheet[:400])) + s.send("q") # any key closes the sheet + + s.key("p") + s.send("equalize") + s.settle() + pal = s.snapshot() + check("the palette finds it by its config name", + "even out every split" in pal.screen(), + repr(pal.screen()[:400])) + check("with the key it would have skipped", "C-a =" in pal.screen(), + repr(pal.screen()[:400])) + check("and the row runs it", + any(h["action"].startswith("run:") for h in pal.hits), + str([h["action"] for h in pal.hits][:8])) + + +if __name__ == "__main__": + test_a_nudged_row_of_columns_goes_back_to_even() + test_it_divides_a_split_between_the_panes_behind_it() + test_a_minimised_pane_is_not_counted() + test_one_pane_has_nothing_to_even_out() + test_it_is_in_the_palette_and_the_cheatsheet() + sys.exit(report()) |
