aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_shaders.py (follow)
Commit message (Collapse)AuthorAge
* slosh: floating panes and the drop grammaruser2026-08-20
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A pane can float: lifted out of the layout, drawn over the tiled panes and under the modals, freely moved and shaped. The model is a flag and a rect on the leaf, exactly like `minimized` -- the seat in the tree is kept, layout skips it, the siblings absorb its share, and un-floating returns it home. The drawn rect is the wanted one clamped to the tab every frame, never written back, so a terminal squeeze round-trips exactly; an explicit move or resize *does* write back, because the hand is the author of the intent. Floating pops the pane to the centre: the jump is what says it happened. Painted after everything tiled, so the hit list -- searched backwards -- resolves an overlap to the float you can see: the z-order and the click order are the same list. It casts a shadow (float_shadow, one number), and `floating` joins the states table above `unfocused`, so a float is never dimmed as merely-unfocused. The hands: the title moves it, any other border resizes it (a bottom corner on both axes; the grabbed edge pins at walls and floors, never sliding the window), H J K L move a focused float, the =/+ key grows it about its centre, `-` shrinks it (its split was a refusal anyway), and hovering shows which edges a grab would take, from the same derivation the press uses. Equalize moved to `0` to free the key; `C-a f` floats (finder went to `s`, where it reads as search); `C-a F` opens a new floating shell -- the throwaway terminal; the function row f1..f12 became nameable in configs along the way. Guards throughout: the last tiled pane cannot float or minimize away, closing it lands the top float, splits are refused with their own words, floats are not swap targets. Dropping got a grammar: while a pane is carried, the pane under the pointer subdivides into a swap centre and four edge bands meaning "insert me beside this pane, on this side" -- detach_leaf + place_beside, the surgery cross-tab moves already had. Bands are thirds (the first map of quarters showed the swap owning half of each axis), the hovered band fills the half the drop would hand over, the release resolves against the painted frame's hits so it cannot disagree with the fill, and a drop between siblings along the axis is a reorder that always fits -- refusing it was refusing the most ordinary drag there is. Also: rename-pane/rename-tab as palette actions (renaming was a double-click and nothing else, the one verb the palette could not reach), and C-u clears the rename editor like it clears the picker's query. Design log in PLAN-FLOATING-PANES.md; to fold into DESIGN.md as D22/M10-11.
* slosh: defaults most people can live in, said by shadersuser2026-08-19
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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().
* 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: dim the panes you are not in, and a knob to say how muchuser2026-08-15
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `dim_unfocused 60`, on by default, 0 to turn it off. The dimming itself was already possible -- `states { unfocused { dim } }` -- and deliberately not the default. The argument then was that ambient contrast is a taste, and shipping a taste as a default is how a tool earns a reputation for fighting you. That argument was mostly about the escape hatch: "turn it off" meant knowing the states block exists and what a shader chain is. A single number does not have that problem, so the objection goes away and the default can be the one most people want. The knob and the table are one mechanism -- the knob fills the unfocused chain in when the config has not -- so writing the chain by hand still wins, including writing an empty one. `states { unfocused { } }` means nothing at all, and is not quietly refilled, which needed the parser to distinguish "said nothing" from "said none". 60 by choice: white text lands on #c3c3c3, which reads as "not this one" without reading as "not available", because half the time you are reading the other pane. Picked by looking at 40, 55, 70, 90 and 120 rather than by taste alone. The tests that had to change are the interesting part. Six in test_shaders.py asserted "back to green after the drag", which is no longer what normal looks like -- and rewriting them to compare against a snapshot taken before the drag failed too, because a drag *moves focus*: press a title and that pane is yours, so the two panes legitimately swap appearances across one. They assert the rule now (the pane you are in keeps its colour, the others are pushed back), which is both what we mean and immune to the next change of defaults. test_states.py's "an unfocused pane is left alone" is inverted, with the reason written where the assertion used to be, because a test that flips should say why it flipped.
* sl0ppty: status_pad defaults to 4user2026-08-14
| | | | | | | | | | | | | | Wider than the panes' own margin (gap * gap_aspect, 2 at the defaults) rather than equal to it, so the strip and the line read as chrome sitting outside the layout instead of as another row of it. Picked by looking at 2, 3, 4 and 6 on a real screen; the difference between adjacent values is one column and is not something to choose from a config file. One test had an opinion about where the tab strip starts. It sampled a style at column 3 to prove the shader pass leaves the strip alone, which was true only because the strip happened to begin there; it now finds the strip's first painted column and asks about that. The assertion was never about column 3 and should not have said so.
* sl0ppty: the test cycle was 80s, it is now 5.5suser2026-08-14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Measured before changing anything, because the obvious suspect was wrong: the build is not slow. zig caches objects, so even a clean rebuild is ~0.15s and an incremental one is 0.08s. The C unit tests are 0.00s. All eighty seconds were in the python suite, and almost all of that was waiting on durations that were guesses. Four things, in order of what they bought. Run the python tests in parallel. They are separate processes that share nothing — script mode opens no socket — so each is now its own make target with a stamp, and `make -j` runs them at once. 78s -> 21s for free. The stamps are worth as much as the parallelism day to day: an untouched test whose binary has not changed does not run again, so `make test` after editing one test file is 1.1s and after editing nothing is 0.02s. The binary is a prerequisite of every stamp, so changing the code still re-runs everything. Then stop guessing at durations. `settle N` is the only thing that reads a pane's pty, so it is how you wait for a program's output — but `send` and `snapshot` are already synchronous with the app, which means a settle after a mouse event, waiting to assert on our own chrome, is waiting for nothing at all. Those are removed. Where a program's output really is the thing being waited for, harness.until()/until_text() polls in 10ms steps and returns the instant it lands: measured, that is one poll, against the 200-300ms those calls used to donate. The default settle drops 120ms -> 20ms on the same reasoning. Three effects are gated on a real clock rather than on anything observable — the hover dwell, and a toast's lifetime. Those genuinely have to wait, so the tests that are not *about* the timing now configure the clock down instead (hover_delay_ms 20, toast_ms 150) and the two tests that are about it keep the real defaults, because a dwell test that cannot observe not-yet-elapsed proves nothing. Two tests turned out to be passing by accident, which is the useful kind of thing a speedup shakes loose: both asserted the resize hint was gone after a drop while the pointer was still sitting on the handle. It was only gone because the dwell had not re-elapsed — resting on a handle *should* re-arm the hint. They now move off the handle first and assert the thing they meant. Unrelated but found on the way: the Makefile tracked no header dependencies, so editing a .h rebuilt nothing and the only safe move was `make clean`. -MMD -MP fixes it; touching shader.h now rebuilds exactly the six objects that include it. That one was a correctness bug wearing a performance costume. .fastfeedback/SPEED.md records the measurements and how to write a fast test here, so the next person does not rediscover that settle-after-a-click is free to delete.
* sl0ppty: pane states are a table, not three hardcoded knobsuser2026-08-14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0ppty: positional shaders, and a config block to ask for themuser2026-08-14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Six passes that are pure functions of where a cell is: vignette, gradient, zebra, ruler, margin, spotlight. The position was already in the context and nothing had used it; these are what it was for. The one worth having on its own merits is `ruler`. A column guide that costs no columns, needs no support from the program in the pane, and works in anything — an editor that has never heard of one, a pager, a shell. It marks the background only: a guide that recoloured the text would be making the code it marks harder to read, which is the opposite of the job. `margin` is the same idea from the other side, everything past a column receding so an over-long line visibly runs out of the pane you meant to use. `spotlight` is the first thing to need something new in the context. The cursor belongs to whichever pane is focused, so it is handed only to that pane — otherwise every other pane's light would chase a cursor somewhere else. It leaves everything inside its radius completely alone, because dimming the text you are actually working on to make it easier to find would be silly, and ramps rather than stepping at the edge. `vignette` and `spotlight` both measure distance with a row counting double, because a cell is about twice as tall as it is wide and an effect that claims to be round has to say so in cells. The layout's own centre-distance already does this. The test for it is that on a 41x21 pane — square on screen, not in cells — the top edge and the left edge come out equally dark. Scanlines are not a seventh shader. A CRT scanline is `zebra` with a band of one row, and shipping the same arithmetic under two names would be pretending they are different things. These are not policy, so they needed a way to be asked for, and a KDL block whose children are the passes gets it without inventing a syntax: order in the block is order in the chain, because a chain is a sequence, and listing a shader twice is how you get two rulers. Unknown names are refused with a warning rather than guessed at. Empty by default — these are yours to want, not opinions we hold.
* sl0ppty: dashed borders and a deeper push-back while dragginguser2026-08-14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two changes to the same moment, on opposite sides of the chrome boundary. Every pane that is not being dragged now draws a dashed border, because every one of them is somewhere the pane could be dropped and a dashed frame says so without needing a legend. The pane in your hand keeps a solid one, so the two states are distinguishable by shape and not only by colour — which matters on a terminal whose palette makes the greying subtle, and to anyone who would not see the greying at all. The pane actually under the pointer still takes the existing drop-target highlight on top of the dashes: these are all targets, that one is the one you are on. Borders are chrome, so this is not a shader and could not have been. It lives in draw_frame, keyed off the same drag.moved that gates the greying, so a press that turns out to be a click neither greys nor dashes anything. The greying itself gets a second pass: desaturated *and* darkened, rather than only desaturated. Grey alone still competes for attention at the same brightness, which is what made the dragged pane fail to pop. Two shaders instead of one that does both, because "colourless" and "recessed" are different things to want and want separate knobs — drag_grayscale 200 and drag_dim 140. This is the first thing to actually use the chain, and it is what the chain was for. The knob that used to mean "off" no longer does, and the test that asserted it caught that immediately: drag_grayscale 0 now leaves drag_dim still dimming. Rather than couple them, the test became three — both off, each alone — which is better coverage of a two-knob effect than the single case was of a one-knob one.
* sl0ppty: drag greys the other panes, and dim_unfocuseduser2026-08-14
The first two consumers of the shader pass, and between them they settle how a pane gets one. Dragging a pane by its title now greys every *other* pane, so the one that is moving lifts off the page — which is the thing that made a colour pass worth building. It engages only once the pointer has actually moved, because a press on a title that turns out to be a click would otherwise flash the whole session grey on its way to doing nothing. dim_unfocused fades everything but the focused pane and is off by default: an always-on effect is the kind you stop seeing and start fighting, so it should be asked for. Both are derived every frame rather than attached and cleared. Focus and drags move through far too many paths — hover, click, the finder, a close, a layout apply, any keystroke ending a drag — to keep an attachment in sync with, and a pane left grey by the one path that forgot is a bug that only turns up in front of someone. The rect and the collapsed flag are recomputed every pass for exactly this reason; this is the same rule applied to colour. It also makes the awkward cases free: dropping into the margin, a keystroke ending the drag, or the dragged pane being closed under it all un-grey without anywhere having to say so, and there are tests for each because "it turns off again" is the whole risk. The two policies replace rather than stack. Dragging an unfocused pane while dim_unfocused is on would otherwise give it two reasons to be grey, and they compound into one muddy grey that reads as neither; the drag wins, so the dragged pane is at full colour whatever the focus says. shade_apply no longer caps at SHADE_MAX. That cap was right when the only shaders were the ones a pane had attached, and wrong the moment the session derives one on top of them: a full stack would have silently dropped it. The count is the caller's to bound. The chrome check that mattered was already there: test_drop_target_is_visible samples a frame corner mid-drag, and it still passes, because the pass runs between the contents and the chrome drawn over them.