1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
|
# Shaders
A colour pass over a pane's cells: a pure function from (cell, position,
strength) to that cell's colours. It runs after the pane's contents are composed
and before the chrome that goes over them, which is why "contents, not chrome"
falls out of the paint order rather than needing a rule. For the frame, see
[chrome shaders](chrome.md).
Cells only — foreground, background, attributes. Text and width are never
touched, because rewriting text would desync selection and copy.
```kdl
shaders { // every pane gets these, in the order written
vignette amount=70
ruler amount=60 at=80 color="#7aa2f7"
}
```
A chain is a sequence: `grayscale` then `tint` is not `tint` then `grayscale`.
Listing a shader twice is how you get two of it. An unknown name is refused with
a word and skipped, never guessed at.
## The built-ins
| shader | takes | does |
|---|---|---|
| `dim` | `amount` | darken towards black |
| `grayscale` | `amount` | drain colour, weighted for the eye |
| `tint` | `amount` `color=` | pull towards a colour |
| `vignette` | `amount` | darken towards the pane's edges |
| `gradient` | `amount` `direction=0..3` | fade towards the background |
| `zebra` | `amount` `band=1` | darken alternate bands; `band=1` is a scanline |
| `ruler` | `amount` `at=80` `color=` | mark a column, background only |
| `margin` | `amount` `at=100` | everything past a column recedes |
| `spotlight` | `amount` `radius=12` | brightness falls away from the cursor |
`amount` is 0..255 and defaults to 128. Two props belong to the *pass* rather
than the shader and work on any of them: `where="content"|"chrome"` and
`channel="both"|"fg"|"bg"`.
## Strength as an expression
`amount` can be an expression instead of a number, evaluated for every cell.
That is how you get an effect nobody built in, without building anything:
```kdl
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="#7aa2f7"
}
```
| | |
|---|---|
| variables | `x` `y` — the cell, inside the rect the pass runs over |
| | `cols` `rows` — the size of that rect |
| | `curx` `cury` `cursor` — the cursor, and whether this pane has it |
| | `focused` — 0 or 1 |
| | `t` — milliseconds, for animation |
| | `since` — milliseconds the pane has been in its current state |
| | `above` `below` — lines of scrollback hidden past the viewport's top and bottom edges, both 0 at the present |
| operators | `+ - * / %` (integer; division by zero is 0, not a crash) |
| | `< > <= >= == != && \|\| !` give 0 or 1, so `(x < 10) * 200` is a rule |
| | `& \| ^ ~ << >>` on the 32-bit value, binding **tighter** than comparison — so `x & 7 == 0` asks what it looks like, unlike C |
| | `a ? b : c` — both sides are evaluated, then one is chosen |
| functions | `min(a,b)` `max(a,b)` `abs(a)` `clamp(v,lo,hi)` |
| | `dist(x1,y1,x2,y2)` — counts a row double, because a cell is about twice as tall as it is wide |
| | `sin(deg)` `cos(deg)` — **degrees** in, −255..255 out, so `128 + sin(t / 4) / 2` is a breathe |
| constants | `PI` = 180, `TAU` = 360 — pi as an *angle*, which is what it is here: a half turn |
`PI` being a half turn is what makes a radian-shaped formula port across as
written: `sin(TAU * x / cols)` is one cycle over the pane's width, `PI / 2` is a
quarter turn, `PI / 6` is thirty degrees. It is also *more* exact than radians
could be in a language with no fractions — a sixth of a turn is 30 whole degrees,
where `3.14159 / 6` in integers is 0. For the same reason there is no `deg2rad`
or `rad2deg`: there is one angle unit, and a conversion could only lose the angle
or invent a second scale to disagree with.
There are no loops and no recursion, on purpose: a config cannot spin and there
is nothing to sandbox. An expression that does not compile drops that one shader
with a warning — never the config, and never a half-strength version of what you
asked for.
The expression produces the **strength**, never the colour. That keeps the mixing
in C, gives an expression no way to produce an invalid cell, and makes the result
cacheable: a program that does not read the clock is evaluated once per cell into
a map and reused, so a shader you wrote in your config costs about what a
compiled one does. Reading `t` or `since` costs a per-cell evaluation every
frame, which is the honest price of animation.
## Ready-made
Thirty-two presets are in
`contrib/shaders` —
a cursor line, a crosshair, a torch, a phosphor CRT, sonar pings that follow
your cursor. Each is a file you can paste into your config or `include`.
`contrib/shader-tour` cycles a running session through them, and
`contrib/shadertoy.html` previews the language in a browser (a test cross-checks
that preview against the real compiler, so it cannot lie to you).
## Prototyping in a pane
Edit, save, look, guess again is a slow way to arrive at a colour. A program can
set the chains for the pane it is running in, in the same syntax the config uses:
```bash
printf '\033]5577;1;shader;chrome;tint color="#7aa2f7" amount="abs(t / 8 %% 510 - 255)"\033\\'
printf '\033]5577;1;shader;content;dim amount=90\033\\'
printf '\033]5577;1;shader;chrome;\033\\' # that rect: back to normal
printf '\033]5577;1;shader;\033\\' # no rect named, so both of them
```
The field after `shader` is which rect an entry means **when it does not say** --
an entry's own `where=` wins -- and the rest of the payload is a document in the
config's syntax: one entry, several separated by `;`, or a whole `shaders { }` block.
What it says replaces both chains, as naming the block in a config does. The reply
counts what went where. The session answers on the program's stdin, `\033]5577;1;shader-reply;ok\033\\`
or `shader-reply;error;bad amount for tint: ...`, so a typo says so instead of
looking like a shader that does nothing.
**Three ways to take it all back**, because the program that painted a pane is
not always in a state to put it back:
| | |
|---|---|
| `printf '\033]5577;1;shader;\033\\'` | from the program, one exchange, both rects |
| `printf '\033]5577;1;shader-load;/path/to/preset.kdl\033\\'` | a `shaders { }` file, read by the session and routed by `where=` |
| `slosh cmd '{"cmd":"clear-shaders","id":3}'` | from outside; `id` 0 or absent means the focused pane |
| the `clear-shaders` action | on a key you bind, or from the palette |
None of them is gated on `in_band_shaders`. A chain can outlive the setting that
allowed it -- paint a pane, then turn the setting off -- and a way out that the
setting can take away is not one. They clear what the *pane* set: the config's own
chains and the session's own dimming are not this pane's doing.
`contrib/shader-repl` is that loop with a prompt on it, and **what you type at it
is what a config file says** -- not a dialect of it:
```
chrome> tint amount=200 one entry, for the rect the prompt names
chrome> tint where="content" amount=200 ...or the one the entry names, which wins
chrome> dim amount=90; tint amount=40 several, separated as a config separates them
chrome> shaders { … } the block itself, over as many lines as it takes
chrome> include "contrib/chrome/shine.kdl" a file, the way a config includes one
chrome> :load shine the same, for the presets that ship with it
```
`:paste` prints the document back as a `shaders { }` block with `where=` on every
entry — and that block can be typed straight back in, which is the point of
borrowing the syntax rather than inventing one. A test reads it off the screen,
clears the pane, types it back and compares the cells.
The text is a **document**: what it says replaces both chains, the way naming
`shaders { }` in a config replaces the block rather than adding to it. The reply
counts (`1 chrome, 1 content`) say where the passes went, which is the only way to
see that an entry's `where=` did what you meant.
Nothing in the prompt parses KDL. A file goes over by path and a pasted block via a
temporary file, because the session already has the parser — and a second reader of
a config file would be a second opinion about what it says, exactly where comments
meet quoted strings.
It is a readline prompt, so editing, up/down and ctrl-r work as they do in a shell,
and history is kept between runs in `$XDG_DATA_HOME/slosh/shader-repl.history` --
the chains only, since `:quit` is not something you want to press up past. Tab
completes the commands, the shader names, the property keys, the expression
language's own variables, constants and functions, and filenames after `:load`.
`:help` prints the same list at once. A test checks that list against
`src/shader.c` and `src/expr.c`, because a completion list that has gone stale
reads as "that is all there is".
**Off by default.** It needs `in_band_shaders true`, because a program that can
restyle the session it happens to be running in is a hazard first and a
convenience second: `cat` the wrong file and your panes go dark. With it on, a
pane can only paint *itself* -- not its neighbour, and not anything the config
said about anybody else.
## Your own, compiled
A shader is a C function from one cell to that cell's colours. Any `*.so` in
`~/.config/slosh/shaders/` that exports one is loaded at startup and named in
the config exactly like a built-in. Skeleton, Makefile and the rules a shader
has to keep:
`contrib/shader-plugin`.
It is native code in the session's process, so install ones you trust — the same
standing as `shell` and a layout's `command=`, which can already run anything as
you.
|