aboutsummaryrefslogtreecommitdiffstats
path: root/src/sl0ptty.h (follow)
Commit message (Collapse)AuthorAge
* sl0ppty: rename from sl0pttyuser2026-08-14
| | | | | | | | | | | | | | | | 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.
* sl0ptty: kitty graphics passthroughsl0p2026-08-14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A program in a pane transmits an image and it appears. libghostty-vt parses the protocol and tracks images and placements; what a multiplexer has to add is re-emitting them to the client, which is exactly what tmux and zellij do not do — and the reason tools fall back to sixel under them. Per frame: walk the visible tab's panes, transmit each image once (a=t), place every visible placement (a=p,C=1) after the cell diff so a repainted cell cannot land on top of one, and delete placements that vanished. Image ids are remapped into a high range, so two panes both using image 7 stay two images and neither collides with what the client's terminal already had. A reattaching client is told everything again, because it has seen nothing. The cropping rule is the part worth remembering: a placement is N pixels drawn across M cells, so the placement itself tells you the pixels per cell — the client's font metrics never come into it. Clipping at a pane edge, or at the top of the viewport when a tall image is partly scrolled off, moves the source rectangle; asking for fewer columns or rows alone scales the image into them instead of cropping it. The first version did exactly that, and dropped partially-visible placements entirely rather than cropping them. Tests cover the multiplexer-specific case first: two panes, both using image id 7, must get different client ids. Then placement position in screen cells, following the layout, cropping at edges and when taller than the pane, nothing from collapsed panes, and removal when a pane closes or the image scrolls away. A live check confirms the wire: transmitted once, re-placed per frame, not re-transmitted. 416 checks green (389 headless + 27 live).
* sl0ptty: select-to-copy, middle-click paste, and toastssl0p2026-08-14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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).
* sl0ptty: scrollback — wheel, keyboard, and a clickable indicatorsl0p2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | libghostty-vt already keeps the scrollback, so the work was deciding whose wheel it is: - a program tracking the mouse gets the wheel events themselves; - a full-screen program without mouse tracking (vim, less) has no scrollback of ours to show, so the wheel becomes arrow keys, which is what it means to that program; - anything else scrolls our viewport. All three are tested against real programs rather than asserted in principle. C-a PageUp/PageDown/Home/End, and scroll_lines in the config for the wheel. Typing snaps back to the live view, because reading scrollback and then typing into a screen you are not looking at is a trap. New output does not yank the viewport back — it just grows the gap, which the indicator shows. The indicator is compact, right-aligned, budgeted after the split button and before the title (a title is the thing you can most afford to lose), and clicking it returns to the bottom. Three test failures on the way, all mine and all instructive: the top of the scrollback is the shell's first prompt rather than the first line of output; typing without a newline types rather than runs; and two side-by-side panes share a screen row, so asserting on line(y) reads both frames at once. 341 checks green.
* sl0ptty: M7 — layout files and suspended panessl0p2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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).
* sl0ptty: M4 — OSC 5577 pane status bars, buttons and click reportssl0p2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The payoff milestone: sl0ppi's pi extensions work here unmodified. printf '\033]5577;1;status;building 3/7\033\\' printf '\033]5577;1;buttons;approve:Approve;cancel:Cancel\033\\' ╰ building 3/7 ──────────────────────[Approve]─[Cancel]─╯ libghostty-vt's UNKNOWN_SEQUENCE reports APC only, so the sequences are picked up by a scanner (osc5577.c) reading the same bytes the terminal does — lib-vt discards an OSC it does not know, so nothing is drawn and no pane output has to be buffered. The scanner is a byte-at-a-time state machine, so a sequence split across reads still lands. Buttons are budgeted from the right before the status text gets any columns, and each registers its hit as it is painted — the split button's rule, for the split button's reason. A press sends the click report to that pane's stdin. In-band purposes arrive here too, and by construction can never be declared: a pane printing one is asking, not telling. A pane cannot relabel a purpose a layout declared, which is now tested end to end rather than only at the API. One real bug, found by the hostile-input tests: an over-long button id was truncated into a *valid* one, so a pane could mint a 40-character id that collides with the 32-character id its program already trusts. Ids are now unescaped into a large buffer and validated at full length. Acceptance test replays answer-picker's exact byte patterns, including `buttons` with no payload (drop buttons, keep status) and a click report that matches the extension's own CLICK_RE. Four test bugs on the way, each worth remembering: printf eats %3B as a format specifier; the driver's unescaper reads \0 as NUL so \033 became NUL+'33'; a trailing ST inside a double-quoted shell string escapes the quote (BEL avoids it); and readline eats a typed ESC as a meta prefix, so tests that drive a pane must not use an interactive shell. 187 checks green (160 headless + 27 live).
* sl0ptty: M2 — server/client split, detach/reattach, control socketsl0p2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0ptty: M1 — layout tree, splits, focus, frames, and the hit list in usesl0p2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0ptty: M0.5 — the headless driver and the harness everything rests onsl0p2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | | --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.
* sl0ptty: input decoder + per-pane re-encodesl0p2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | The half libghostty-vt does not do: decode what the outer terminal sends us into semantic events, then re-encode each event for the target pane against that pane's own negotiated modes. Decoder (src/input.c): legacy keys and modifiers, SS3, tilde keys, the kitty keyboard protocol (CSI u, shifted/base layout sub-params, press/repeat/release, associated text), SGR mouse incl. drag and wheel, bracketed paste, focus events, alt-prefix, UTF-8, and partial sequences retained across reads. Re-encoding (pane_send_key/mouse/paste) drives lib-vt's key and mouse encoders via setopt_from_terminal, so a pane gets exactly what it negotiated. The mouse encoder wants surface pixels; we declare a 1x1 pixel cell so cell coordinates are exact rather than approximated. The prefix key is handled on decoded events, not bytes, so it cannot be confused by a byte that merely looks like C-a inside a UTF-8 or escape sequence. Tests: 40 table-driven decoder cases (tests/input_test.c) + a live check that drives the same client into a legacy pane and a kitty/mouse pane and asserts each receives its own encoding (tests/live_input.py). 59 checks, all green.
* sl0ptty: M0 — libghostty-vt vendored, one pane, compositor, diff renderersl0p2026-08-13
Design contract in DESIGN.md. libghostty-vt vendored fresh from ghostty main@bedb6f2 (zig 0.16), built to a 14MB static lib. M0: pty spawn (no libutil), a lib-vt terminal per pane, a screen cell buffer, a diff emitter that repaints only what changed, raw mode, SIGWINCH, and a ~120Hz frame cap that coalesces pty reads. Also the seed of the headless harness: --headless dumps the composited screen without a tty, so composition is testable in milliseconds. tests/live_m0.py covers what headless cannot: raw mode, the diff emitter, resize propagation, and the quit path. 6/6 green.