aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-19 13:46:57 +0200
committeruser <user@clank>2026-08-19 13:46:57 +0200
commit42b3c10995eb67778d9dd3aa2fd80624896acd5f (patch)
tree751bcb8604967dc8687cf23fbeb1033aa26ca427 /tests
parentslosh: a website that is a page, not a mock session (diff)
downloadslosh-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.c19
-rw-r--r--tests/test_bell.py125
-rwxr-xr-xtests/test_chrome_shaders.py2
-rw-r--r--tests/test_config.py4
-rw-r--r--tests/test_drag.py2
-rw-r--r--tests/test_responsive.py2
-rw-r--r--tests/test_shaders.py4
-rw-r--r--tests/test_states.py113
-rw-r--r--tests/test_tabs.py2
-rw-r--r--tests/test_wide_marks.py5
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(
[