aboutsummaryrefslogtreecommitdiffstats
path: root/src/headless.c (unfollow)
Commit message (Collapse)Author
2026-08-27selection: drag past an edge scrolls the viewport under ituser
Text taller than the viewport was not selectable in one gesture: the drag stopped dead at the pane's border. Two faults, one visible and one waiting. The visible one: DRAG_SELECT motion subtracted the content origin in uint16_t, so a pointer above or left of the pane underflowed, the grid lookup failed, and the selection froze. The waiting one: the anchor was a pair of *viewport* coordinates re-resolved on every extend, so the moment anything scrolled mid-drag the anchor would have drifted with the viewport, re-anchoring to whatever slid under the press. The anchor is now a tracked grid ref -- the thing lib-vt documents as being for 'long-lived anchors such as selections' -- so it follows its cell across scrolling, pruning and reflow, and only the near end of the selection moves. If a program flooding output prunes the anchored row out of scrollback entirely, the selection keeps what it had; there is no cell left to anchor a new answer to. Motion clamps into the content rect (a pointer past an edge is pointing at the nearest cell), and holding past the top or bottom edge scrolls on a clock rather than on motion, because a resting pointer sends no events and 'keep going' is exactly what resting it there means. app_tick() does the step, app_next_deadline_ms() wakes the poll loop for it -- the same mechanism toasts and hover guides already use -- and both front ends call the tick, so the behaviour exists under the server and the headless harness alike. Rows per step is how far past the edge the pointer sits: one row past creeps, further hurries, so the hand already dragging is also the throttle. The metronome is select_scroll_ms, a knob so the test can configure the clock down instead of sleeping. The test drags to one row above the pane and simply holds: the viewport walks to the top of scrollback (clamping makes the endpoint exact), the copy starts at a line that was never on screen with the press, and still ends at the pressed row -- that last check is the tracked anchor's.
2026-08-18slosh: rename the project from sl0pptyuser
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.
2026-08-17sl0ppty: a --script pane is told no session rather than somebody else'suser
`server.c` set `SL0PPTY_SESSION` for its panes and carried a comment saying the value was "deliberately unset under `--script`". Nothing unset it. A value never set is a value inherited, so a script driver started from inside a pane handed its own panes the *outer* session's name -- and the recipe the skill documents, `-s ${SL0PPTY_SESSION:-main} cmd`, then sent its commands to a session it was not in. Not a fallback to a guess: a confident wrong answer, because the name it read was a real session that really answers. Both variables now go through one `session_env()`, called by the server with its name and by the headless driver with NULL, because the two modes have to agree about what a pane is told and a comment in one of them is not agreement. `SL0PPTY_BIN` is set there too rather than inherited, so it names the binary that actually made the pane instead of one that made some pane somewhere -- which is the whole reason that variable exists. Found while explaining a stale pane title, from a check in `test_skill.py` that failed under `make test` and passed on its own. It was not flaky: the suite runs from inside a pane, where these are set, and the tool I ran it from by hand had an environment where they were not. A test whose answer depends on where you run it looks exactly like a race. So it now passes the lie in deliberately: the session under test is started with `SL0PPTY_SESSION=somebody-elses-session`, which makes the difference between "never set" and "cleared" visible from any shell. The mirror check is next to it -- a real session tells its panes its *own* name, even when started from inside another one -- so the obvious wrong fix, unsetting it in both modes, fails. Both were verified by making both wrong versions and watching each test bite. The two title checks now wait for the line instead of sampling after 120ms. That was not the bug (a shell prints in about 12ms here, even under disk churn) and it is one assumption fewer.
2026-08-14sl0ppty: one-shot --headless printed a blank screenuser
`--headless` is documented as "run once, print the screen", and it printed nothing for any command that finished — which is most of them. `make smoke` had been showing six blank rows. settle() calls app_reap() every iteration, and app_reap closes a pane whose child has exited; closing the last one takes the tab with it. A command like `sh -c printf ...` exits immediately, so the pane and its content were gone before app_compose ever ran, and the compose found an app with no tabs. So settle() now takes an optional screen and composes into it before the reap rather than after. This is exact rather than a narrowed race: pane_pump() only marks a pane dead on read() returning 0, and it reaches EOF only after it has drained everything the pty had, so a pane cannot be marked dead while its output is still pending and the last snapshot always holds the finished screen. --script passes NULL and is byte-for-byte unchanged. smoke also asked for 6 rows, which after the outer margin, the tab strip and two borders leaves about one row for the pane — under min_pane rows, so it collapsed to an empty frame and would have shown nothing even with the fix. It asks for 12 now. Found while renaming; it predates the rename, reproduced on the commit before.
2026-08-14sl0ppty: rename from sl0pttyuser
A pty is a pseudo-terminal, and that is what this multiplexes: the extra t was never standing for anything. Renaming now, while the only users are the people who typed the old name on purpose, is cheaper than renaming later. Mechanical, but not only cosmetic. The name is also the runtime contract, so this moves $SL0PTTY and $SL0PTTY_CONFIG, ~/.config/sl0ptty/config.kdl, the socket directory ($XDG_RUNTIME_DIR/sl0ptty, /tmp/sl0ptty-<uid>), and the identity string a program gets back from an OSC 5577 hello. A session left running under the old binary keeps its old socket directory and is invisible to the new one; there is no migration and it does not seem worth writing one. The sl0ppi fork keeps its name: it is a different thing, and D1 still points at it.
2026-08-14sl0ptty: select-to-copy, middle-click paste, and toastssl0p
Drag over text, release, and it is on your clipboard — no chord, no menu, the selection is the copy. The clipboard lives on the client's machine, so the copy travels as OSC 52 and therefore works over ssh; the session keeps its own copy as the primary selection, which is what middle click pastes back. Selected cells are inverted rather than recoloured, so a selection reads the same over whatever theme the program inside is using. A program that is tracking the mouse keeps its own clicks (shift is the escape hatch), which is tested rather than assumed. Toasts: transient announcements in the bottom-right that expire on their own, stacked up to three. 'copied 13 chars', 'config reloaded', a pane's OSC 9 notification, or {"cmd":"notify","text":"..."} from an orchestrator — an agent in a pane can now say 'build finished' and have it appear. OSC 52 from a pane reaches the same clipboard. Two real bugs found on the way: - mouse events are hit-tested against what was painted, and nothing had been painted before the first frame, so a click that arrived first was dropped. Both front ends now compose once before accepting input, which is what really happens in a session anyway. - the plain leaf_new never installed the clipboard and notification handlers; only panes built from a layout file had them. Every ordinary pane was silently missing both. The test escaping traps continue to earn their comments: a real newline inside a driver argument terminates the *command* rather than reaching the shell, \a is not a driver escape (\x07 is), and an interactive shell's line editor eats a typed ESC — so a pane driven with escape sequences must not be one. 356 checks green (329 headless + 27 live).
2026-08-13sl0ptty: M7 — layout files and suspended panessl0p
A session can be a file: layout { tab name="api" purpose="project:api.a1b2c3" cwd="~/dev/api" { pane purpose="agent:main" command="pi" pane split="rows" { pane purpose="service:web" command="npm run dev" suspended=true pane purpose="shell:scratch" } } } built by --layout at startup or applied to a running session with {"cmd":"apply-layout", path|kdl, replace?}. Idempotency stays with the caller, which is where sl0ppi's CLI design already puts it: diff list-tabs against the projects on disk, then apply what is missing. close-tab completes that loop. Purposes a layout declares are locked, so identity comes from the file rather than from whatever the program inside prints — tested by having a pane try to take one in-band and be refused. Suspended panes: a pane that exists, is laid out, and has run nothing, showing what it would run and starting on its first keystroke (which is consumed, not forwarded). This is sl0ppi's D6 — a root with twelve projects must not become twelve running servers — and it is the reason can be cheap. Also fixes a test-isolation bug latent since M2: the live tests attached to the session called "main", which is whatever happens to exist on the box rather than one they created. They now use a unique session per run, which is how a run that had somebody else's htop in it became reproducible. 284 checks green (257 headless + 27 live).
2026-08-13sl0ptty: M2 — server/client split, detach/reattach, control socketsl0p
The session now outlives its client, which is the point: agents keep running while you are gone. The server owns the panes, the layout and the renderer; the client is a terminal in raw mode and a socket, forwarding bytes and painting what it is sent. It does not even decode input, so the decoder can change without redeploying clients. sl0ptty attach to 'main', creating it if nobody is home sl0ptty -s NAME a named session sl0ptty ls live sessions sl0ptty cmd LINE one control command C-a d detach Connections are not clients. A connection becomes the display client only by sending HELLO; control commands and liveness probes are just connections and never displace anybody. This was found the hard way, one hour after the server first ran: 'sl0ptty ls' kicked the attached client off, because ls connects to see whether a socket answers. That is precisely the fault that gave the zellij fork a 20-25% CLI failure rate, rebuilt and then fixed structurally. The regression runs 20 x (ls + control) against a live client and demands 0 failures and a client that can still type. cmd.c now holds the command vocabulary once, shared by the headless driver and the control socket, so a script written against one works against the other — 'sl0ptty cmd "snapshot text"' against a detached session prints its screen. tests/test_session.py: 18 checks over create, split, probe-resistance, detach, survival, drive-while-detached, reattach, displacement and cleanup. 123 checks green overall.
2026-08-13sl0ptty: M1 — layout tree, splits, focus, frames, and the hit list in usesl0p
app_t owns the tree, focus, the prefix and what a key means; the interactive client and the headless driver are now both thin front ends over it, so there is exactly one implementation of every command. Layout is the pure function D6 promised: layout(tree, rect) recomputed every frame, remainder spread across children so nothing drifts, and splitting in a direction that already exists grows that split rather than nesting — three splits give three equal columns, not 1/2 + 1/4 + 1/4. Chrome: rounded frames, aspect-corrected gap, centred titles. The split button is budgeted BEFORE the title and hit-tested from the rect it was drawn in, which is the fix the fork needed four attempts to land. Every painted element registers (rect, action) as it is painted; a click is a lookup. Mouse into a pane is translated to pane-local coordinates. Three bugs the harness caught, none of which unit tests would have: - one click on the split button split twice: press *and* release both fired. Chrome now acts on press; forwarding to a pane still gets every event. - callbacks were registered as &fn, but the option is pointer-typed, so the stored 'callback' was a stack address. Any OSC 2 or DSR jumped into a dead frame. Now a hard segfault turned into two regression tests. - a keystroke that changed nothing still emitted a cursor hide/show pair. The cursor is hidden lazily now, so an idle frame costs zero bytes. 96 checks green in 6s (40 unit, 23 screen, 33 layout), plus 9 live.
2026-08-13sl0ptty: M0.5 — the headless driver and the harness everything rests onsl0p
--script runs the whole compositor with no tty: commands in (send, raw, settle, resize, snapshot, alive), composited screen out as JSON — rows of text, style runs, cursor, and the hit list. Styles are emitted as runs rather than per-cell objects, because a test wants 'columns 0..5 of row 0 are bold red', not 1920 objects. The hit list is empty until M1 paints something clickable, but the type, the JSON, and hit_test() exist now: one geometry, established before there is anything to get wrong. Also fixes two host-policy defaults found by the cursor never appearing. lib-vt is a VT library, not a terminal, so it starts with DECTCEM (mode 25) off and grapheme clustering (2027) off. Both are now set via MODE_DEFAULT, so they survive RIS. tests/harness.py + tests/test_screen.py: 19 checks over composition, styling, wide chars/clusters, scrollback, resize and the full input round trip — including the kitty/legacy re-encoding matrix, which no longer needs a pty. make test is now 2.4s for 59 checks; make test-live keeps the four that genuinely need a tty.