aboutsummaryrefslogtreecommitdiffstats
path: root/include/slosh.h (follow)
Commit message (Collapse)AuthorAge
* osc 7501: say what you are doing in somebody else's protocoluser2026-10-09
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | OSC 5577 only works for programs that know slosh exists. The program status protocol is the same idea written down for everybody, by ghostty's author and implemented by ghostty and Rex, so a program that already speaks it needs to know nothing about us: printf '\033]7501;state=working:app=cargo:msg=YnVpbGRpbmc=\033\\' Implemented *as well as* 5577, not instead of it. 5577 is a two-way control channel -- buttons answer on the program's stdin, shaders are refused with a reason -- and 7501 deliberately is not, so neither absorbs the other. What 7501 buys is the programs we do not control. The scanner becomes one pass for four things: 5577, 7501, OSC 133 ; A and RIS. The last two are here because 7501 ties record lifetime to them -- a new prompt or a full reset is how a stale `working` dies without a heartbeat -- and because a pane should pay for that scan once. osc5577_fn gives way to osc_scan_cbs_t. Reports are all-or-nothing, which is the spec's rule: no state, an unknown state, a broken limit, base64 that will not decode, a control character in decoded text or a malformed id discards the whole report rather than writing a partial one. A malformed *pair* is skipped and the rest parsed. A pane keeps up to PANE_PS_MAX records with LRU eviction, and composes the one line it has from them by precedence -- blocked, error, done, working, idle -- resolving `app` from the nearest ancestor that has one, the way the hierarchy is allowed to mean something. It lands in the slot 5577's `status` already had, because every reader of a pane wants "what is this doing", not "which protocol said so"; `panes` reports program_state, program_kind, program_app and progress beside it, with -1 for a progress nobody reported, since a bar drawn at 0% says something false. The feature-detection reply is rate-limited here. Note for the next re-vendor: upstream lib-vt implements 7501 too, newer than our pin, and answers the support query itself through write_pty without rate-limiting it. See "What upstream did" under D1 in DESIGN.md before swapping this half of the file for GHOSTTY_TERMINAL_OPT_PROGRAM_STATUS.
* panes: read and write a pane by id, without taking focususer2026-09-29
| | | | | | | | | | | | | | | | | | `raw` wrote into the focused pane and `snapshot` composited the active tab, so driving a pane anywhere else meant focusing it or selecting its tab first. Both of those move the view of whoever is watching, and both lose to anybody doing the same thing at the same time. `raw` now takes an `id` (0 is the focused pane, as before), and `capture` answers one pane's viewport as text -- from the pane's own terminal, so it works for a pane in a background tab, changes no focus, and leaves the human's selection alone. It builds its selection as a snapshot rather than setting one on the terminal, because a read with a visible side effect is not a read. `capture` tells "no such pane" apart from "that pane is empty": a caller polling a dead id would otherwise wait forever on a pane that is quiet because it is gone.
* panes: $SLOSH_PANE, so a program knows which pane it is inuser2026-09-29
| | | | | | | | | | | | | | | | | | | | A program in a pane could name every pane but its own. `panes` says which one is *focused*, and focus belongs to the human, so anything that inferred "where do I live" from it did its work in whichever tab somebody else happened to be looking at -- and lost a race with anybody else asking the same question. So export the id into the child, the way tmux has $TMUX_PANE and zellij $ZELLIJ_PANE_ID. The id is taken before the pane is made, because pane_new spawns the program and assigning it afterwards is one instruction too late; it lives on the pane rather than the layout, so a rerun keeps it -- an identity that changed under a rerun would be no identity at all. Unset, not zero, when there is none: a stale value inherited from an outer session would read as this one's. `panes` also reports each pane's pid now, which is the same question answered for anything that cannot read its own environment: walk your parents up until one of them is on that list.
* osc 5577: `busy`, and a sidebar that shows it (busy_mark)user2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A status is a line of text, and a line of text cannot say whether it is still true. "building 3/7" reads exactly the same while it builds and ten minutes after it stopped, which is the one thing somebody reading it from another tab wants to know -- and the only workaround available to a program was to write the answer into the words and keep rewriting them. So `busy`: printf '\033]5577;1;status;make test\033\\' printf '\033]5577;1;busy;1\033\\' ... printf '\033]5577;1;busy;0\033\\' A flag of its own rather than a field of `status`, because a status is the *whole* payload after the verb -- `status;a;b;c` has always been the text "a;b;c" -- so anything added to that line would change what every existing sender means, including the five pi extensions D1 promises byte compatibility to. Orthogonal, so a program can leave one line up and toggle the other; anything but 0/false/no/off is on; `clear` takes both; and a pane whose program has gone is never busy, whatever it last claimed, because a spinner turning under a dead tab is a lie you keep looking at. In a sidebar the row turns `tab_status_busy` -- the theme's accent, derived like the other status colours, so no theme needs a line -- and takes a spinner in the column `tab_bar_padding` was already holding blank, so nothing reflows when a program starts or stops working. `busy_mark` is the frames, separated by spaces: one frame is a static mark and costs nothing, several make it turn, and only then does the session keep a frame clock -- via the same `animating` flag an animated shader uses, so the cost lands only while such a row is actually composed, and `""` drops the mark entirely and leaves the colour to say it. The frame is a pure function of the clock, like every other pulse here, so two clients attached to one session show the same one. `panes` now reports `status` and `busy`, which is the scriptable half of the same fact: what a pane says about itself, without reading its screen.
* themes: ask the binary where it is, not the prefix it was built foruser2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | `theme_name "sl0p"` with all seven themes sitting in /usr/share/slosh/themes: "no theme named sl0p". The lookup knew two places -- your own themes directory and the PREFIX this build was compiled with -- and on that machine the first did not exist and the second was /usr/local, because that is the Makefile's default and the package was built with PREFIX=/usr. Neither was where the themes actually were, and nothing in the search could ever have found them. A compiled-in prefix is a guess about where the binary will end up, and the binary is the one thing that knows where it really is. So the search now asks it: path_self() (/proc/self/exe, _NSGetExecutablePath, GetModuleFileName -- lifted out of server.c, which was already doing this for SLOSH_BIN) gives <bin>/../share/slosh/themes for an installed slosh wherever it was installed to, and <repo>/contrib/themes for one being run out of a build tree, which makes a checkout able to name a theme without installing anything first. The compiled-in prefix stays as the last candidate, for a binary sitting somewhere that says nothing about its data, and duplicates collapse so the usual install does not report the same directory twice. Your own directory is still first and still shadows everything, which is the property that matters; the rest is about the shipped set being findable at all. The tests that assumed a closed world of two temp themes now narrow to their own names by typing, since the shipped ones are (correctly) always in the list -- and the "no themes anywhere" toast lost its test, because with the shipped set reachable that state takes a binary with nothing beside it. The case that started this is a test of its own.
* search: a scrollback search bar on /, matches highlighteduser2026-09-08
| | | | | | | | | | | | | | | | | | | | | | | | C-a / opens an inline search bar on the focused pane's status row, on lib-vt's GhosttySearch (D4's pattern: the scrollback walk, reflow and alt-screen survival are the library's, the UI and the wiring are ours). Type to search live; the viewport follows the current match; up/down (or C-n/C-p) step through them with wraparound; Enter keeps your place, Escape snaps back. A k-of-n counter reads on the right, or "no matches". Every match on screen is tinted, and the one the bar is on is tinted apart -- warm gold by default, painted after the shader pass so the scrolled wash does not dilute a match you are hunting for. Four themeable colours (search_fg/bg, search_cur_fg/bg). Bare / took the key from help, which keeps ?; that needed the legacy decoder to read the shift out of a punctuation byte the way it already did for capitals -- without it ? and / decode identically (SLASH, no mods) and cannot be two keys on a client without the kitty protocol. tests/test_search.py drives it end to end (39 checks): the bar, the counter, stepping and wrap, all-match vs current highlighting, Enter/ Escape, new output updating the count, case-insensitivity, the keyboard belonging to the bar, click-away, and that / and ? no longer collide.
* pane: drop the render snapshot of panes on hidden tabs -- 154K -> 66K eachuser2026-09-03
| | | | | | | | | | | | | | | | | | | | An updated GhosttyRenderState retains a full copy of the pane's viewport (rows, cells, per-row arenas, styles) so renders can read it without the terminal lock. Measured with bpftrace and a lib-vt micro-benchmark, that copy is ~140KB for an 80x24 pane -- more than twice the terminal it mirrors (~57KB) -- and every pane kept one forever, composed or not. Only the current tab is ever composed, so app_compose now drops the snapshot of every pane on the other tabs (a pointer test once dropped), and pane_new no longer creates one at all: pane_compose and pane_select_word build it on first use. Both already begin with ghostty_render_state_update(), so a dropped snapshot is rebuilt, never missed, and the tab switch already forces a full repaint. Server memory per empty 80x24 terminal drops from 154K to 66K anon (Superlogical reports 68K on macOS; tmux 4K, zellij 1.2-6.2M). Fifty background tabs: 8.2M -> 3.8M, reclaimed on close, and cycling through tabs does not ratchet. What remains is the lib-vt terminal itself.
* pty: install the terminfo entry implicitly when it is missing everywhereuser2026-09-02
| | | | | | | | | The lookup miss now writes the embedded entry to ~/.terminfo and uses it, so a bare box gets the full TERM with no step at all. An entry that exists anywhere is respected -- possibly newer, certainly somebody's -- and a HOME that cannot be written leaves the xterm-256color fallback standing. --install-terminfo stays as the by-hand spelling; the writer moved to pty.c where the panes are.
* Add competitive multiplayer support.Ingmar Steen2026-09-02
| | | | Last input wins, canvas size is adjusted to active connection.
* path: a directory ends at either separator on WindowsPeter Geissler2026-08-28
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two places split a path by hand, both with strrchr(path, '/'), both falling back to "." when there was none: include_path() in config.c and watch_config() in server.c. On Windows "none" is the ordinary case. `SLOSH_CONFIG=C:\Users\you\slosh\config.kdl` is how a person writes it, and a path that has been near a native API comes back all backslashes whatever it started as. So an include resolved against "." -- `cannot open ./themes/t.kdl` -- and, worse and quietly, the config watcher watched "." too. Not the config's directory: the current directory of whatever started the session. Hot reload did not fail, it watched somewhere else and waited. Saving your config did nothing at all, with no error to go looking for. The default path escaped this by accident. slosh builds it as `C:\Users\you/.config/slosh/config.kdl` -- mixed, because a literal joined to $HOME always is -- and that one embedded '/' is what strrchr found. It worked for the path slosh writes and not for the one a user does. Both callers now use path_dir(), which learns to stop at a backslash too, and server.c takes path_base() for the other half rather than keeping its own copy of the arithmetic. Windows-only: a backslash is a legal character in a POSIX file name, and splitting on it there would invent a directory out of a file called `a\b`. path_dir() also keeps the separator when what is left would be a bare drive, because `C:\config.kdl` lives in `C:\` and `C:` names the current directory on drive C, which is a different place. Verified on the Windows box with a fully backslashed SLOSH_CONFIG: --check passes, the include loads, and editing the config reloads the running session. Watching a second directory is still unreliable; that is the watcher's own bug and the next commit.
* path: an absolute path does not always start with a slashPeter Geissler2026-08-28
| | | | | | | | | | | | | | | | | | | | | | | | | `include "C:/themes/t.kdl"` on Windows did not fail to find the file. It found a different one, or rather looked for it: absoluteness was tested as `path[0] == '/'`, a drive letter is not that, so the path was read as relative and joined to the including file's directory. The result was cannot open C:/Users/you/cfgA/C:/Users/you/cfgB/t.kdl which at least says what happened. A layout's `cwd="C:/srv/api"` took the same turn through path_resolve() and said nothing at all -- the pane started in the wrong directory, which is the failure this file's own header comment is about. Windows spells absolute three ways and all three now count: rooted on the current drive (`\dir`), rooted on a named one (`C:\dir`, `C:/dir`), and a UNC share. A drive letter has to be followed by a separator to qualify, because `C:file` is relative to the current directory *of that drive* -- a real thing, and not one we can resolve for anyone. Windows-only, deliberately. `C:` is an ordinary file name on POSIX and a path beginning with one is relative there, so reading it as a drive would break the platform that works in order to fix the one that does not.
* ctrl_d_exits: Ctrl-D at a prompt, where the tty cannot do itPeter Geissler2026-08-28
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Windows has no VEOF. The console driver does not know the character, so ^D arrives at cmd.exe and PowerShell as an ordinary keystroke and they ignore it -- it even echoes, which is how you find out: the line reads `echo hello^D`. The one key that closes a pane on every other platform does nothing at all, and closing a pane becomes a word you have to type. That you can type `exit` instead is the tell: the intent was never ambiguous, only unspoken. So slosh speaks it. What goes to the pane is the word, not a signal and not a close from outside -- the shell runs its own cleanup, returns its own status, and keep_dead decides what is left behind, all three of which reaching past it would skip. It is also what the user would have typed, so there is nothing new to explain. The knob is what makes it honest, because a wrong guess costs a pane. It fires only with the shell itself in the foreground (a program that is running may want the EOF for itself -- `cat` reading to end is the whole job), only on the primary screen (in an editor or a pager ^D is half a page down, and the alternate screen is the pane stating that rather than us matching process names), and only on an empty line (POSIX sends EOF there; what it does not do is discard what you typed). The line is an estimate. The shell's editor is on the far side of a pty, so pane_send_key counts what it forwarded since the last Enter. Editing it does not model -- a recalled history entry, ^U, completion -- leaves the count too high, which forgets to exit. That is the direction to be wrong in; the other one throws away a half-typed command. Default true on Windows, false on POSIX, where the line discipline already does this and does it properly: ^D there is also EOF for `cat` and every REPL, none of which is ours to intercept. A knob rather than an #ifdef so the guards are the same code everywhere and can be tested where the test suite runs -- test_ctrl_d.py turns it on and drives all four decisions, plus the case that does exit. Telling trapped from forwarded needs a program that reports what it received: on POSIX a forwarded ^D also ends an idle shell, and looking for the word `exit` cannot separate them either, because bash prints `exit` itself on EOF. Verified on the Windows box as well, where it is the default: ^D at an idle cmd.exe prompt takes the pane (and with it the session, having no panes left), `more` in the foreground keeps its own EOF, and a half-typed line survives with the line still on screen.
* windows: a native port, on ConPTY and AF_UNIXPeter Geissler2026-08-28
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | slosh builds and runs on Windows 10/11, x86-64 and ARM64, as one exe with no runtime dependency beyond the OS: panes, splits, tabs, detachable sessions, the config file and its live reload, the control socket, the mouse. The port is a compatibility layer rather than a fork. compat/win holds stub <poll.h>, <sys/socket.h>, <termios.h> and friends that redirect to include/compat_win.h, which is force-included ahead of every translation unit -- so the existing sources keep their POSIX includes and their POSIX call sites, and the platform differences are answered in one place. Two decisions carry most of it. Everything the event loop waits on is a socket. On POSIX a pty master, an inotify descriptor and a unix socket are all file descriptors, so one poll() covers them; Windows has no such union, and WSAPoll waits on SOCKETs and nothing else. Rather than rewrite server.c's loop around WaitForMultipleObjects -- which caps at 64 objects and cannot express POLLOUT -- each non-socket source gets a pump thread that copies it into one end of a loopback socketpair. The loop is then the POSIX one unchanged, and only what sits behind the descriptor differs. Socket descriptors are tagged rather than guessed: a wrapped SOCKET comes back as SL_SOCK_BASE + slot, so read, write, close and poll can tell a socket from a CRT descriptor with certainty instead of inferring it from the numeric range. That is why socket() and accept() are shimmed too -- an untagged descriptor escaping into the loop is the one failure mode this scheme has, and it is worth closing completely. Three things bit hard enough to be worth naming. A pseudoconsole does not stop the child inheriting your stdio. CreateProcess copies the parent's standard handles into the child when STARTF_USESTDHANDLES is absent, and those win. With slosh's own stdout redirected -- a pipe under ssh, a file under --script, a log under the daemon -- the shell wrote its banner into slosh's output instead of into the pane, and the pane had a correct title (ConPTY emits that itself) over an empty body. The three handles are cleared for the duration of the call, leaving the child nothing to inherit. A ConPTY never reports EOF. The output pipe belongs to the pseudoconsole, not to the child, so it stays open after the child exits and pane_pump()'s read() -- the entire mechanism by which a pane learns its program is gone -- never returns 0. A pane whose shell had been told `exit` sat there looking alive forever. So the exit is waited for on the process handle, and ClosePseudoConsole is what then produces the EOF. That third thread is also why the per-pty state is reference counted: a pump blocked on a dead pipe must not be waited for, or pty_close would hang the session on the one program that misbehaves, so whoever finishes last frees. cmd.exe does not use CommandLineToArgvW's quoting. A literal quote is \" to an ordinary program and a backslash-then-end-of-quoted-section to cmd, so building `cmd /c notepad "C:\...\config.kdl"` the first way handed the editor \"C:\...\config.kdl\", backslashes and all, and notepad answered "Not a valid file name" about a path that existed. cmd /c now gets the rest of the line verbatim -- the caller composed a complete command with its own quoting, exactly as it does for sh -c. Paths are mixed, and only some programs mind. slosh joins with '/' while the environment supplies '\', so the config landed at C:\Users\you/.config/slosh/config.kdl. The CRT opens that without complaint, which is why --check reported it fine, but a GUI file dialog rejects it outright. Anything handed to another program now goes through sl_path_native() first; the internal representation is left alone. The rest is small. Panes run %ComSpec%, sessions live in %LOCALAPPDATA%\slosh rather than $XDG_RUNTIME_DIR because a session socket must not follow a roaming profile to another host, there is no SIGWINCH so a resize is noticed by comparing the console size on a bounded poll, no fork so the daemon is a fresh detached process that reconstructs its arguments on the command line, and no process group on a pty so the foreground program is the youngest descendant of the pane's shell. C-a e prefers whichever console editor is actually installed -- notepad is a GUI program, so a pane running it draws nothing and looks broken -- and says so when it has to fall back. make -f Makefile.windows ARCH=x86_64 (or aarch64) cross-compiles from a mac or Linux, and builds natively on the box with zig on PATH.
* selection: drag past an edge scrolls the viewport under ituser2026-08-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* slosh: OSC 8 hyperlinks pass through the compositoruser2026-08-20
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | lib-vt tracks a hyperlink per cell and slosh dropped every one of them: the client's terminal can only offer a link it was sent, so `ls --hyperlink`, gcc diagnostics and friends silently lost theirs inside the session. The link rides beside the cell -- cell_t is the shader ABI and a link is not a colour -- as a plane of ids into an interned URI table, append-only so an id in prev_link still means the same URI next frame (which is what lets the diff compare ids instead of strings), and capped so a hostile pane cannot grow it forever. URIs with control bytes are refused outright: they are emitted verbatim inside an OSC, and a control byte in one is a way to write escape sequences into the client's terminal. The diff wraps emitted runs in OSC 8, closes what it opens before the frame ends, and treats a link-only change as a change. Chrome painted over a link takes the link with it, so a frame, modal or float across a URL is never clickable with the pane's link; links survive scrollback with the text that carries them. Plain-text URLs stay the outer terminal's own matcher, with the catch documented in panes.md: under mouse capture Ghostty only offers links with shift added to the gesture (shift+cmd hover/click), shift being its capture escape. Wrapped URLs can never be regex-matched through a multiplexer -- rows repaint as hard lines -- which is exactly what OSC 8 links, passing through whole, are for. `snapshot format:"bytes"` returns the emitter's own output for a frame (a second call is the delta): the model and the bytes are two different things, and this codebase already paid once for asserting on one while the other was wrong.
* 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().