diff options
| author | user <user@clank> | 2026-08-19 13:46:57 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-19 13:46:57 +0200 |
| commit | 42b3c10995eb67778d9dd3aa2fd80624896acd5f (patch) | |
| tree | 751bcb8604967dc8687cf23fbeb1033aa26ca427 /tests | |
| parent | slosh: a website that is a page, not a mock session (diff) | |
| download | slosh-42b3c10995eb67778d9dd3aa2fd80624896acd5f.tar.gz slosh-42b3c10995eb67778d9dd3aa2fd80624896acd5f.tar.xz slosh-42b3c10995eb67778d9dd3aa2fd80624896acd5f.zip | |
slosh: defaults most people can live in, said by shaders
The out-of-the-box look, rethought as one piece:
- the compiled-in palette calms down: muted blue #7aa2f7 on the same
neutral darks, with the colours that mean something kept semantic --
amber bell, red-orange dead. sl0p stays one include away as the house
style, and contrib/themes/default.kdl is the starting point written
down (theme-tour shows it).
- derived state defaults now actually follow the theme: the scrolled
wash is rebuilt from scroll_bg after the theme block (it used to bake
the compiled accent), and declaring a state -- in any file of an
include chain -- still wins. The dump writes derived states as
comments so C-a e cannot pin what is meant to keep following.
- a bell you can see: the frame blinks three times in theme's bell
colour then breathes until answered, and a soft diagonal shimmer --
the bell colour halved towards default_fg, width breathing on a sine
-- crosses the body for the same 900ms. bell_indicator false silences
all of it; states { bell { } } replaces it like any state. since
carries the clock dependency, so an idle session pays nothing.
- scrollback says where it continues: while scrolled, the viewport's
top and bottom rows melt towards an edge with content past it and
turn solid at the buffer's end. New expression variables above/below
(lines hidden past each edge, own dependency bit, cached per scroll
position), carried in shade_ctx_t -- SLOSH_SHADER_ABI 3 -> 4.
- expressions survive the dump: a program keeps its source and cb_chain
writes it back, so amount="(y % 2) * 40" no longer dumps as a number.
- headers move to include/, -Iinclude everywhere; the plugin Makefile's
SLOSH var becomes SLOSH_DIR because slosh exports SLOSH=1 into every
pane it runs, and building from inside a session read that as a path.
- comment drift culled where features outran the prose, and the
derivation boilerplate folded into derived_pass()/ctx_scroll().
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/expr_test.c | 19 | ||||
| -rw-r--r-- | tests/test_bell.py | 125 | ||||
| -rwxr-xr-x | tests/test_chrome_shaders.py | 2 | ||||
| -rw-r--r-- | tests/test_config.py | 4 | ||||
| -rw-r--r-- | tests/test_drag.py | 2 | ||||
| -rw-r--r-- | tests/test_responsive.py | 2 | ||||
| -rw-r--r-- | tests/test_shaders.py | 4 | ||||
| -rw-r--r-- | tests/test_states.py | 113 | ||||
| -rw-r--r-- | tests/test_tabs.py | 2 | ||||
| -rw-r--r-- | tests/test_wide_marks.py | 5 |
10 files changed, 269 insertions, 9 deletions
diff --git a/tests/expr_test.c b/tests/expr_test.c index 19968d1..4745ca3 100644 --- a/tests/expr_test.c +++ b/tests/expr_test.c @@ -286,6 +286,25 @@ int main(void) { refused("a ternary without its colon", "1 ? 2"); refused("something that is not an expression at all", "@@@"); + printf("\n-- the source survives compilation\n"); + { + /* The dump has to write back the line somebody wrote: bytecode cannot be + * un-compiled, so the program keeps its text. Byte for byte, including a + * constant expression -- folding must not eat the spelling. */ + const char *src = "(y % 2) * 40"; + char err[128] = {0}; + expr_prog_t *p = expr_compile(src, err, sizeof err); + ok("a program remembers its source", + p && expr_source(p) && strcmp(expr_source(p), src) == 0, + p ? expr_source(p) : err); + if (p) expr_free(p); + p = expr_compile("1 + 2", err, sizeof err); + ok("even one that folded to a constant", + p && expr_source(p) && strcmp(expr_source(p), "1 + 2") == 0, + p ? expr_source(p) : err); + if (p) expr_free(p); + } + printf("\n-- totality: there is no loop to write\n"); { /* The language has no way to express unbounded work, so this is a diff --git a/tests/test_bell.py b/tests/test_bell.py index b152835..64b58aa 100644 --- a/tests/test_bell.py +++ b/tests/test_bell.py @@ -51,6 +51,129 @@ def titlebar(snap, p, n=10): return snap.line(p["y"])[p["x"] : p["x"] + n] +def frame_fg(snap, p): + """The left border, one row below the corner: a cell that is frame and + nothing else, clear of the title, the mark and the buttons.""" + return (snap.style_at(p["x"], p["y"] + 1) or {}).get("fg") + + +BELL = "#e0af68" # the default theme's bell colour +FRAME_IDLE = "#45454a" + + +def test_a_bell_flashes_the_frame_by_default(): + """The mark is one cell wide, which is not news you can see from the + corner of your eye -- so by default the frame says it too: full-strength + blinks in theme's bell colour, then a breathe until it is answered. The + blink windows are 150ms, so polling catches one; at full strength the + tint *is* the bell colour, which is what makes the assertion exact.""" + l = lay(TWO_PANES) + with Session(SH, cols=90, rows=16, layout=l) as s: + s.settle(30) + mine = [p for p in s.panes() if p["focused"]][0] + other = [p for p in s.panes() if not p["focused"]][0] + check( + "a frame nobody rang is quiet", + frame_fg(s.snapshot(), other) == FRAME_IDLE, + str(frame_fg(s.snapshot(), other)), + ) + ring(s, other["id"], mine["id"]) + snap = s.until(lambda sn: frame_fg(sn, other) == BELL) + check( + "the frame of the pane that rang lights up in the bell colour", + frame_fg(snap, other) == BELL, + str(frame_fg(snap, other)), + ) + + # The body shimmer: one soft diagonal sheen of the bell colour + # gliding across the pane over the first 900ms. A blank cell's + # background is black until the band reaches it, so "the shimmer + # passed here" is a background that is not. + cx = other["content_x"] + other["content_w"] // 2 + cy = other["content_y"] + other["content_h"] // 2 + + def body_bg(sn): + return (sn.style_at(cx + 6, cy) or {}).get("bg") + + snap = s.until(lambda sn: body_bg(sn) not in (None, "#000000")) + check( + "a shimmer sweeps the body of the pane that rang", + body_bg(snap) not in (None, "#000000"), + str(body_bg(snap)), + ) + # ...and it is temporary: past 900ms the body is clean again while + # the bell still stands -- asserted on the mark rather than the frame, + # because the mark is state and the frame is mid-animation. The + # announcement ends; the reminder stays. + s.settle(1100) + snap = s.snapshot() + check( + "and it is gone again while the bell still stands", + body_bg(snap) in (None, "#000000") and MARK in titlebar(snap, other), + f"body {body_bg(snap)}, titlebar {titlebar(snap, other)!r}", + ) + # Answering is looking, same as the mark: the flash must end with it, + # not on its own schedule. + s.api("focus", id=other["id"]) + s.settle(30) + snap = s.until(lambda sn: frame_fg(sn, other) not in (BELL, None)) + got = frame_fg(snap, other) + check("and looking at it puts the flash out", got != BELL, str(got)) + os.unlink(l) + + +def test_the_flash_follows_the_theme_and_the_indicator(): + """Derived, not stored: theme's `bell` moves the flash with the mark, and + `bell_indicator false` -- whose promise is silent *and* invisible -- takes + the flash with it.""" + l = lay(TWO_PANES) + c = cfg('theme {\n bell "#00ff00"\n}\n') + with Session(SH, cols=90, rows=16, layout=l, config=c) as s: + s.settle(30) + mine = [p for p in s.panes() if p["focused"]][0] + other = [p for p in s.panes() if not p["focused"]][0] + ring(s, other["id"], mine["id"]) + snap = s.until(lambda sn: frame_fg(sn, other) == "#00ff00") + check( + "the flash is in the theme's bell colour", + frame_fg(snap, other) == "#00ff00", + str(frame_fg(snap, other)), + ) + os.unlink(c) + + c = cfg("bell_indicator false\n") + with Session(SH, cols=90, rows=16, layout=l, config=c) as s: + s.settle(30) + mine = [p for p in s.panes() if p["focused"]][0] + other = [p for p in s.panes() if not p["focused"]][0] + ring(s, other["id"], mine["id"]) + # The first two blink windows: if it were going to flash, this is when. + for _ in range(4): + s.settle(100) + got = frame_fg(s.snapshot(), other) + if got != FRAME_IDLE: + break + check( + "bell_indicator false silences the frame too", got == FRAME_IDLE, str(got) + ) + os.unlink(c) + + c = cfg("states { bell { } }\n") + with Session(SH, cols=90, rows=16, layout=l, config=c) as s: + s.settle(30) + mine = [p for p in s.panes() if p["focused"]][0] + other = [p for p in s.panes() if not p["focused"]][0] + ring(s, other["id"], mine["id"]) + for _ in range(4): + s.settle(100) + got = frame_fg(s.snapshot(), other) + if got != FRAME_IDLE: + break + check("an empty bell state means nothing at all", got == FRAME_IDLE, str(got)) + os.unlink(l) + os.unlink(c) + + def test_a_bel_marks_the_pane_that_rang(): l = lay(TWO_PANES) with Session(SH, cols=90, rows=16, layout=l) as s: @@ -275,6 +398,8 @@ def test_a_minimised_pane_rings_on_the_bar(): if __name__ == "__main__": + test_a_bell_flashes_the_frame_by_default() + test_the_flash_follows_the_theme_and_the_indicator() test_a_bel_marks_the_pane_that_rang() test_looking_at_it_is_what_puts_it_out() test_a_bell_in_another_tab_shows_on_the_strip() diff --git a/tests/test_chrome_shaders.py b/tests/test_chrome_shaders.py index 7bbcfb2..feec133 100755 --- a/tests/test_chrome_shaders.py +++ b/tests/test_chrome_shaders.py @@ -23,7 +23,7 @@ from harness import Session, check, report # A pane that paints a known, non-default colour into its own content. GREEN = "#00ff00" -FRAME = "#ff5fd7" # the default focused frame colour +FRAME = "#7aa2f7" # the default focused frame colour DIM_FRAME = "#45454a" # ...and the colour of a frame you are not in SH = [ "/bin/sh", diff --git a/tests/test_config.py b/tests/test_config.py index 1e4cff0..cb80829 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -562,7 +562,7 @@ def test_every_surface_has_its_own_theme_name(): p = s.pane() check( "the focused frame keeps its own colour", - (snap.style_at(p["x"], p["y"] + 1) or {}).get("fg") == "#ff5fd7", + (snap.style_at(p["x"], p["y"] + 1) or {}).get("fg") == "#7aa2f7", str(snap.style_at(p["x"], p["y"] + 1)), ) @@ -587,7 +587,7 @@ def test_an_unknown_theme_name_is_refused_not_ignored(): p = s.pane() check( "and the compiled-in default is kept", - (s.snapshot().style_at(p["x"], p["y"] + 1) or {}).get("fg") == "#ff5fd7", + (s.snapshot().style_at(p["x"], p["y"] + 1) or {}).get("fg") == "#7aa2f7", str(s.snapshot().style_at(p["x"], p["y"] + 1)), ) os.unlink(conf) diff --git a/tests/test_drag.py b/tests/test_drag.py index e301fac..3265caf 100644 --- a/tests/test_drag.py +++ b/tests/test_drag.py @@ -282,7 +282,7 @@ def test_drop_target_is_visible(): run = snap.style_at(right["x"], right["y"]) check( "the drop target is highlighted while dragging", - run and run["fg"] == "#ff5fd7" and "bold" in run["attrs"], + run and run["fg"] == "#7aa2f7" and "bold" in run["attrs"], str(run), ) s.send(rf"\e[<0;{right['x'] + 5};{right['y'] + 3}m") diff --git a/tests/test_responsive.py b/tests/test_responsive.py index d3dc90d..fe88d4d 100644 --- a/tests/test_responsive.py +++ b/tests/test_responsive.py @@ -361,7 +361,7 @@ def test_a_tab_is_laid_out_or_it_is_a_list_never_both(): # ---- hovering a row in the stack ------------------------------------------- -FRAME_FOCUS = "#ff5fd7" # the rule, while hovered +FRAME_FOCUS = "#7aa2f7" # the rule, while hovered TITLE_FOCUS = "#ffffff" # the title, while hovered FRAME_IDLE = "#45454a" diff --git a/tests/test_shaders.py b/tests/test_shaders.py index 7afd643..56001a6 100644 --- a/tests/test_shaders.py +++ b/tests/test_shaders.py @@ -407,7 +407,7 @@ def test_the_hovered_target_is_still_highlighted_over_the_dashes(): run = snap.style_at(right["x"], right["y"]) check( "the pane under the pointer keeps its drop highlight", - run and run["fg"] == "#ff5fd7" and "bold" in run["attrs"], + run and run["fg"] == "#7aa2f7" and "bold" in run["attrs"], str(run), ) release(s, right["x"] + 5, right["y"] + 3) @@ -503,7 +503,7 @@ def test_positional_shaders_stay_off_the_chrome(): snap = s.snapshot() check( "a full-strength positional stack still leaves the frame alone", - frame_fg(snap, p) == "#ff5fd7", + frame_fg(snap, p) == "#7aa2f7", str(frame_fg(snap, p)), ) diff --git a/tests/test_states.py b/tests/test_states.py index ce32ac3..a0f47f2 100644 --- a/tests/test_states.py +++ b/tests/test_states.py @@ -116,6 +116,56 @@ def test_scrollback_is_coloured_by_default(): ) +def test_scrollback_fades_at_the_edges_that_continue(): + """While scrolled, the viewport's top and bottom rows melt towards the + edge -- the way a scrollable list fades where it continues -- and each + fade exists only while there is content past that edge: `(above > 0)` is + a binary gate, so hitting the top of the buffer turns the top edge solid. + Reaching the bottom ends the scrolled state entirely, which is the same + statement about the other edge.""" + with Session(SH, cols=60, rows=20, config=cfg()) as s: + s.until_text("line 39") + pane = s.pane() + mid = pane["content_h"] // 2 + last = pane["content_h"] - 1 + + s.send(r"\x01\x1b[5~") # C-a PgUp: mid-buffer, content both ways + s.settle(30) + snap = s.snapshot() + top, m, bot = (fg(snap, pane, row=r) for r in (0, mid, last)) + check( + "mid-buffer, the top rows melt into the edge", + top != m, + f"top {top}, mid {m}", + ) + check("...and the bottom rows too", bot != m, f"bottom {bot}, mid {m}") + + s.send(r"\x01\x1b[H") # C-a Home: the very top of the buffer + s.settle(30) + snap = s.snapshot() + top, m, bot = (fg(snap, pane, row=r) for r in (0, mid, last)) + check( + "at the top of the buffer the top edge turns solid", + top == m, + f"top {top}, mid {m}", + ) + check( + "while the bottom still says the present is below", + bot != m, + f"bottom {bot}, mid {m}", + ) + + s.send(r"\x01\x1b[F") # C-a End: back to the present + s.settle(30) + snap = s.snapshot() + top, m, bot = (fg(snap, pane, row=r) for r in (0, mid, last)) + check( + "and the present is opaque everywhere", + top == m == bot, + f"top {top}, mid {m}, bottom {bot}", + ) + + def test_an_unfocused_pane_is_dimmed_by_default(): """This used to assert the opposite, deliberately: ambient contrast is a taste, and the argument against shipping a taste was that turning it off @@ -184,6 +234,69 @@ def test_a_states_block_beats_the_knob(): os.unlink(conf.name) +def test_the_scrolled_wash_follows_the_theme(): + """The wash over scrollback is theme's scroll_bg, and that promise is only + kept if the wash is derived *after* the theme block is read: it used to be + baked from the compiled palette, so every theme shipped left the default + accent smeared over its scrollback. A chain somebody wrote still stands, + whichever file of an include chain it was written in.""" + + def scrolled_fg(text, include=None): + d = tempfile.mkdtemp(prefix="slosh-scrolled-") + if include is not None: + open(os.path.join(d, "inc.kdl"), "w").write(include) + conf = os.path.join(d, "config.kdl") + # One theme block per file: the loader reads the first node by name, + # so a second `theme { }` would be a block that quietly says nothing. + open(conf, "w").write(text) + with Session(SH, cols=60, rows=12, config=conf) as s: + s.until_text("line 39") + pane = s.pane() + s.send(r"\x01\x1b[5~") # C-a PgUp: into the scrollback + s.settle(20) + # Mid-viewport, clear of the derived edge fades (three rows a + # side): this test is about the wash, and the wash alone. + got = fg(s.snapshot(), pane, row=pane["content_h"] // 2) + return got + + # White text tinted 22/255 towards pure green keeps green at full and + # pulls the other channels down: green stays the largest channel, which is + # the assertion, so the exact rounding stays the implementation's. + def greenish(c): + return c is not None and c[3:5] == "ff" and c[1:3] < "ff" + + base = ( + "theme {\n" + ' default_fg "#ffffff"\n' + ' default_bg "#000000"\n' + ' scroll_bg "#00ff00"\n' + "}\n" + ) + + got = scrolled_fg(base) + check( + "a theme's scroll_bg moves the wash with the indicator", greenish(got), str(got) + ) + + got = scrolled_fg( + base + 'states { scrolled { tint amount=255 color="#ff0000" } }\n' + ) + check("a chain you wrote beats the derivation", got == "#ff0000", str(got)) + + got = scrolled_fg('include "inc.kdl"\n', include=base) + check("it follows a theme brought in by an include", greenish(got), str(got)) + + got = scrolled_fg( + 'include "inc.kdl"\n' + base, + include='states { scrolled { tint amount=255 color="#ff0000" } }\n', + ) + check( + "and a chain written in an include is not re-derived over", + got == "#ff0000", + str(got), + ) + + def test_a_flattened_tab_still_says_which_panes_are_not_live(): """A collapsed header is chrome, and the shader pass never touches chrome (D13) — so the words have to carry what the colour carries everywhere diff --git a/tests/test_tabs.py b/tests/test_tabs.py index 3ae570a..0583ad3 100644 --- a/tests/test_tabs.py +++ b/tests/test_tabs.py @@ -309,7 +309,7 @@ def test_json_api(): # ---- hovering the strip ---------------------------------------------------- -ACCENT = "#ff5fd7" # the fill of the tab you are in, and hover on the rest +ACCENT = "#7aa2f7" # the fill of the tab you are in, and hover on the rest INK = "#141418" # text on that fill BRIGHT = "#ffffff" # that text while the pointer is on it DIM = "#45454a" # a tab you are not in diff --git a/tests/test_wide_marks.py b/tests/test_wide_marks.py index 9438a64..11ea5f6 100644 --- a/tests/test_wide_marks.py +++ b/tests/test_wide_marks.py @@ -39,7 +39,10 @@ def run(mark, cols=100, rows=14): cfg = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False) # An unmistakable bell colour: the assertion is "how many columns did the # mark claim", which means finding those columns without guessing. - cfg.write('bell_mark "%s"\ntheme { bell "#00ff00" }\n' % mark) + # ...and without the default bell flash: it paints the whole frame in + # that same colour at full strength, which would make "the first column + # that is not the bell colour" the far corner of the pane. + cfg.write('bell_mark "%s"\ntheme { bell "#00ff00" }\nstates { bell { } }\n' % mark) cfg.close() p = subprocess.run( [ |
