aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_shader_expr.py (follow)
Commit message (Collapse)AuthorAge
* sl0ppty: ruff format the Pythonuser2026-08-17
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The bumpy one, again. Every `.py` outside `vendor/` plus the three `contrib/` scripts that are Python with no extension: 60 files, 6,714 lines rewritten, no behaviour changed. Nothing here is worth reading -- the next commit adds it to `.git-blame-ignore-revs` so `git blame` walks past it. It is a bigger reformat than the C was, 60% of the lines against 17%, and for a reason worth writing down rather than tuning away: ruff and black break a call that does not fit onto one argument per line, where this code aligned the continuation under the opening paren. The test suite is thousands of `check("some prose", cond, detail)` calls, so nearly every one of them changed shape and the suite grew 3,308 lines. Line length was the one lever that moved it -- 120 columns would have been a 40% reformat and no growth at all -- and it was left at 88, because picking an unusual width to dodge a one-time diff is paying forever to avoid paying once. Import sorting came with it. Two files had `import os, pty, uuid, select, struct, sys, termios, time, fcntl, signal` on one line and now have ten. Verified after, not before, because a formatter that alters behaviour would be worse than none: - `make retest`, 1801 checks green -- and this reformats the suite that does the checking, including `harness.py`, which every one of those files imports. - `make test-live`, the tty tests the fast suite skips. They matter here more than usual: `live_input.py` and `live_m0.py` are two of the files whose comma-imports were split, and nothing in the fast suite would have noticed if that broke them. - All 60 files still compile, `make fmt` twice in a row changes nothing the second time, and `ruff check --select I` has nothing left to say.
* sl0ppty: zero strength is not a passuser2026-08-16
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `shader_abi.h` has said `0 is identity` from the start, and the pass did not honour it: it materialised a cell's default colours -- resolving "whatever the terminal draws" into our `default_fg`/`default_bg` -- and only then called a shader that was contractually going to change nothing. So a chain that is currently *off* still repainted. `focused * 200` on an unfocused pane, `(since < 500) * 255` half a second later, `(x < 10) * 200` from column ten on: every one of those wrote our idea of the default over the terminal's own. Visible whenever the two differ, which for a real terminal theme is always, and invisible in the test suite because the tests compared against the materialised value. Found by measuring, not by reading: I was checking that each example I was about to hand over actually changed the pane, and one that should have changed nothing changed eight style runs. The fix is an ordering: work out the strength, and if it is zero leave the cell before touching it. A constant-zero shader now skips its whole rect before the loops. Cheaper too -- a pass that is off costs no materialisation and no call -- though that is a side effect and not the reason. Three test changes, all of them the same correction. `tests/shader_test.c`'s traversal probe was built `{.kind, .fn}` with `amount` defaulting to 0, so it was asking to be called for every cell at zero strength; it now asks for 255, which is what those checks are about. A new block pins the rule: a zero-strength pass visits nothing and leaves an unset cell unset. And four checks in `tests/test_shader_expr.py` asserted `== "#ffffff"` for cells outside an effect's band -- the materialised default. They now assert the cell has no colour of its own at all, which is the stronger claim and the one that would have caught this.
* sl0ppty: a shader's strength can be an expressionuser2026-08-14
Adding an effect meant writing C -- in the binary, or since D15 in a shared library. Both are a lot of ceremony for "dim every other row". So: `amount` may be an expression, compiled to bytecode when the config is read and evaluated per cell. shaders { dim amount="(y % 2) * 40" // scanlines dim amount="(x > cols - 10) * 120" // a right margin tint amount="255 - dist(x, y, curx, cury) * 12" color="#ff5fd7" } The scope is the argument, and that is the whole design. Every built-in here is already colour_op(amount) with the amount a pure function of position -- vignette, zebra, ruler, margin and spotlight differ only in that function -- so the part worth handing to a config is the *number*, not the colour maths. Three things follow at once: the mixing stays in C where it is fast and correct; an expression cannot produce an invalid cell, because it produces one byte; and that byte is cacheable. The cache is what makes an interpreter affordable, and the numbers are why this exists at all. Measured on a 200x50 pane, one pass: native vignette 6.1 ns/cell constant expression, folded 5.6 ns/cell positional expression, mapped 5.2 ns/cell animated (reads the clock) 35.0 ns/cell A shader you wrote in your config costs what a compiled one does. A program that does not read the clock is evaluated once per cell into a byte map and reused until something it reads changes; one that does read the clock is interpreted per cell, per frame, which is the honest price of animation and is opt-in. (The earlier measurement of 56ns for interpretation was per evaluation -- correct, and the reason the map exists rather than an argument against the language.) What a program reads is derived by the compiler rather than declared, and that dependency set *is* the cache key. A stale map is therefore unrepresentable rather than merely unlikely -- the same rule the layout follows, arrived at from the other direction. A unit test caught the first violation of it before anything else did: `cols` and `rows` carried no dependency flag, on the theory that the key always contains the rect, and so a program reading only the size looked constant and was folded at compile time, where every size is zero. The language is deliberately total. No loops, no recursion, no jumps: even `?:` compiles to evaluating both sides and selecting, because nothing in it has an effect. Evaluation is O(program length), so a config cannot spin, and there is nothing to sandbox. Division by zero is 0 rather than a trap: an expression that is wrong should look wrong, not take the session with it. One that does not compile drops that one shader with a warning -- not the config, and not the half-strength version of what you asked for, which is what the number path did with garbage and is now also fixed. shader_t gained the compiled program, so the plugin ABI goes to 2. A plugin built against 1 is refused with "shader ABI 1, we speak 2" and needs nothing but a rebuild -- the check earning its keep the first time it was asked to.