diff options
| author | user <user@clank> | 2026-08-14 14:25:13 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-14 14:25:13 +0200 |
| commit | 858234c05031a7bffc3ffe2a9b2aa4530d3ff0d7 (patch) | |
| tree | 951d60c94949dc43c468252142a93006663b9f76 /tests | |
| parent | sl0ppty: positional shaders, and a config block to ask for them (diff) | |
| download | slosh-858234c05031a7bffc3ffe2a9b2aa4530d3ff0d7.tar.gz slosh-858234c05031a7bffc3ffe2a9b2aa4530d3ff0d7.tar.xz slosh-858234c05031a7bffc3ffe2a9b2aa4530d3ff0d7.zip | |
sl0ppty: pane states are a table, not three hardcoded knobs
dim_unfocused, drag_grayscale and drag_dim were three knobs describing two
situations, and there was no way to say anything about the third, fourth or
fifth. They are replaced by a table: a state names a chain, and the chain is
exactly as expressive as the global `shaders` block because it is the same
parser.
states {
dragging { }
drop_target { grayscale amount=200; dim amount=140 }
suspended { grayscale amount=180; dim amount=90 }
scrolled { tint amount=30 color="#ffcc88" }
unfocused { dim amount=90 }
}
A pane is usually in several states at once, so exactly one wins: the first to
match, in a fixed order running from the most deliberate to the most ambient.
Fixed rather than read from the block, because it is a ranking by urgency
rather than a preference — a pane you are holding should not be recoloured by
anything, and a mode the whole screen is in outranks an ambient hint about one
pane. They do not stack, which is the same call the drag policy already had to
make by hand: two reasons to be grey compound into a muddy grey that reads as
neither. Defaults are the old drag behaviour and nothing else, so out of the
box this changes nothing.
drop_hover is now separable from drop_target. The pane under the pointer was
already distinguished by its border; it can now differ in colour too, and
defaults to the same chain so it does not have to.
Two states asked for did not survive contact with the code, and it is worth
writing down why:
`dead` cannot exist. app_reap() runs every iteration of the server loop, before
the paint, and closing the last pane closes the tab — so a pane whose program
exits is gone before a frame containing it is ever composed. A `dead` shader
would name something that could never draw. Making it real means keeping dead
panes until they are dismissed, which is a feature about lifetimes and not
about colour.
`suspended` could not be seen either, for a much smaller reason: draw_cb drew
its "press a key to run" label and returned before reaching the shader pass. It
falls through now. A pane that has not started is a state worth colouring, and
its label is the only contents it has.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_shaders.py | 163 |
1 files changed, 153 insertions, 10 deletions
diff --git a/tests/test_shaders.py b/tests/test_shaders.py index 5620e78..b3e92ee 100644 --- a/tests/test_shaders.py +++ b/tests/test_shaders.py @@ -29,6 +29,16 @@ def cfg(text): return f.name +def unfocused_cfg(amount): + return "states {\n unfocused { dim amount=%d }\n}\n" % amount + + +def drag_cfg(chain): + """Both drop states get the same chain, which is what the defaults do.""" + return "states {\n drop_target { %s }\n drop_hover { %s }\n}\n" % ( + chain, chain) + + def content_fg(snap, pane): """The colour of the pane's own first content cell.""" run = snap.style_at(pane["content_x"], pane["content_y"]) @@ -65,7 +75,7 @@ def release(s, x, y): s.send(rf"\e[<0;{x + 1};{y + 1}m") -# ---- dim_unfocused --------------------------------------------------------- +# ---- the unfocused state --------------------------------------------------- def test_off_by_default(): with Session(SH, cols=80, rows=14) as s: @@ -82,7 +92,7 @@ def test_off_by_default(): def test_dims_everything_but_the_focused_pane(): - with Session(SH, cols=80, rows=14, config=cfg("dim_unfocused 128\n")) as s: + with Session(SH, cols=80, rows=14, config=cfg(unfocused_cfg(128))) as s: s.settle(200) split(s) s.settle(200) @@ -100,7 +110,7 @@ def test_dims_everything_but_the_focused_pane(): def test_the_dimming_follows_focus(): - with Session(SH, cols=80, rows=14, config=cfg("dim_unfocused 128\n")) as s: + with Session(SH, cols=80, rows=14, config=cfg(unfocused_cfg(128))) as s: s.settle(200) split(s) s.settle(200) @@ -120,7 +130,7 @@ def test_the_dimming_follows_focus(): def test_dimming_never_touches_the_chrome(): - with Session(SH, cols=80, rows=14, config=cfg("dim_unfocused 200\n")) as s: + with Session(SH, cols=80, rows=14, config=cfg(unfocused_cfg(200))) as s: s.settle(200) split(s) s.settle(200) @@ -132,7 +142,7 @@ def test_dimming_never_touches_the_chrome(): "#" in str(snap.style_at(3, 1)), str(snap.style_at(3, 1))) -# ---- drag_grayscale -------------------------------------------------------- +# ---- the drag states ------------------------------------------------------- def test_dragging_greys_the_other_panes(): with Session(SH, cols=90, rows=16) as s: @@ -169,7 +179,7 @@ def test_dragging_greys_the_other_panes(): def test_full_strength_is_a_true_grey(): with Session(SH, cols=90, rows=16, - config=cfg("drag_grayscale 255\n")) as s: + config=cfg(drag_cfg("grayscale amount=255"))) as s: s.settle(200) left, right = split(s) s.settle(200) @@ -202,7 +212,7 @@ def test_a_press_that_never_moves_greys_nothing(): def test_the_drag_greying_outranks_the_focus_dimming(): """Two reasons to be grey would compound into one muddy grey.""" - with Session(SH, cols=90, rows=16, config=cfg("dim_unfocused 128\n")) as s: + with Session(SH, cols=90, rows=16, config=cfg(unfocused_cfg(128))) as s: s.settle(200) left, right = split(s) s.settle(200) @@ -235,18 +245,18 @@ def drag_and_sample(conf): def test_the_two_drag_knobs_are_independent(): - both_off = drag_and_sample("drag_grayscale 0\ndrag_dim 0\n") + both_off = drag_and_sample(drag_cfg("")) check("with both off a drag leaves every pane alone", both_off == GREEN, str(both_off)) # Desaturate only: channels converge, brightness roughly survives. - gray_only = drag_and_sample("drag_grayscale 255\ndrag_dim 0\n") + gray_only = drag_and_sample(drag_cfg("grayscale amount=255")) ch = [int(gray_only[i:i + 2], 16) for i in (1, 3, 5)] check("grayscale alone desaturates without darkening much", ch[0] == ch[1] == ch[2] and ch[1] > 0x60, str(gray_only)) # Darken only: the hue survives, the brightness does not. - dim_only = drag_and_sample("drag_grayscale 0\ndrag_dim 200\n") + dim_only = drag_and_sample(drag_cfg("dim amount=200")) ch = [int(dim_only[i:i + 2], 16) for i in (1, 3, 5)] check("dim alone darkens without desaturating", ch[0] == 0 and ch[2] == 0 and 0 < ch[1] < 0x60, str(dim_only)) @@ -415,6 +425,133 @@ def test_positional_shaders_stay_off_the_chrome(): frame_fg(snap, p) == "#ff5fd7", str(frame_fg(snap, p))) +# ---- states as a table ----------------------------------------------------- + +def lay(text): + f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False) + f.write(text) + f.close() + return f.name + + +def test_the_dragged_pane_can_have_a_state_of_its_own(): + conf = 'states {\n dragging { tint amount=255 color="#0000ff" }\n}\n' + with Session(SH, cols=90, rows=16, config=cfg(conf)) as s: + s.settle(200) + left, right = split(s) + s.settle(200) + press(s, left["x"] + 4, left["y"]) + motion(s, right["x"] + 5, right["y"] + 3) + s.settle(120) + snap = s.snapshot() + check("the pane in your hand takes the dragging state", + content_fg(snap, left) == "#0000ff", str(content_fg(snap, left))) + release(s, right["x"] + 5, right["y"] + 3) + s.settle(200) + check("and gives it back on release", + content_fg(s.snapshot(), s.panes()[0]) == GREEN, "") + + +def test_the_hovered_pane_is_a_different_state_from_the_rest(): + conf = ('states {\n' + ' drop_hover { tint amount=255 color="#ff0000" }\n' + ' drop_target { tint amount=255 color="#0000ff" }\n' + '}\n') + with Session(SH, cols=120, rows=20, config=cfg(conf)) as s: + s.settle(200) + s.key("\\\\") + s.settle(150) + s.key("\\\\") + s.settle(250) + panes = sorted(s.panes(), key=lambda p: p["x"]) + if len(panes) < 3: + check("three panes for the drop-state test", False, str(len(panes))) + return + a, b, c = panes + press(s, a["x"] + 4, a["y"]) + motion(s, b["x"] + 3, b["y"] + 3) + s.settle(150) + snap = s.snapshot() + check("the pane under the pointer is the hover state", + content_fg(snap, b) == "#ff0000", str(content_fg(snap, b))) + check("the other candidates are the plain target state", + content_fg(snap, c) == "#0000ff", str(content_fg(snap, c))) + release(s, b["x"] + 3, b["y"] + 3) + s.settle(150) + + +def test_a_suspended_pane_is_a_state(): + """It draws a label rather than a program, and that is still contents.""" + l = lay('layout {\n tab name="t" {\n pane\n' + ' pane suspended=true command="sleep 1"\n }\n}\n') + conf = 'states {\n suspended { tint amount=255 color="#00aaff" }\n}\n' + with Session(SH, cols=90, rows=14, config=cfg(conf), layout=l) as s: + s.settle(250) + panes = s.panes() + susp = [p for p in panes if p["suspended"]] + check("the layout made a suspended pane", len(susp) == 1, str(panes)) + if not susp: + return + snap = s.snapshot() + run = snap.style_at(susp[0]["content_x"] + susp[0]["content_w"] // 2, + susp[0]["content_y"] + susp[0]["content_h"] // 2) + check("a suspended pane reaches the shader pass at all", + (run or {}).get("fg") == "#00aaff", str(run)) + + +def test_scrollback_is_a_state(): + noisy = ["/bin/sh", "-c", + 'printf "\\033]2;p\\007"; i=0; while [ $i -lt 200 ]; do ' + 'printf "\\033[38;2;0;255;0mline %d\\033[0m\\n" $i; i=$((i+1)); done; ' + 'stty raw -echo; cat'] + conf = 'states {\n scrolled { tint amount=255 color="#ffaa00" }\n}\n' + with Session(noisy, cols=80, rows=14, config=cfg(conf)) as s: + s.settle(300) + p = s.pane() + check("not scrolled yet, so no state colour", + content_fg(s.snapshot(), p) == GREEN, + str(content_fg(s.snapshot(), p))) + # No scroll command exists, so drive it the way a person does: the + # wheel. SGR button 64 is wheel-up. + for _ in range(6): + s.send(rf"\e[<64;{p['content_x'] + 2};{p['content_y'] + 2}M") + s.settle(250) + check("looking at the past is a state you can colour", + content_fg(s.snapshot(), p) == "#ffaa00", + str(content_fg(s.snapshot(), p))) + + +def test_exactly_one_state_wins(): + """A pane is in several at once; the ranking decides, and does not stack.""" + l = lay('layout {\n tab name="t" {\n pane\n' + ' pane suspended=true command="sleep 1"\n }\n}\n') + conf = ('states {\n' + ' suspended { tint amount=255 color="#00aaff" }\n' + ' unfocused { tint amount=255 color="#ff0000" }\n' + '}\n') + with Session(SH, cols=90, rows=14, config=cfg(conf), layout=l) as s: + s.settle(250) + susp = [p for p in s.panes() if p["suspended"]] + if not susp or susp[0]["focused"]: + check("a suspended, unfocused pane to test with", bool(susp), + str(s.panes())) + return + run = s.snapshot().style_at( + susp[0]["content_x"] + susp[0]["content_w"] // 2, + susp[0]["content_y"] + susp[0]["content_h"] // 2) + check("suspended outranks unfocused, and they do not mix", + (run or {}).get("fg") == "#00aaff", str(run)) + + +def test_an_unknown_state_is_refused(): + conf = 'states {\n haunted { dim amount=255 }\n}\n' + with Session(SH, cols=60, rows=12, config=cfg(conf)) as s: + s.settle(200) + check("an unknown state name leaves the session running", + s.alive() and content_fg(s.snapshot(), s.pane()) == GREEN, + str(content_fg(s.snapshot(), s.pane()))) + + if __name__ == "__main__": test_off_by_default() test_dims_everything_but_the_focused_pane() @@ -435,4 +572,10 @@ if __name__ == "__main__": test_config_shaders_run_in_written_order() test_an_unknown_shader_is_refused_not_guessed() test_positional_shaders_stay_off_the_chrome() + test_the_dragged_pane_can_have_a_state_of_its_own() + test_the_hovered_pane_is_a_different_state_from_the_rest() + test_a_suspended_pane_is_a_state() + test_scrollback_is_a_state() + test_exactly_one_state_wins() + test_an_unknown_state_is_refused() sys.exit(report()) |
