aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/README.md (follow)
Commit message (Collapse)AuthorAge
* shaders: remove the .so plugin loaderuser2026-09-02
| | | | | | | | | | | | 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.
* slosh: the website lives in web/ -- one directory, one build, make wwwuser2026-08-19
| | | | | | | | | | | | | | | | | | | | | | Consolidate everything the website is made of: - web_new/ -> web/: the front page, install.sh, and build-site - contrib/webdemo/ -> web/demo/: the in-browser machine, committed for the first time (it had never been checked in) - make www at the top level assembles the whole site into build/www (NO_DEMO=1 skips the guest cross-compile) - web_plan.md deleted: the plan is fulfilled and web/README.md is now the record Path fallout: the overlay theme symlink gains one ../ (web/demo is a level shallower relative to contrib/themes), PYSRC in the Makefile follows the demo scripts, contrib/README.md drops its webdemo section. ROOT := ../.. in the demo Makefile survives unchanged -- web/demo sits at the same depth contrib/webdemo did. Verified: make www builds front page + docs + demo end to end, web/demo make check boots the image to slosh, tests/test_docs.py and make fmt-check green.
* 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().
* slosh: rename the project from sl0pptyuser2026-08-18
| | | | | | | | | | | | | | | | | | | | | 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.
* sl0ppty: a skill for driving it, and the two variables that made one possibleuser2026-08-17
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `.agents/skills/driving-sl0ppty/SKILL.md`. The convention is `.agents/skills/<name>/SKILL.md` with YAML frontmatter -- already in this tree under `vendor/libghostty-vt`, so it is not an invention -- and it means an agent working in a checkout picks the skill up without being told, while a directory per skill leaves room for adjuncts later. `contrib/README.md` and `docs/scripting.md` say where it is and how to use it elsewhere: copy or symlink the directory into wherever your agent keeps skills. **Writing it turned up the reason there was no skill worth writing.** A pane is told `SL0PPTY=1` and nothing else, so a program inside one knows it is in a session and cannot find out *which* -- `ls` lists them all and none of them is labelled "the one you are in". So the whole most valuable case, an agent driving the session it is running in, was guesswork. The server now sets `SL0PPTY_SESSION` and `SL0PPTY_BIN` in its own environment, which every pane inherits through fork: one place, no signatures threaded, and unset under `--script`, which has no socket to name. `SL0PPTY_BIN` is read from `/proc/self/exe` rather than `argv[0]`, and exists because the failure it prevents is the one I hit writing the examples -- a session started from a build tree, and `sl0ppty` not on the pane's `PATH`, so the skill's own commands did not run. What the skill actually teaches, none of which `docs/scripting.md` says because a reference is not allowed to have opinions: - Work belongs in a pane that was **given** a command, not typed into somebody's shell. Then `alive` and `exit_code` off `panes` are how you wait, and the output survives the program (D14) so it is still there to read. - There is **no wait primitive over the socket**: `settle` is headless-only, and `deadline` is about repaints and says nothing about program output. Poll. - `purpose` is the handle. Titles change underneath you; a declared purpose cannot be relabelled by the program in the pane, which is what makes it safe to address. - `snapshot` is the whole composited session, and only what is on screen. Cut a pane out with its `content_*` rect; do not expect scrollback. - **The echoed command line matches your own marker.** `echo DONE` puts `DONE` on screen twice. Count, or use a marker you did not send. - Write `\e`, never `\033`: the unescaper takes `\0` first, so `\033` arrives as NUL followed by `33` and nothing complains. That bug was live in this repo's own test suite until last week. - A program in a pane can draw its own status and real buttons with OSC 5577 and read the click off its stdin, which is how it asks a question instead of printing a prompt and hoping. - Close what you made; do not `quit` a session you did not start. `tests/test_skill.py` is the part that keeps it true. A skill is documentation an agent acts on without a human reading it first, so a stale one is worse than a missing one: a wrong verb is a failed tool call somebody else has to debug. Every verb the file names is sent to a live session and must not answer `unknown cmd`; every `panes` field its central recipe polls must exist; the environment it promises is read out of a real pane; the recipe itself is run end to end and must report exit status 3 with its output still on screen. It also greps the file for paths out of a home directory, because "plug and play" is a property somebody has to keep. That test found `close-pane` in the draft, which is the *keybinding* action name -- the verb is `close`. Exactly the class of error the test exists for, found before anybody's agent ran it. Verified: 1795 checks, all green, 43 of them in `tests/test_skill.py`. Then the skill's recipe run verbatim against a real session -- tagged pane, polled to `alive:false`, `exit status: 3`, `compiling`/`built`/`exited: status 3` still readable -- and from inside a pane, `session=agentdemo bin=/…/sl0ppty` and a `building 3/7` status drawn in its own frame.
* make coverage: a number, cheaplyuser2026-08-15
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `make coverage` builds a second binary with gcc's --coverage into build/cov, runs every suite against it, and prints a per-file table from gcov. About a minute. Nothing installed, nothing about the normal zig build changed. gcc rather than clang because the counters need a gcov that matches the compiler, and gcov is what this machine has -- no llvm-profdata, no llvm-cov, no lcov, no gcovr. The aggregation is forty lines of python reading gcov's own summaries, which is the whole reason this was cheap. The suites drive it through $SL0PPTY_BIN, one line in the harness, so there is no second copy of the test code and no risk of the coverage run testing something else. Two things about the result, both in the report itself: - the C unit tests link the *same* instrumented objects, so input.c, kdl.c, shader.c and expr.c are credited for the tests that exercise them. Built separately, input.c read 54% while having a dedicated decoder test -- a lie about where the tests are rather than about the code. - server.c reads ~8% and is not untested. The session server is a daemonised child ending in _exit(), which is right there and skips the flush gcov needs. The counters are lost, not the tests. Said in the output, because an unexplained low number invites someone to "fix" working code -- and changing correct code to make a tool happier is the wrong trade. Where it lands today: 85.4% of 6298 lines. The gaps that are real rather than mechanical are jsonval.c (58%), screen.c (64%) and pty.c (59%) -- error paths and the parts of the diff emitter that only a resize or a weird terminal reaches.
* sl0ppty: write the session back out as a layout, and a loop that uses ituser2026-08-15
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Dogfooding this while building it has one sharp edge: a running session keeps the binary it started with. `reload` re-reads the config, it cannot re-read the code, so every build you want to try means losing your screen. The only thing worth carrying across a restart is the shape, and sl0ppty already knows how to read one -- it just could not write one. `dump-layout` is the inverse of `--layout`: tabs with their names and purposes, how the panes are split, in what proportion, in which directory, running what, which tab was active and which pane had focus. Two details make it worth trusting. The directory is where the shell *is*, read from /proc, not where the pane was started -- after any amount of `cd` those differ and only one of them is useful. And a pane running the session's default shell is dumped with no `command`, so it comes back as a shell rather than as a shell wrapped in a shell, one layer deeper every restart. contrib/sl0ppty-dev is the loop: attach, and when the session ends because you asked for a restart, come straight back with the layout. It is two halves because it has to be -- `restart` runs inside the session, and the moment the session ends so does the shell it was typed in, so it leaves the layout and a note and the loop outside picks them up. What does not come back is what was running *inside* a program. A shell returns as a shell, in the right directory, with an empty history. That is the limit of restoring a layout rather than a session, and the script says so rather than implying otherwise. Three bugs found by the round-trip test, all the same kind -- a dump that loads without complaint and rebuilds something subtly different: - `split=` belongs on the node that *has* the children, not on the children. Written the other way it parses fine and gives the wrong tree. - a split's weight was never restored, because the edit meant to read it silently matched nothing and was never applied. - `active=true` landed on the wrong tab: the index is recorded while the new tabs sit after the old ones, and `replace` then moves them to the front. Which is why the test asserts that a dump of a reload is byte-identical to the first dump. Anything the dumper loses shows up there at once, where reading it and nodding would not.
* contrib: the shadertoy presets, usable in sl0pptyuser2026-08-15
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | They only existed in a browser, which is a strange place for a terminal multiplexer's shaders to live. Now each one is a file you can paste. contrib/shaders/ holds all thirty-two, one per file, *generated* from the presets in shadertoy.html by contrib/gen-shaders. The page stays the source: it is where they are written, previewed and conformance-tested against the compiler, and a hand-kept second copy would drift the day after it was written. `gen-shaders --check` says whether the files are current, which is a test, so the drift is a failing build rather than a stale example. contrib/shader-tour cycles a session through them, in the shape theme-tour already established — write the config the session watches and let the watcher do the rest. It takes one shader, one group, or nothing for all of them, and it resolves a short name (`torch`) as well as the whole slug (`guides-torch`), because nobody remembers which group a shader is in. The tour writes the theme alongside the shader: shaders resolve default-coloured text through theme's default_fg/default_bg, so a preset judged without one is being judged against a guess. The new test is the one that matters. Every shipped file is loaded into a real session and has to *change the cells of a pane* -- an example that previews beautifully and does nothing when pasted is worse than no example, and neither the toy's conformance test nor a config parse would notice. Three of them are then checked for the specific thing they claim: scanlines alternate row by row, indent guides land on every fourth column, a flat dim is flat. One session, reloaded per preset, which is both quick and a fair exercise of the path anyone will actually use. Renamed the "the built-ins, as expressions" group to drop the qualifier from its filenames; it was generating built-ins-as-expressions-scanlines.kdl, which is a sentence rather than a name.
* contrib: presets worth stealing, and a test that they do anythinguser2026-08-15
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Eleven examples, grouped, and most of them are less "effect" than *instrument*: a cursor line, a crosshair, indent guides, a right margin at three quarters, a torch that dims everything more than eight cells from where you are looking. Things that tell you where you are, inside a program that has never heard of any of them -- which is the argument for shading contents rather than chrome, made in examples instead of prose. Then the fun: phosphor and amber CRTs (grayscale, tint, scanlines, vignette as a four-pass chain), film grain, a rolling bar, an interlace flicker; sonar pings expanding from the cursor, ripples, a barber pole, plasma from two triangle waves, matrix rain with a fading tail; and every built-in written as an expression, because `vignette` being `dim` with an argument is the whole design and is better shown than argued. Two rules the presets follow, both about staying identical to the C: never multiply `t` (it is a millisecond clock and gets large -- divide it down first and the arithmetic stays in an int32 forever), and keep hash coefficients small for the same reason. `(x * 11) % 23` is fine; the usual Knuth constant is a different number in C than in JavaScript. The test now lifts the *presets* out of the page as well as the evaluator, so one added later is conformance-checked without anyone remembering to add it. And it evaluates every preset over a whole pane at four points in time and fails any that is zero everywhere -- an example that does nothing reads as a broken shader system rather than a bad example. That check caught its own first draft: sampling every other row made three `y % 2` effects look dead when they were fine. It samples every cell now, which costs six hundredths of a second for four hundred thousand evaluations, because the thing being tested is quick.
* contrib: a shadertoy for shader expressionsuser2026-08-14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | You cannot see an expression by reading it. `(y % 2) * 40` is scanlines and `(x > cols - 10) * 120` is a right margin, and neither is obvious until it is on a screen -- so here is a screen. One HTML file, no build and no server: open it and it works, offline, including from a checkout on a machine that has never run sl0ppty. It draws a fake pane with plausible contents, applies a chain of passes to every cell, and prints the config line to paste. The mouse moves the cursor, so `dist(x, y, curx, cury)` behaves the way it will in a pane rather than the way it does at coordinates you imagined. Anything reading `t` animates. The presets are every built-in written as an expression, which is the equivalence the whole design rests on: `vignette` and `spotlight` are not special, they are `dim` with an argument. It also reports what each expression *reads* and whether that makes it cacheable, because the dependency analysis is the interesting half of the implementation and is otherwise invisible. The hazard is that this is a second implementation of the language, in another language, and it would disagree with the compiler quietly -- in the third digit of a colour, on one preset, at one cursor position. So tests/test_shadertoy.py lifts the evaluator straight out of the page (not a copy of it, the page itself) and runs a few hundred expression/environment pairs through both it and src/expr.c, including the corners: division by zero, the ternary's untaken branch, `dist` counting a row double, and six expressions that both sides must refuse. build/expr_eval is the real compiler as a filter, which is what makes that comparison possible at all. It found a bug on its first run, in the page: unary `-` and `!` did not thread the environment through their closures, so `!x` threw instead of evaluating. Which is the argument for the test, made better than I could have made it. Skipped with a message when node is missing -- a checker for a contrib toy is not a reason the suite cannot run.
* sl0ppty: shaders can be loaded from shared librariesuser2026-08-14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Adding an effect meant editing shader.c and rebuilding, which is fine for us and useless to anyone else. Now any `*.so` in the shader directory that exports a plugin table is loaded at startup, and what it declares is named in the config exactly like a built-in -- same syntax, same three parameters, same pass. Default location is a `shaders` directory beside the config file, so dropping the .so next to config.kdl is the whole installation. The interesting part is what a plugin can see, which is one header. shader_abi.h holds cell_t, color_t, shade_ctx_t, shader_t and nothing else: no libghostty-vt, no pty, no layout, no session. A shader is therefore unable to reach anything but the cell it was handed, not by rule but because nothing else is declared where it can find it. cell_t and color_t moved there from sl0ppty.h for the same reason -- that is the one place they are a contract with code compiled outside this tree rather than an internal type. Loading is deliberately narrow: - refused unless the library reports our ABI version *and* matching struct sizes. The version says which contract; the sizes catch someone editing their copy of the header, which a version number alone calls fine and a running session calls garbled colours; - refused if it redefines an existing name, because a config naming `dim` means the `dim` it was written against; - all-or-nothing per file, and per file: a bad library is a line on stderr and everything else still loads (D9 fail-open); - nothing is ever unloaded. A shade_fn may be sitting in a live config or on a pane, so dlclose() is a jump into an unmapped page. A reload picks up newly *added* plugins; replacing one needs a new session. This is the single exception to D9's "a plugin is a subprocess", and it is argued in D15 rather than drifted into: a shader is called per cell, per pass, per frame, which no subprocess can be in the path of, and the measured alternative -- interpreting bytecode per cell -- is 56ns against 8ns for the compiled function. On trust, `shell` and a layout's `command=` already let a file in the config directory run anything as you, so a .so beside them is not a new boundary. The docs say so plainly rather than implying a sandbox. contrib/shader-plugin is a working skeleton: Makefile, two commented effects (one positional, one animated), and the three rules a shader has to keep. It is built and loaded by the test suite, so a broken example is a failing test rather than someone else's afternoon.
* sl0ppty: contrib themes, a way to look at them, and a handoveruser2026-08-14
Six themes, each setting every colour the config knows about. A theme that has fallen behind silently keeps the compiled-in default for whatever it forgot, which looks like a bug in the theme rather than a missing line in it, so a test compares each file against the names config.c actually parses. contrib/theme-tour applies one to a running session, or cycles the lot. It does that by writing over the file the session was started with and letting the config watcher notice, which makes the showcase a demonstration of the watcher as well. HANDOVER.md is for the next context. It deliberately does not restate DESIGN.md or the README; it is the part that was only learnable by getting it wrong — which state is derived and which is intent and what happens when you confuse them, the dwell repaint list, that glyph width and glyph weight are different problems, that settle() in the harness is a sleep in disguise, and that a test pinned to a coordinate it does not care about will eventually look like a regression it is not.