| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
| |
The static musl release builds cannot dlopen -- musl compiles it out
-- so loadable shader plugins were a feature most shipped binaries
could not use. Nothing we ship used it either: every shader in
contrib/shaders is an expression. Gone: the dlopen/glob loader in
shader.c, shader_dir in the config, the plugin ABI table and entry
macro, the Windows dl*/glob shims that existed only for it, the
contrib/shader-plugin skeleton, its tests, and every doc mention.
shader_abi.h stays as the shader model (cell_t, shade_ctx_t,
shader_t) with a note about what it used to be.
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Some 230 sentences hung their qualifications between dashes, and at that
density the tic reads as the writing's signature rather than a tool it
occasionally picks up. Each one is now the punctuation the sentence
actually wanted: a colon where the aside defines, parentheses where it
whispers, a comma where it barely pauses, a full stop where two thoughts
were pretending to be one. Definition lists and reference tables take
colons throughout.
Two double hyphens remain on purpose, both literal: the argv separator
in a shell example, and a table cell that means 'takes nothing'.
|
| |
|
|
|
|
|
|
|
|
|
| |
Every C comment, commit message and generated config in this repo
writes the aside dash as a spaced double hyphen; the Markdown alone
used U+2014, some 220 of them. One spelling, and the ASCII one -- the
prose now matches the code it documents.
Mechanical (every occurrence was the spaced form, checked including
the one inside a code fence, where it was comment text), so the words
themselves are untouched.
|
| |
|
|
|
|
|
|
| |
splash_ms and the project settings join the knobs table, the pane-state
ranking gains the floating it was missing, contrib/themes' seventh file is
named for what it is, the shader table's numbers are called examples with
the real omitted-value defaults beside them, the index links workspaces,
and the verb table learns splash.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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().
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
sl0p + pty was error-prone to type and the zero made it worse; slosh
(slop + sh) is one syllable, impossible to typo, and slosh.foo is ours.
Mechanical rename everywhere it appears: binary, env vars (SLOSH,
SLOSH_SESSION, SLOSH_BIN, SLOSH_CONFIG), socket dir (/tmp/slosh-<uid>,
$XDG_RUNTIME_DIR/slosh), config path (~/.config/slosh), header guards,
src/sl0ppty.h -> src/slosh.h, contrib/sl0ppty-dev -> contrib/slosh-dev,
the driving-slosh skill, docs and tests. The sl0ppi fork references and
the sl0p theme stay: one is history, the other is the brand.
The rename surfaced a latent bug in test_osc5577: the hello-reply
answer always appeared on screen twice (ECHOCTL echo plus cat -v), but
a seven-character implementation name happened to wrap the second copy
across the 60-column boundary, so counting substrings in wrapped text
found one and passed for the wrong reason. A five-character name moved
the wrap and exposed it. The loop guard itself is correct -- with echo
off before the request there is exactly one reply -- so the test now
counts on de-wrapped text and tolerates the pane's own echo.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
chrome> include "contrib/chrome/heartbeat.kdl"
heartbeat.kdl: 1 chrome, 0 content
chrome> shaders {
...> tint where="chrome" color="#ff5fd7" amount="abs(sin(t / 5))"
...> dim where="content" amount=80
...> }
1 chrome, 1 content
Both of those were errors an hour ago, and the second one was my fault twice
over: `:paste` printed `include "..."` and printing something you cannot type
back is a prompt lying about its own output. Pasting it answered `unexpected
argument for include`, and `load "..."` answered the same about `load`.
So the grammar is now the config's, all of it: an entry, several with `;`, an
entry with its own `where=`, a whole `shaders { }` block over as many lines as it
takes, `include "f.kdl"`, `load f`. There is a test that reads `:paste`'s block
off the screen, clears the pane, types it back line by line and compares the
cells -- round-tripping is the property, not a nice-to-have.
**The text is a document.** What it says replaces both chains, exactly as naming
`shaders { }` in a config replaces the block rather than adding to it, and an
entry's `where=` decides which rect it lands on. That drops a rule I invented last
week -- "one chain, one rect", which refused an entry that named the other one --
because it made the prompt's mode a law rather than a default and made a two-rect
block, the thing `:paste` prints, impossible to paste back. The prompt now keeps
the document and sends all of it, so a line aimed at the frame leaves the contents
alone, and the reply says `1 chrome, 1 content` rather than `ok`: with routing
visible, the counts are the answer worth reading.
`route_entries` is the one place routing is decided now -- config file, preset
file, typed line, same function. `config_parse_chain_doc` takes the wrapper or the
bare entries, because both are things a config contains and therefore both are
things somebody will paste.
Still nothing in the prompt parses KDL. A file goes by path; a pasted block goes
via a temporary file, since an OSC payload cannot carry a newline -- a control byte
ends the sequence, by the spec -- and joining lines with `;` would need exactly the
comment-and-quote scanner I refused to write. The only syntax the script looks at
is counting braces to know whether you have finished typing, and an empty line
gives up on that.
One hint, for the mistake that started this: when the session says `unknown
shader`, the prompt adds `(a file? :load <path>, or include "<path>")`. After the
fact rather than before, so a plugin's shader name is never second-guessed.
Verified: `tests/test_shader_repl.py` is 41 checks -- every input shape, the
multi-line paste and giving up on one, the round trip, the hint -- and
`tests/test_inband_shaders.py` gained the document rule in both directions,
including that a document naming only the frame takes the contents pass off. Full
clean suite green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
chrome> :load sine-comet
sine-comet.kdl: 1 chrome, 0 content
chrome> :paste
// sine-comet.kdl
include "/home/user/dev/sl0ppty/contrib/chrome/sine-comet.kdl"
A path, or the bare name of anything in `chrome/` or `shaders/` beside the
script, from any directory. Entries land wherever their own `where=` says, so one
file can fill both chains and a preset goes where its author put it. Tab completes
filenames after `:load` and vocabulary everywhere else.
**The prompt does not parse the file.** I started writing the scanner -- find the
`shaders {` block, count braces, strip `//` outside quoted strings so that
`color="#ff0033"` and `t / 8` survive -- and stopped, because that is a second
reader of a format that already has one, and the two would disagree exactly where
comments meet strings. Instead the session gets the path and reads it with the
parser it already has: `config_parse_chain_file`, built on the same
`parse_shader_entry` a config file goes through. Anything that is not a top-level
`shaders { }` block is ignored, so a whole `config.kdl` can be handed over and
only its shaders apply.
The alternative was regenerating all 45 presets into something easier to read from
Python. That would have cost them what makes them useful: each one is a config
fragment today -- pasteable, `include`-able, checkable with `sl0ppty --check`, and
loaded as a config by `tests/test_chrome_presets.py`. A format only the prompt can
read would be a fourth thing to keep in step.
`:paste` after a load answers with an `include` line rather than the file's thirty:
the file is on disk, it is already the block, and reprinting it scrolled the pane
you were looking at -- which is how I noticed.
Both chains are built beside the live ones and swapped in together, so a file that
goes wrong half way leaves the pane as it was rather than wearing the half that
parsed. Each compiled expression is matched to the chain whose shader points at it
before either is installed; freeing a program the other chain still uses is the one
bug here that would not appear until the next repaint.
Verified: nine checks in `tests/test_shader_repl.py` -- a preset by bare name, a
file with both rects going to both chains, the three ways to be wrong (no such
file, no `shaders { }` block, no argument), what `:paste` says afterwards, and tab
completion. Then all 45 shipped presets loaded by name in one session: none
refused.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Angles here are degrees, and pi in an angle language is not a number -- it is a
half turn. So `PI` is 180, `TAU` is 360, and a formula written the radian way
ports across as written:
sin(TAU * x / cols) one cycle over the pane's width
sin(TAU * p * 3 / perim) three lobes fitted exactly to a border
PI / 2 a quarter turn
PI / 6 thirty degrees, exactly
That last line is the argument. A numeric pi would be 3 in this language, and
`3.14159 / 6` in integers is 0 -- so radians cannot express a sixth of a turn
here at all, while degrees give it as a whole number. The pun is not a
compromise; it is the more precise reading of the two.
Which is why there is no `deg2rad` or `rad2deg` beside them, and asking for one
says `unknown function`. There is one angle unit. A conversion could only lose
the angle (`deg2rad(90)` is 1, and `sin(1)` is nearly nothing) or invent a second
scale for the language to disagree with itself about.
Spelled-out numbers rather than variables: they read no environment, carry no
dependency, and fold into the program like the literals they are. The browser
preview learned the same two, from the same table.
Verified: 12 cases in `tests/expr_test.c`, including that `deg2rad` and
`rad2deg` are refused; the cross-check agrees with the compiler on 700
evaluations, now including `sin(TAU * x / cols)` and friends.
Two smaller things ride along, both from the repl. `PI`/`TAU` tab-complete, and
its drift test grew a third table to check -- it already caught `sin`/`cos`
missing before I did. And it now checks the script *parses* before starting a
pane that runs it: I broke the indentation of one line here, and the failure
arrived as `IndexError: list index out of range` from a pane that died before
its prompt, which is a long way from "line 212".
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
128 + sin(t / 4) / 2 a breathe that is actually a sine
abs(sin(x * 6 + t / 5)) a wave travelling along a frame
abs(sin(dist(x, y, curx, cury) * 20 - t / 4)) / 2 ripples from the cursor
Degrees in, -255..255 out. Two decisions, both for the same reason -- this is a
language people type into a config file, not one they read a manual for.
Degrees because that is what anybody guesses, and 255 because it is the scale
every strength here already uses: `128 + sin(t / 4) / 2` is a breathe centred
half way up with nothing to rescale, and `abs(sin(...))` is an amount as it
stands.
Every angle wraps, negative ones included. `t / 4` does not stop at 360 and an
expression that died at one turn would be no use for the one thing this is for.
A 91-entry table of round(255*sin) at one-degree steps, three quadrants by
symmetry, no interpolation -- the argument is an integer and there is nothing
between two degrees to interpolate. A table and not a polynomial because
`contrib/shadertoy.html` reimplements this language in JavaScript so the preview
can run in a browser, and `tests/test_shadertoy.py` compares the two on every
expression: identical data with identical reduction cannot drift the way two
approximations of one curve would. The JS carries the same 91 numbers.
Until now the only oscillator was `abs(t / 6 % 510 - 255)`, a triangle wave
written as arithmetic. It works and it looks like what it is -- a ramp with a
corner in it. A sine has no corner, which is the whole difference between a pane
that pulses and a pane that flickers.
Verified: 19 cases in `tests/expr_test.c` (the cardinal angles, quadrant
symmetry, wrapping past a turn and below zero, and the breathe idiom); the
cross-check now agrees with the compiler on 660 evaluations including generated
trig; the repl's completion drift test caught `sin`/`cos` missing from its list
before I did, which is what it is for. Then six chains in a real pane -- breathe,
travelling wave, clipped crests, plasma, rolling scanlines, cursor ripples --
each measured over eight distinct frames.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`&` `|` `^` `~` `<<` `>>`, on the 32-bit value the VM already used. There was
nothing: `x & 7` answered `unexpected trailing input at offset 2` and
`(x ^ y) % 16 < 2` answered `expected ) at offset 3`, which is a fair
description of a parser that had never heard of either.
`^` is the reason to bother. `(x ^ y) & 1` is the cheapest interesting texture
there is, and the language could express a sweep, a ramp and a comparison but
not one bit of pattern without `%` gymnastics.
**Bits bind tighter than comparison, which is not what C does.** `x & 7 == 0`
means `x & (7 == 0)` in C -- a precedence Ritchie put on record as a mistake,
kept for a decade of compatibility that a shader expression in a config file
does not have. Here it asks what it looks like it asks. Among themselves the
usual `&` then `^` then `|`; shifts stay where C has them, just under `+ -`.
`&&` and `||` still parse as themselves: the bitwise rungs decline the first
character when a second one follows it.
Semantics are JavaScript's, deliberately and to the letter -- the shift count
masked to five bits (`1 << 33` is 2), `>>` keeping the sign (`-8 >> 1` is -4).
Not taste: `contrib/shadertoy.html` reimplements this language in JS so the
preview can run in a browser, and `tests/test_shadertoy.py` compares the two on
every expression it can think of. Anything C leaves implementation-defined had
to be pinned to what JS would show, so the signed right shift is written out in
the VM rather than left to the compiler.
Verified: 22 new cases in `tests/expr_test.c` covering the operators, both
precedence questions, the sign, and the masked count; the preview learned the
same six operators and the same ladder, and the cross-check now agrees with the
compiler on 564 evaluations, including generated ones that reach for bits. Then
seven bitwise chains in a real pane -- an XOR texture, a drifting one, a stepped
pulse that walks eight shades of `t >> 7 & 7` and needs no `abs`.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
printf '\033]5577;1;shader;\033\\' # from the program
sl0ppty cmd '{"cmd":"clear-shaders","id":0}' # from outside it
bind "z" "clear-shaders" # from the keyboard
`app_clear_pane_shaders` empties both of a pane's chains and says whether
there was anything there, which is what lets a key answer "shaders cleared"
or "no shaders on this pane" instead of maybe having done something.
In-band it is not a new verb: naming no rect means all of them. `shader;`
sits under `shader;chrome;` and `shader;content;` as the degenerate case of
"which rect", so a program puts a pane back in one exchange and cannot leave
it half done. `contrib/shader-repl`'s `:both` now uses that rather than two.
**None of the three is gated on `in_band_shaders`.** A chain outlives the
setting that allowed it -- paint a pane, then turn the setting off, and the
paint is still there, because a chain belongs to the pane and not to the file
(there is a test that reloads with it off and clears anyway). A way out that
the setting can take away is not a way out. The operator path matters for the
same reason: the program that made a pane unreadable is not always in a state
to put it back.
They clear what the *pane* set. The config's chains and the session's own
dimming are not this pane's doing and are derived every frame anyway.
The action is bound to nothing, like `scroll-up`: it is in the palette, which
is what the palette is for, and the docs say to bind it if you leave the
setting on. Which is where a second bug turned up -- the palette truncates a
label at 26 characters, so "undo this pane's own shaders" read as "undo this
pane's own shade" on screen. Renamed to fit, and `tests/test_palette.py` now
checks every label against the column, since nothing failed for that: it just
looked like a typo in front of someone.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`contrib/shader-repl` imports readline, which is most of it: editing,
up/down, ctrl-r, and history kept between runs in
`$XDG_DATA_HOME/sl0ppty/shader-repl.history`. Chains only -- a `:` command
is taken back out of the history it would otherwise fill, because pressing
up four times past `:quit` and `:paste` to reach the line you were working
on is the annoyance history exists to remove.
Tab completes the commands, the nine shader names, the property keys, the
two rects, the three channels, and the expression language's own variables
and functions. `:help` prints the lot at once, which is the same list from
the same place.
That list is a claim about what exists, so `tests/test_shader_repl.py`
checks it against `src/shader.c` and `src/expr.c` and fails if either grows
a name it does not have -- verified by removing `spotlight` and watching it
fail. Then it types every name it offers at a real prompt and requires `ok`
from a real session, because a name that completes and does not parse is a
lie with a keystroke attached. History across two runs, a tab completion,
and `:help` are driven the same way, through a pty.
Two things that cost me time and are now written down where they bite:
keys go into a pane escaped (`\r`, `\t`, `\e[A`) because `raw` takes the rest
of one command line and a literal newline ends the command instead of
arriving; and Enter is CR, which is what a terminal sends and what readline
waits for.
Also: **a bare argument on a shader entry is now a complaint.** Every
parameter is a property, so `tint color="#000000" spotlight` is not a pass
with a spare word, it is a missing `;` -- KDL reads one node, the second
name does nothing, and the session used to answer `ok`. It now says
`unexpected argument for tint: spotlight (separate entries with ';')`. Found
by watching the prompt get used.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
printf '\033]5577;1;shader;chrome;tint color="#ff5fd7" amount="abs(t / 8 % 510 - 255)"\033\\'
The field after `shader` is which rect -- `content` or `chrome` -- and the
rest of the payload is a chain in exactly the syntax a `shaders { }` block
uses, taken verbatim so that `;` can go on separating entries. Empty text
clears that chain. The session answers on the program's stdin, `shader-reply;ok`
or `shader-reply;error;bad amount for tint: ...`, because a prototyping loop
that gets no answer cannot tell a typo from a build without the feature.
`contrib/shader-repl` is the prompt this exists for: type a chain, watch the
pane, and `:paste` prints what you have as a config block. Both ends are the
same parser, so what you arrive at is what you can keep -- verified by
pasting `:paste`'s output into a file, linting it, and checking the frame
comes out the same colour.
**D13 said this was impossible on purpose, and the reasoning stands**: a
program that can restyle the session it happens to be running in is a hazard,
and `cat hostile.txt` should not dim your panes. But *impossible* was the
wrong shape for it -- editing a file, saving, looking and guessing again is
slow enough that shaders go unwritten. So it takes `in_band_shaders true`,
one line of consent, and it is scoped to the pane that asked: its own two
chains, never its neighbour's, never what the config said about anyone else.
There is a test for the neighbour.
Each chain owns the programs its expressions compiled to, and is swapped in
whole, so a chain that turns out not to read leaves the pane looking like it
did rather than half-restyled. Measured: 1,800 replacements of an animated
chain, resident size flat after the first pass.
Also here, because it is the same paint order: the chrome chain now sits
between the config's and policy's, exactly where the content chain does.
Dead code out -- `app_shade_add`/`app_shade_clear`/`app_shade_count` had no
callers and their comment claimed the opposite of what this commit does.
|
|
|
The README was 331 lines and had become a manual with a title. Everything
in it was true and most of it was the wrong thing to hand somebody who
has just arrived: five pages of shader expression semantics before they
have split a pane.
So the front page is 60 lines -- what it is, install, run, where the docs
are, and six bullets on what is actually different about it -- and the
detail moved into docs/, nine pages that each answer one question:
index.md what it is, install, run, where to go
keys.md the leader, every default, rebinding, direct binds
panes.md splitting, resizing, the mouse, dead panes, small terminals
config.md the file, live reload, include, themes, the knobs
shaders.md the passes, the built-ins, the expression language
chrome.md where="chrome", channel, since, the perimeter trick
layouts.md a session as a file, and writing one back out
scripting.md the control socket, OSC 5577, headless
design.md the two decisions, and a map of the source
docs/nav holds the order and the grouping and nothing else -- titles come
from each page's own heading, so a renamed page cannot end up with two
names. It is what the site generator will read.
Reference material that only existed in code comments is written down for
the first time: every action that can be bound (including the two with no
default key, which is what the palette is for), every control verb, the
built-in shaders with what each takes, the pane-state ranking, and the
layout file's shape.
Two things I got wrong on the first pass and fixed before committing: the
docs linked repo files through github.com URLs for a project whose remote
is a private git server, so those are inline paths now; and the README
advertised `make docs`, which does not exist yet. It will in the next
commit, and it can be advertised then.
|