diff options
| author | user <user@clank> | 2026-08-15 23:06:32 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-15 23:06:32 +0200 |
| commit | f6281fbec93d92bfc7e9164492e436d0236d9179 (patch) | |
| tree | c78b435dec7f62c948e2e8144f29f47ccab2fa7d /README.md | |
| parent | sl0ppty: a command palette, on a key you choose (diff) | |
| download | slosh-f6281fbec93d92bfc7e9164492e436d0236d9179.tar.gz slosh-f6281fbec93d92bfc7e9164492e436d0236d9179.tar.xz slosh-f6281fbec93d92bfc7e9164492e436d0236d9179.zip | |
sl0ppty: shaders on the chrome, and two keys for the arrangement
Five things, in one commit because they land in the same few files --
the action table, the shader pass, config.kdl -- and splitting them by
hunk would buy a tidier log at the price of commits that do not build.
Each is verified end to end; the suite is 46 files green.
Chrome shaders. `where="chrome"` on any entry in `shaders` or in a
`states` block runs that pass over a pane's frame -- border, padding,
title, buttons -- instead of its contents. Nothing else changes: same
registry, same `amount`, same expressions, same chain order, one new
word, because a frame is another rect and so it is another pass. It runs
over the frame's whole rect with the contents punched out as a hole,
rather than four passes over four sides: an effect has to be able to
travel round a border, and four passes would each count x/y from zero
and restart at every corner. The hole keeps the two honest -- a skipped
cell is never visited, so a full-strength chrome tint cannot reach your
text. It is also the only kind of pass that can colour a pane a small
window has collapsed to a single row (D6), which is the state where a
colour is worth the most.
`channel="fg"|"bg"|"both"` came out of looking at the first border
flash: `tint` mixes both of a cell's colours, and a frame's background
is the terminal's own default, so the pass materialised it to our guess
at that default and mixed towards the colour -- a dark rectangle behind
the glyphs, left there until the pane changed state. The mask is
enforced by the pass, not the shader: the other colour is snapshotted
before materialising and put back after, so an unset colour goes back to
unset, it works for a built-in and a plugin alike, and no shader grows
an fg-only twin of itself.
A `bell` pane state and a `since` variable, because "flash the border
when a pane rings" needed both and neither existed. `since` is
milliseconds since the pane entered its current state, stamped in the
one function that decides the state -- a transition is the one thing a
frame cannot tell you, so it is remembered where it cannot disagree with
the state it dates. `bell` ranks above the ambient states and below
`dead`: a pane that rang while you were elsewhere is news, and a pane
that rang and then exited is a pane that exited. It ends when you look
at the pane, because that is what answering a bell is.
Animation needed a clock and did not have one: a shader reading `t` only
advanced when something else caused a frame, so a pulse froze the moment
you stopped typing. `app_next_deadline_ms` now reports `anim_ms` while a
chain that reads the clock ran over the last frame -- derived from what
was applied, so a pulse hung off `dead` costs a clock while a pane is
dead and nothing for the rest of the session. A `deadline` verb makes
that observable; the server's poll loop already lived on the number.
contrib/chrome has seven presets and the perimeter trick they are built
on: on a frame the coordinate you want is distance along the ring, which
the language can work out, and `min(d, P - d)` is what makes an effect
cross the corner it started from. Hand-written, unlike contrib/shaders,
because the toy previews contents and a frame has no preview to give --
so tests/test_chrome_presets.py checks each one the only way left: load
it into a real session, assert the frame moves and the contents do not.
`equalize` (C-a =) puts every share back to even: each split's children
weighted by how many visible panes are behind them, so one pane beside a
stack of three is a quarter of the width and "even" means the same thing
at every depth. Exactly equal cell counts are not something a tree can
always express, so the action promises the even division it can.
`rotate-layout` (C-a Space) turns the whole tab a quarter turn
clockwise. Every split changes axis and a row split's children reverse,
because the pane on top of a stack is the pane on the right once you
turn it; get that asymmetry wrong and you have a mirror image, which
looks almost right until four turns fail to come back. They do come
back, exactly, which is why the inverse is three more turns rather than
a second mirrored routine with its own chance of being wrong -- and why
a key as easy as space is the right one rather than a hazard. A turn is
refused when it would not fit: a share of the height is not always a
share the width can afford, and a node that cannot give its children
their floor takes the whole tab down to a list. So the turn is tried,
the layout is asked what it made of it, and one that flattened the tab
is put back with a word. Same rule `split_fits` applies to splitting.
Keys: the palette moves from `shift+p` to `p`, and cycling tabs from
`n`/`p` to tab/shift+tab. `p` is pressed far more often than "the tab
before this one" and was the only shifted letter in the defaults; tab is
what every other tabbed thing cycles with, and `\e[Z` decodes as
shift+tab on a plain terminal, so this costs nothing on a client without
the kitty protocol. `n` is free again. The actions are untouched: two
lines of config put the old binds back.
The shader ABI goes to 3: `shade_ctx_t` gained `state_ms` and struct
shader gained `channels`, both the pass's business rather than a
shader's. Plugins need a rebuild, which is what the version is for.
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 44 |
1 files changed, 40 insertions, 4 deletions
@@ -48,10 +48,12 @@ connection goes away. Reattach with the same command. | `C-a \` / `C-a -` | split into columns / rows | | `C-a h j k l` or arrows | move focus | | `C-a H J K L` or shift+arrows | move the boundary between panes | +| `C-a =` | give every visible pane an even share again | +| `C-a Space` | turn the whole layout a quarter turn (four brings it back) | | `C-a z` / `C-a m` | zoom a pane to fill the tab / minimise it | -| `C-a c` · `C-a n` `C-a p` · `C-a 1..9` | new tab · cycle · select | +| `C-a c` · `C-a Tab` `C-a shift+Tab` · `C-a 1..9` | new tab · cycle · select | | `C-a f` | find a pane by name | -| `C-a shift+p` | the command palette: every action by name | +| `C-a p` | the command palette: every action by name | | `C-a PgUp` `C-a PgDn` `C-a Home` `C-a End` | scrollback (the wheel works too) | | `C-a x` / `C-a r` | close a pane / re-run a finished one | | `C-a d` / `C-a q` | detach / quit | @@ -116,7 +118,7 @@ mistyped command in a fresh session no longer closes the session. ### The rest of it -- **Run a command without knowing its key.** `C-a shift+p` opens a palette of +- **Run a command without knowing its key.** `C-a p` opens a palette of every action there is, by phrase ("split into columns") or by the name a config file uses (`split-cols`). It shows the chord beside each one, so using it teaches the key that would have skipped it — and it lists actions @@ -187,7 +189,7 @@ shaders { } ``` -Integer arithmetic over `x y cols rows curx cury focused t`, with `min max abs +Integer arithmetic over `x y cols rows curx cury focused t since`, with `min max abs clamp dist` and comparisons that give 0 or 1 — so `(x < 10) * 200` is how you write a rule. Thirty-two ready-made ones are in [`contrib/shaders`](contrib/shaders) — a cursor line, a crosshair, a torch, a @@ -198,6 +200,40 @@ mixing in C and means the whole program can be worked out once into a per-cell map and reused: a shader you wrote in your config costs about what a compiled one does. +**The frame is a rect too**, so the same passes can run over a pane's chrome +instead of its contents — `where="chrome"` — and a border becomes somewhere to +say what a pane is doing, for no room and no words: + +```kdl +states { + // a dead pane's frame breathes red until you deal with it + dead { tint where="chrome" channel="fg" color="#ff0033" amount="abs((t / 8) % 510 - 255)" } + // a bell nobody has answered: a quarter-second flash, then gone + bell { tint where="chrome" channel="fg" color="#ffcc00" amount="(since < 250) * 255" } + // and the panes you are not in recede, frame and contents together + unfocused { dim amount=60; dim where="chrome" channel="fg" amount=90 } +} +``` + +`x`/`y` there are the *whole frame's*, so an effect travels round a border +rather than restarting at every corner, and the contents are cut out of the +pass — a full-strength chrome tint cannot reach your text. `channel="fg"` keeps +it a border rather than a slab: a frame's *background* is the terminal's own +default, and mixing that towards a colour paints a rectangle behind the glyphs +instead of colouring them. A pane that a small +window has collapsed to a single row is chrome all the way through, which +makes this the only kind of pass that can still colour it. Anything that reads +`t` also asks the session to keep painting (`anim_ms`, 20fps by default, and +only while such a shader is on screen), because a pulse that froze the moment +you stopped typing would just look broken. + +On a frame, the coordinate you usually want is not `x`/`y` but **distance along +the perimeter**, which the language can work out — and that is what makes a +shine travel round a border, a ripple leave a corner in both directions, or an +arc rotate. Seven of those are in [`contrib/chrome`](contrib/chrome): a shine, a +shimmer, ripples, a spinner, a static gradient, corner glints, and a sheen +across the title rule. + **You can add your own** without rebuilding sl0ppty: a shader is a C function from one cell to that cell's colours, and any `*.so` in `~/.config/sl0ppty/shaders/` that exports one is loaded at startup and named |
