summaryrefslogtreecommitdiffstats
path: root/docs (follow)
Commit message (Collapse)AuthorAge
* 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.
* 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.
* slosh --install-terminfo: the binary carries its own terminfo entryuser2026-09-02
| | | | | | | | | | | | The xterm-256color fallback works everywhere; the real entry is better, and a tarball user on a bare box had no easy way to get it. The entry is generated from the vendored tree (contrib/terminfo, so it describes the terminal core we actually ship), compiled with tic, embedded as bytes, and written to ~/.terminfo under every name it answers to, letter and Darwin-hashed layouts both. No root, no package, no conflict with a ghostty installed later. Proven end to end in a mount namespace: bare box falls back, installs, and the next pane says xterm-ghostty.
* pty: fall back to xterm-256color where xterm-ghostty has no terminfouser2026-09-02
| | | | | | | | | | | | TERM=xterm-ghostty was unconditional, but the entry ships with ghostty and a machine that has never seen ghostty has ncurses' database and nothing else. There, nano exits the moment it opens (C-a e flashing a pane) and a shell that cannot look up kbs guesses at backspace. The entry is now looked up the way curses would -- TERMINFO, ~/.terminfo, TERMINFO_DIRS, the system dirs, letter and Darwin-hashed layouts -- and when absent, panes get xterm-256color, which every ncurses database carries. Verified both ways with a mount namespace hiding the database.
* docs: sweep the em dashesuser2026-09-02
| | | | | | | Parentheses for the asides, colons for the explanations, commas and semicolons for the joins, (none) for the empty table cells. Same treatment the landing page got, across README, docs/, and both manpages (\(em included).
* attach tag: just the count, in theme coloursuser2026-09-02
| | | | | | | The +x+y offset goes -- 'N clients' says the thing that matters. The inverse-video chip goes with it: the tag now draws in attach_fg / attach_bg from the theme, defaulting to gray ink on the terminal's own background. Standing information, not an announcement.
* multi-attach: say who else is watching, and where you areuser2026-09-02
| | | | | | | | | | | | A small tag in each client's own top-right corner when several terminals share the session: the viewer count, and on a view that is a crop being panned by somebody else's cursor, the crop's offset into the shared screen ("2 clients +12+4") -- which is exactly the moment a screen moving on its own needs explaining. Stamped per client after projection, because every terminal has its own count of others and its own offset; the shared status bar is composed once and can be cropped out of a panned view anyway. attach_indicator false hides it.
* multi-attach: the config knobsuser2026-09-02
| | | | | | | | | | | | | The patch made shared attach unconditional; these make it a policy. multi_attach false restores the classic displacing attach (EXIT_REPLACED, which old clients already understand). size_follows picks whose size the shared screen takes: active (the patch's rule), smallest (the tmux rule: filler, never a crop), or largest. The size fold lives in apply_canvas, so any attach, resize or departure can move the canvas under the folding policies -- including a non-active client leaving, which under smallest may have been the very client holding the floor down. A config reload re-applies both knobs to the running session.
* Add competitive multiplayer support.Ingmar Steen2026-09-02
| | | | Last input wins, canvas size is adjusted to active connection.
* 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.
* public links: point at github.com/sl0p-foo/slosh instead of cgituser2026-09-02
| | | | | | | | GitHub is the public interaction point now. The formula and PKGBUILD fetch GitHub's /archive/ tarballs (same git-archive bytes, per-ref stable); the PKGBUILD is bumped to 0.1.2 while at it, and its cd's drop the v -- GitHub tag archives extract to slosh-X.Y.Z/. The cgit remote stays for pushing; only published URLs change.
* point homebrew tap at github.com/sl0p-foo/homebrew-sloshuser2026-09-02
| | | | | brew auto-resolves GitHub taps, so the explicit URL is no longer needed: 'brew tap sl0p-foo/slosh' is all users type.
* docs: choredocs-newPeter Geissler2026-09-02
|
* docs: deprecate boring design narrative, more trimming and cullingPeter Geissler2026-09-02
|
* docs: big overhaulPeter Geissler2026-09-02
|
* layout: a float sized but not positioned is centred, per axisuser2026-08-30
| | | | | | | | | | | | | | | | docs/layouts.md always said an absent coordinate takes the centred default; the loader said 0, and a layout float with only w= h= landed in the top-left corner. Now an axis the file does not name is FLOAT_CENTRE, a sentinel no real coordinate can reach, and float_clamp answers it with the middle -- every frame, at whatever size the terminal is by then, until an explicit move or resize writes a real number over it. The webdemo's welcome float is the customer: it must sit centred on a terminal whose size the layout file cannot know. The dump writes a still-centred axis by omission, because the number it happened to land on would pin to this screen's size the one intent that deliberately has none. Every mutating verb already writes back from the drawn rect, so the sentinel survives nothing it should not.
* graphics: a frame the client never got must be said againuser2026-08-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A pane image is re-emitted to the client as decoded RGBA, which for one screenshot is ~4MB of base64 in a single indivisible message -- and conn_send silently dropped anything that would overflow the 4MB outbox, while push_graphics ignored the refusal and the model advanced anyway. Everything stateful in that dropped frame was lost for good: the image was marked sent (so its placements drew nothing, forever), and the deletions rode out with it (so a placement the client had was never revoked). Reported as screenshots in a recovered pi transcript parked at a fixed spot on the screen, immune to scrolling and pane swaps -- those were placements whose deletion left with a dropped frame. The 1000x800 one could never be delivered at all: its transmission alone was bigger than the cap. So the frame is now transactional. Deletions queue and are repeated every flush until gfx_commit() confirms the client heard them; transmissions recorded in a frame are unwound by an undelivered commit so they are sent again. A flush nobody commits counts as undelivered at the next gfx_begin(), which is what the `graphics` control command is: bytes rendered for a CLI, not the client. The server reports the verdict and keeps the paint pending until it lands. The outbox cap now bounds the backlog rather than backlog-plus-message, so one big transmission on a drained queue always fits -- and that surfaced a second way to lose: a queued image legitimately holds the backlog over the cap until the client swallows it, and push_frame read that as a dead client and closed the connection. The session auto-detached every time a screenshot crossed the wire. A full outbox is a client that is *behind*, not gone: push_frame now marks the screen for a full repaint (the dropped diff means the client's cells are unknown) and retries as POLLOUT drains the queue; a client that truly left still shows up as a write error there. The headless suite pins the owed-until-committed contract; the new tests/live_graphics.py drives the real socket with a bigger-than-the-cap image and a reader that stalls, and would have caught both bugs: the flood must not detach, the transmission must arrive whole, and a placement scrolled away must be deleted -- then come back on scroll without a second copy of the pixels.
* docs: less wordyuser2026-08-29
|
* gitignore: DESIGN.md and .fastfeedback are local, not the projectuser2026-08-28
| | | | | | | | DESIGN.md was a scratch design doc and .fastfeedback a per-repo tool cache; neither belongs in the tree. Both stay on disk, ignored. Everything that pointed at DESIGN.md now points at docs/design.md, and the decision counts quoted from it (which had already drifted: the README said twenty-one, the docs said twenty-two) are gone with it.
* windows release: a zip in dist/, built beside the linux tarballsuser2026-08-28
| | | | | | | | | make -f Makefile.windows dist stages slosh.exe as slosh-<version>-windows-<arch>.zip -- a zip, not a tarball, because Windows is the machine that will open it -- carrying the same licensing paperwork as the linux tarballs, with docs/windows.md riding along instead of the manpages. `make release` now builds both architectures via release-windows, so the windows zips land under the same SHA256SUMS as everything else.
* 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.
* docs: the Windows build is a cross-compile, and only thatPeter Geissler2026-08-28
| | | | | | | | | | | | | | | | | | | | | The docs offered a native build as an equal alternative -- "the same command builds natively on the Windows box itself, with zig on PATH and a make" -- and it does not work. Makefile.windows needs sed, cat, mkdir -p and shell grouping, none of which cmd.exe has; and on ARM64 `zig cc` in zig 0.16.0 dies with an access violation on a one-line C file, so the compiler cannot run there at all. The zip that does it matches the published shasum, so this is upstream rather than a bad download. Nobody following those two sentences reached a binary. So they are gone, along with the "# host arch, native" usage line in the makefile and the "cross-compiles too" aside in the two install sections, which both implied that building on the box was the normal path and the cross-compile a bonus. It is the other way round, and now the pages say so once, in the same words each time. `builds and runs natively` on the doc's first line loses `builds`: what is native about slosh.exe is the binary, not where it was made.
* windows: fall back to `edit`, not to a notepad windowPeter Geissler2026-08-28
| | | | | | | | | | | | | | | | | | | | | | | `C-a e` on a Windows with no editor installed opened notepad, which is a GUI program: the pane it was spawned in drew nothing, so the split sat there empty while the config opened in a window somewhere else. The toast said as much, but a pane you have to be told to ignore is still the wrong answer. Windows 11 24H2 ships Microsoft Edit as `edit`, and it is a console program -- it fills the pane like nvim or nano would, so the fallback now has the same shape as the thing it is falling back from, and a first-time user gets a usable editor without installing one. It goes last of the console editors rather than first. Having fetched nvim is a statement of preference; having been shipped `edit` says nothing about what you want, so anything you chose yourself outranks it. notepad stays behind it: `edit` is not on Windows 10, or on 11 before 24H2, and something that opens is better than nothing when the alternative is an empty pane. sl_which() already tries .exe, so it is found as installed; the search still only runs when neither EDITOR nor the config asked for anything.
* docs: the Windows port, and what is different therePeter Geissler2026-08-28
| | | | | | | | | | | | | | | | An install subsection alongside macOS and Linux, and a page under Inside for the rest: how to build it, what changes about running it (%ComSpec% panes, sessions in %LOCALAPPDATA%), and the reasoning the code can only gesture at in comments. The three traps are written down in full, because each one presents as something other than what it is and cost real time: a pane with a title and no body (the child inherited slosh's stdout rather than the pseudoconsole), a pane that stays alive after `exit` (a ConPTY's output pipe belongs to the console, not the child, so the read never ends), and an editor refusing a path that plainly exists (cmd.exe and CommandLineToArgvW disagree about what \" means, and separately the CRT accepts a mixed-separator path where a GUI dialog will not).
* install docs: one subsection per OSuser2026-08-27
| | | | | | | | | | | | 'With Homebrew (macOS or Linux)' followed by a source build made every reader parse which half was theirs; now macOS and Linux each have a heading, and you read yours. macOS is the tap; Linux is the same tap under Homebrew on Linux, or the source build, which the docs landing page had never mentioned brew at all. Also fixes the README telling people to run bare 'make', which prints the help text (the default goal) and builds nothing. install.sh had this figured out and commented on it; the README had not.
* README/docs: stop leaning on the aside dashuser2026-08-27
| | | | | | | | | | | | | 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'.
* README/docs: spell the dash --, as the rest of the tree doesuser2026-08-27
| | | | | | | | | | | 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.
* docs: the landing page stops arguinguser2026-08-27
| | | | | | | | | The 'Why another one' section was two paragraphs of case-making against other multiplexers, and a landing page that defends its existence reads as unsure of it. What the section said truthfully -- the vendored core, recompute-not-remember, scriptable to its depth -- design.md already says where it belongs. The one-line closer stays: 15k lines of C and no dependencies is a fact, not a defence.
* docs: drop the ascii mockup from the landing page toouser2026-08-27
| | | | | | Same reasoning as the README's (24a6db01): the sketch says nothing the first paragraph did not, and on the rendered site it sat where the reader was deciding whether to keep reading.
* slosh(5): the file formats get their manpage; slosh(1) learns shadersuser2026-08-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | | Section 5 is where unix keeps its file formats, and slosh has two documents sharing one syntax -- the configuration and the layouts -- so they share a page the way they share a parser. docs/slosh.5 covers the KDL subset, where the file lives and how reload refuses a broken one, include and its precedence, every top-level setting grouped the way config.md groups them, the theme block, the shader vocabulary with the expression language summarised, the states and their ranking, the keys block including direct bindings, and the layout schema (tab, pane, and their properties). Held to the source like slosh.1 before it: every name in config.c's KNOWN_TOP -- the list the loader refuses unknown settings against, itself held to the loader by test_config.py -- must appear in the page, so a knob cannot be added without this page hearing about it. The pair must cross-reference each other, and both must be pure ASCII and lint clean under groff -ww. slosh(1) grows a SHADERS section -- the one feature a synopsis reader would never guess a multiplexer had -- and its SEE ALSO now names slosh(5) and nothing else: the other multiplexers can advertise themselves. Install puts the page under share/man/man5, the release tarballs carry both pages, and the Homebrew formula installs what the commit it is building has, guarded as before.
* a manpage: slosh(1), installed, shipped, and held to the sourceuser2026-08-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | The distributables were a bare binary and a directory of Markdown, which is fine until somebody types 'man slosh' -- the unix-traditional place to ask. docs/slosh.1 is hand-written man(7), pure ASCII roff, no toolchain: the synopsis, the options, the session commands, the default keys, the mouse (including the new drag-past-an-edge scrolling), where the configuration lives, what SLOSH_* means in a pane's environment, files, exit status. The prose docs stay the long form; the page says so and points at them. 'make install' puts it under PREFIX/share/man/man1 (and uninstall takes it away), the release tarballs carry it beside the LICENSE -- a tarball is what a packager starts from -- and the Homebrew formula installs it when the commit it is building has one, guarded because the formula must be able to build commits older than the page. Prose about a program drifts unless something holds them together, so test_docs.py holds the page to the source the way it already holds the site to docs/nav: every option main.c's parser compares against and every word command must appear in the page, every SLOSH_* variable the code reads or sets must be documented, the roff must be pure ASCII (it travels through implementations that never heard of preconv), and where groff exists the page must lint clean at CHECKSTYLE=3 with -ww. Adding a flag without documenting it is now a failing test, which is the only kind of documentation policy that survives contact with a deadline.
* macos: the pipeline is the project's, the identity is yoursPeter Geissler2026-08-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | Makefile.macos was kept out of git because it named a team, a person and a mac. But almost all of it is mechanism -- build, sign, zip, notarize, staple, unlock a headless keychain -- and that is the project's, not the releaser's. Keeping the whole file out to protect four values meant the one machine that had it was the only place a release could be cut from, and that nobody could read how signing worked without being handed it. So the split is inverted. Makefile.macos, the builder doc and the scripts are tracked; macos-release.mk holds TEAM_ID, SIGNER and BUILDER and is the only thing gitignored. macos-release.mk.example is the template. The identity strings are now assembled from SIGNER and TEAM_ID rather than written out, which means an unconfigured checkout composes "Developer ID Application: ()" -- plausible, wrong, and diagnosed only by codesign a minute into a build. require-identity and require-builder catch it before anything runs and name the file to create; `help` prints "(unset)" rather than the empty shape. macos-remote-build now pushes macos-release.mk instead of Makefile.macos. That is the better arrangement anyway: the builder gets its pipeline from git at the commit being released, so a release is signed by the flow belonging to it and not by whatever the laptop was carrying. MACOS-BUILDER.md was in the same position -- local, and referenced by a file that is now tracked -- so it becomes docs/macos-release.md with the certificate name and notary key id replaced by their shapes.
* docs: config.md audited against the code, three correctionsuser2026-08-26
| | | | | | | | | | | | | | | | | | An audit of every claim in docs/config.md against the parser and the dump found the page complete -- all 47 top-level settings named, no ghosts, defaults and error strings as printed, 48 theme colours exactly -- and three statements wrong or imprecise, all fixed here and in the two comments that repeated one of them: - compact does not switch off gap_aspect. The divider ignores it, but padding is still aspect-corrected through it and the float nudge step still uses it; saying it 'stops applying' was wrong in config.md, config.h and config/config.kdl alike. - 'Two of those take more than one value' miscounted: padding, min_pane/min_split (cols=/rows= properties, whose syntax the page never showed) and project_roots all do. - a compact boundary saves four *columns* or three *rows*; 'four' with no axis was the columns number wearing a general claim.
* compact: shared borders instead of gaps (D23)user2026-08-25
| | | | | | | | | | | | | | | | | | | | | | One knob, `compact true`: panes pack flush against 1-cell divider lines that meet in real junctions, one frame rings the tab, and a pane's title rides the line above it -- a boundary costs one cell where the default look spends four. The lines are strokes: each write says which edges of its cell it needs, unions with what is there, and never clobbers text, so ├ ┬ ┼ fall out of paint order and the focus ring recolours without inventing arms. The divider *is* the gap (eff_gap, one answer for the layout, split_fits and the resize clamp), so gap hits, hover hints and corner drags work unchanged. Interior edges give up click-to-split -- one cell cannot hold two verbs -- and a pane's status row overlays its last content row. Floats, zoom and the D6 flatten keep classic frames through place_hidden: zero new tree cases. Underneath it, mode or no mode: a border drag now moves the boundary on that side, converted on the first motion so the press that never moves keeps its old meaning -- the handle's click still splits, the rim's is still deliberately nothing (drag.rim). Which is also the fix for gap 0, where flush panes leave no gap cell to grab and the resize gesture had silently vanished with it.
* docs: fact-check against the loaderuser2026-08-22
| | | | | | | | 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.
* slosh: put-away panes are miniature pane framesuser2026-08-21
| | | | | | | | | | | | Brackets were tried and still read as text. Each min-bar entry is now a three-row chip wearing a real pane frame -- the same corners, because a frame is what a pane *looks like* in this program, so a shrunken one reads as exactly what it is: a pane, clickable. The put-away mark rides inside each chip and the lone legend is gone; a rung pane's bell mark keeps its own colour inside the frame; hover lights the whole chip and the whole chip is the hit. The bar costs three rows instead of one while anything is minimised (and nothing when nothing is), which is what an affordance that works is worth.
* slosh: minimized panes wear buttons in the stripuser2026-08-21
| | | | | | | | | | | A row of dim bare names read as a caption, and nobody clicks a caption. Each entry now wears square brackets -- [▬ name] -- because that is what clickable already looks like in this program: [re-run], [close], the buttons a pane's own row offers. The put-away mark rides inside each entry instead of standing at the head of the row as a legend: a legend explains the row once, a mark per entry says what each button is. A rung pane's bell mark keeps its own colour inside the brackets, and the whole bracket is the hit, as one geometry demands.
* slosh: M10c, the memory -- floats survive being written downuser2026-08-20
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A dumped session quietly re-tiled its floats, which was the one data-loss gap left in D22. dump-layout now records `floating=true x= y= w= h=` -- the *wanted* rect, because intent is what survives a session and the next screen's layout clamps it like any frame -- layout files declare the same (linted: floating is a bool, x/y/w/h mean nothing without it), and a file whose every pane floats self-heals through ensure_tiled: the top float lands, the same rule closing the last tiled pane follows, no lint required. save-workspace inherits all of it from the shared dumper. `float` over the API takes x/y/w/h and then *places* rather than toggles -- a placement is a statement about where, not about whether -- floating the pane first when it is tiled, re-placing when it already floats, with omitted fields meaning "keep". The intent is stored exactly as asked: the write-back rule is for hands, whose drags need the clamp to agree with the eye; a script means what it typed. And kitty graphics respect the stack. The cell compositor gets occlusion free from paint order, but placements are sent after the diff and would paint an image straight over a float covering it. Each pane's placements are now clipped by the float rects above it -- every float for a tiled pane, the higher-raised for a float -- cropping what one clean edge can express (the pane-edge source-rectangle arithmetic aimed at another clipper, including from the top and left, where the source origin moves and the sub-cell offset is consumed) and suppressing what it cannot: a strip across the middle or a corner overlap leaves a shape one placement cannot say, and the image returns whole when the float moves. No z= needed -- clipping is the z-order, since nothing is ever allowed to overlap. D22's "still owed" paragraph is paid and rewritten; M10 is fully checked.
* slosh: fold floating panes and the drop grammar into DESIGN.mduser2026-08-20
| | | | | | | | | | | | D22 records the float contract: the minimized pattern (a flag and a wanted rect on a leaf that keeps its seat), derived-vs-remembered rects, the hit list as the z-order, the pop to the centre, the shadow-not-a-state, the backdrop invariant, edge-pinned resizes, and the key decisions -- with M10c (layout files, dump-layout, placement over the API, graphics occlusion) recorded as still owed. M11 extends the drag machine section with the drop grammar: swap centre plus third-width insert bands, the fill as the promise, release derived from the painted hits, and the sibling-reorder rule. The working plan file served its purpose and goes; git remembers it.
* 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: floating panes and the drop grammaruser2026-08-20
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A pane can float: lifted out of the layout, drawn over the tiled panes and under the modals, freely moved and shaped. The model is a flag and a rect on the leaf, exactly like `minimized` -- the seat in the tree is kept, layout skips it, the siblings absorb its share, and un-floating returns it home. The drawn rect is the wanted one clamped to the tab every frame, never written back, so a terminal squeeze round-trips exactly; an explicit move or resize *does* write back, because the hand is the author of the intent. Floating pops the pane to the centre: the jump is what says it happened. Painted after everything tiled, so the hit list -- searched backwards -- resolves an overlap to the float you can see: the z-order and the click order are the same list. It casts a shadow (float_shadow, one number), and `floating` joins the states table above `unfocused`, so a float is never dimmed as merely-unfocused. The hands: the title moves it, any other border resizes it (a bottom corner on both axes; the grabbed edge pins at walls and floors, never sliding the window), H J K L move a focused float, the =/+ key grows it about its centre, `-` shrinks it (its split was a refusal anyway), and hovering shows which edges a grab would take, from the same derivation the press uses. Equalize moved to `0` to free the key; `C-a f` floats (finder went to `s`, where it reads as search); `C-a F` opens a new floating shell -- the throwaway terminal; the function row f1..f12 became nameable in configs along the way. Guards throughout: the last tiled pane cannot float or minimize away, closing it lands the top float, splits are refused with their own words, floats are not swap targets. Dropping got a grammar: while a pane is carried, the pane under the pointer subdivides into a swap centre and four edge bands meaning "insert me beside this pane, on this side" -- detach_leaf + place_beside, the surgery cross-tab moves already had. Bands are thirds (the first map of quarters showed the swap owning half of each axis), the hovered band fills the half the drop would hand over, the release resolves against the painted frame's hits so it cannot disagree with the fill, and a drop between siblings along the axis is a reorder that always fits -- refusing it was refusing the most ordinary drag there is. Also: rename-pane/rename-tab as palette actions (renaming was a double-click and nothing else, the one verb the palette could not reach), and C-u clears the rename editor like it clears the picker's query. Design log in PLAN-FLOATING-PANES.md; to fold into DESIGN.md as D22/M10-11.
* slosh: layout files are *.layout, not *.layout.kdluser2026-08-19
| | | | | | | | | | | | | | | | | | | | | The old name argued that the extension should say how to read the file. The argument lost to a collision: zellij speaks the same KDL syntax under that same extension with a different schema, and to anyone arriving from there .kdl said *whose* file it is -- naming the wrong owner is worse than not naming the parser. So a layout is x.layout, a project's is slosh.layout, the name says what the file is and --check says how it was read. (It is still KDL underneath; point your editor's highlighting at *.layout.) Nobody's checked-in workspace vanishes on upgrade: the project scan still finds slosh.layout.kdl, never writes it, and where both spellings exist the new one wins, because two files disagreeing needs one answer. The unparseable-file heuristic in --check learned the new suffix -- it matched '.layout.' with a trailing dot, which the new name no longer has. The webdemo overlay moves with it (demo.layout), rebuilt and boot-checked.
* slosh: split app.c into five units, mechanicallyuser2026-08-19
| | | | | | | | | | | | | | | | | | | | | | | | | | app.c was 7k lines wearing section markers; now the markers are files, cut exactly along them, no behaviour change: app.c the session core: config in force, pane and tab lifecycle, walking, input routing, introspection app_layout.c layout, tree edits, resizing, equalising, rotating, focus app_session.c layout files, checking, workspaces, dumping a session app_draw.c drawing, the shader passes, kitty graphics app_ui.c the finder, modals, the palette, the project picker, renaming include/app_internal.h carries what the five share: the tree/tab/app structs, CFG and the palette macros, and the 50-odd formerly static functions the cut made cross-unit, declared where they are defined. They keep their names; only the five units see the header. One test moved with the code: test_portable_layouts' staleness check greps the source for the layouts section, which now lives in app_session.c. Mechanical, so blame for anything in the four new files continues in app.c before this commit.
* 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: aim the top handle at the middle, and stop ruling through the rowuser2026-08-18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two faults that fed each other, and only the second one made the first visible. The handle went to the **widest** free run of the top row. A centred title leaves two runs of nearly the same width, so a one-cell difference nobody can see threw the handle out toward a corner: on a 116-column pane whose row middle is 60, it sat at 85. Placement should be stated in terms of where the thing wants to be, not of the leftovers it is allowed to occupy -- so it now aims at the middle of the row and slides into the nearest run that is free. Nothing in the way: centred, like the other three sides. A title holding the middle: hard against the title, where the eye already is. That same pane now puts it at 64. The armed edge also ruled the heavy line straight across the whole border, over the pane's name, its buttons, its scroll indicator and a dead pane's epitaph. The row went unreadable exactly while the pointer was on it -- and the title the handle had just been placed beside was wiped, which is what turned a merely-off position into an apparently random one. Each cell of the edge is now painted only if the hit list says the edge still owns it: the same question the handle asks, and the one the click will ask. The epitaph had to start claiming its cells to take part. It answers `panestatus:` and does nothing when clicked, because an epitaph is not a button; what it buys is that ` exited: status 3 ` survives a hover over the border it lives on, next to `[re-run]` and `[close]`, which already survived because they were buttons. Verified: `make retest`, **1877 checks measured**, all green. Four new: the handle is centred on the row when the row is empty, sits against the title when the title holds the middle, and the armed top edge leaves both the name and the three buttons legible. Live on a 116x36 pane, hovering the far left of the top row -- deliberately the rim, so this is the first stage doing all of it: ╭━━━━━━━━━━━━━━━━━━━ sl0ppty ███▲███━━━━━━━━━━━━━━━━━▬ □ ✕ ╮ and on a dead pane, hovering the bottom handle: ╰ exited: status 3 ━━━━━━━━━━███▼███━━━━━━━━[re-run]━[close]━╯
* sl0ppty: the split arrow belongs on the buttonuser2026-08-18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The arrow rode the middle of the dashed boundary line, which put the two halves of one message in two places: the handle said *press here* and a glyph out in the middle of the pane said *this is what happens*. It also meant the direction was unreadable until you were already on the target, since the dashed line is stage two of the hover. So the arrow moves into the handle and the boundary line goes back to being a plain boundary -- which is the honest shape for it anyway: it is the *edge* of the pane you are about to make, and an edge has no middle worth marking. The handle is now one object that explains itself, and because the handle is drawn from the first stage, brushing any part of an edge tells you where its button is and which way that button splits, without previewing a boundary nobody asked about. Knocked out with ATTR_INVERSE rather than drawn in a second colour: the bar is solid `guide`, so a `guide` glyph on top of it would have been invisible, and inverting gets contrast that follows whatever colour the theme sets for no new knob. Along the handle the arrow sits in the middle, where the pointer is aiming. Across it -- a distinction only the upright pair has, being the two that grow a second cell -- it sits on the edge's own cell rather than the one over the content, because it marks a boundary and the boundary lands on the frame. Verified: `make retest`, **1873 checks measured** (`test_split_ux.py` 100 of them), all green. The test that pinned the arrow to the dashed line is now the test that pins it to the handle, on all four sides, plus one that it is legible from the first stage with no boundary drawn. Checked live on a 116x36 pane: `◄█` at column 2 and `█►` at column 117 on the middle row of their bars, `━━━███▼███━━━` on the bottom border's single row, and -- the case that shows why this is worth it -- hovering the *far left* of the top row draws `███▲███` at column 85, where the handle actually is, having dodged a centred title. A note on the two commits before this one: their messages quote 1801 and 1807 checks, which were carried over from earlier in the session rather than measured. The figure was 1865 at that point. The code and the tests were right; the numbers in those two messages were not.
* sl0ppty: a horizontal handle gets heavier, not talleruser2026-08-18
| | | | | | | | | | | | | | | | | | | | | | | | | The armed handle grew by one cell into the pane on all four sides, which is one change on two axes and therefore two different results. A cell is about twice as tall as it is wide -- the fact `gap_aspect` exists for, and the one the split picker measures its longer side by -- so the extra column on a left or right edge read as a thicker line, and the extra row on a top or bottom edge read as a wall. It also spent a row of somebody's output to say something the glyph was already saying: `█` is the heaviest thing one cell can draw, and against `━` it needs no help. So only the upright sides widen now. A horizontal handle stays on its own row. That also removes its claim on a content cell, which is why the inner-strip hit is registered on those two sides only -- the paint and the hit still describe the same rect, which is the whole reason the guide registers what it thickens. Verified: `make retest`, 1807 checks, all green -- six new, pinning the asymmetry in both directions: the top and bottom handles occupy exactly the row their border is on, the left and right ones exactly two columns, and the inner column of each upright handle hit-tests as that border rather than as the pane. Measured on a live session rather than argued: hovering the bottom border of a 116x36 pane puts seven block cells on row 37 and no others, inside the `━` rule; hovering the left border puts them on rows 16-22 across columns 2 and 3.
* sl0ppty: the middle of a border is the button, not all of ituser2026-08-18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Clicking a pane's border to split toward it is the right gesture and was the wrong target. A border is the longest thing on the screen and it sits between two things you did mean to click -- a title, a gap, the pane beyond it -- so reaching a cell too far changed the layout, and the top row was worse than the sides: clicking a pane's header to focus it split it upward. So the target shrank. Each side carries a *handle* of three to seven cells in the middle, and only the handle splits; the rest of the edge is the *rim*, which arms the guide and does nothing when pressed. Three is the floor the pane buttons already settled on for the reason written there -- a one-cell target is a thing you miss with a mouse -- and seven is the ceiling, because a third of a tall pane's border is most of that border again and brings the accidents back with it. Hover now answers two questions in two stages, because they are two questions. Anywhere on an edge, the edge goes heavy and the handle thickens by a cell into the pane: that says where the button is, which is the thing you can no longer infer. On the handle, the dashed boundary and the arrow as well: that says what the click will do. The hint follows the same split, so sweeping an edge neither draws a boundary nor offers a split you cannot get from where the pointer is. Three things this had to get right. **The thickened handle is registered, not just painted.** The guide adds the hit for the inner strip in the frame that draws it, and reads the outer rect back from the hit list rather than recomputing it, so there is one opinion about where the handle is (D6). A handle you can see and cannot click would be worse than either mistake alone. **The top row cannot put its handle in the middle.** The buttons own its right and a centred title -- the default -- owns exactly the middle. The title winning is the existing rule, and it is what keeps a double-click rename from splitting twice on its way, so the handle is placed last, in the widest run of cells the row has left and nearest the middle among equals. A row with nothing left offers no upward split; its other three edges are unaffected. Each handle cell is then painted only if the hit list still agrees it belongs to the handle, so a block never lands on a pane's name. **The row keeps the drag and loses the split.** `title:<id>` takes no side now and `title:<id>:t` -- the handle -- carries one, which is why the press path reads the side out of the action instead of assuming it. Press-and-hold skips the dwell on a handle, where a press means something, rather than anywhere on an edge. Verified: `make retest`, 1801 checks, all green. Nine of them are new or rewritten: the two-stage guide, the caption belonging to the handle, the rim splitting nothing on any of the four sides, the top row's handle still splitting upward on a titled pane, and the rim of the top row not. Tests read the handle's cell out of the hit list (`Snapshot.handle`/`.rim`) rather than deriving the geometry, so a test cannot agree with a broken formula. Also checked by hand on a live session, which is where the placement rule earned itself: hovering the rim draws the heavy edge and the block and no dashed line, hovering the handle adds the line and the arrow, three clicks on pane headers split nothing, and on a pane titled `sl0ppty` the top handle lands to the right of the title and left of the buttons -- `drag to move · click to split up` at x=42, `drag to move` either side of it.
* sl0ppty: make tools, and a ruff version that cannot driftuser2026-08-17
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `ruff` is not a system package on most machines, and `pip install --user ruff` is refused outright on a distro python (PEP 668) -- so the last two commits shipped a formatter nobody could run without knowing to build a venv by hand. `make tools` builds it: $ make tools ruff 0.16.3 in .venv survives make clean; rm -rf .venv to start over clang-format stays a system package, because everywhere it is one. `make tools` says so if it is missing rather than pretending it handled it. **The venv is looked in before `$PATH`, and the order is the point.** `ruff.toml` now pins the version with ruff's own `required-version`, so a ruff of the wrong one refuses to run and names both versions. Preferring `$PATH` would then strand somebody who has 0.14 from their package manager and 0.16.3 sitting in the `.venv` the repo just built for them -- refused by the tool, with the fix already on disk and not being looked at. Tested with exactly that: 0.14.0 on `$PATH`, `make fmt-check` still clean. The pin exists because a formatter whose output moves between releases turns "formatted" into "formatted by whoever committed last", and the diff lands on the next person to touch the file. Refusing loudly is better than reformatting quietly. Bumping it is one line in `ruff.toml`, one in the Makefile, and a reformat commit, which is the honest cost of a new version. The hook resolves ruff the same way, off `git rev-parse --show-toplevel` so it works from a subdirectory, and its "not installed" message now says `make tools` instead of naming three package managers and hoping. Verified from the state a fresh clone is in: with no ruff anywhere, `make fmt` says `ruff not found. run: make tools` and stops; after `make tools` (2.6s) both `fmt` and `fmt-check` work with nothing on `$PATH`; the hook formats a staged Python file using the venv; the venv survives `make clean`; and `.venv/` is ignored. `make fmt` over the whole tree after all this changed nothing, which is the useful part: the pinned version agrees with what is already committed. One near-miss worth recording: the `.gitignore` line for `.venv/` had to be written twice. A `git reset --hard` undoing a throwaway probe commit took the working tree with it, including that edit -- which is what `--hard` does, and why the state was checked afterwards rather than assumed.