aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAge
...
* macos: sign and notarize on a mac that is not this onePeter Geissler2026-08-26
| | | | | | | | | | | | | | | | | | | | | | An unsigned mac binary is "damaged and can't be opened", so a release has to be signed with a Developer ID and notarized by Apple, and that needs a mac holding a private key. That mac is not the machine you develop on -- under `make all` it never is -- so `make macos-dist` builds nothing here: it syncs the tree to the builder over ssh, has it build, sign, notarize and staple, verifies the result there and fetches the zip back. The near end needs only ssh, rsync and a hasher, so it works from Linux. contrib/macos-builder-setup turns a bare mac into that builder and is re-runnable, with --check to say what is missing. The two things that make an unattended builder different from a laptop are handled rather than worked around: a keychain of its own, unlocked from a 0600 password file because a headless mac has no unlocked login keychain and codesign dies with "User interaction is not allowed"; and exactly one copy of the identity in the search list, because two make codesign ambiguous. No identity is in the repo. Who signs, which team, which mac -- that is Makefile.macos, which stays out of git along with dist/, and macos-dist tells you so when it is not there.
* homebrew: a formula, and a script that keeps it honestPeter Geissler2026-08-26
| | | | | | | | | | | | | | | | | | | | `brew install slosh` is the shortest install there is, and the formula is small because `make install` already does the work: zig as the only build dependency, both zig caches redirected into the build directory so the sandbox does not deny them and the user's real cache is not polluted, and the commit id handed to the Makefile because a snapshot tarball has no .git for `git describe` to read. The url/version/sha256 triple is the part that rots. contrib/brew-release resolves a ref to a full commit id, fetches the cgit snapshot, hashes what it actually got, and rewrites those three stanzas -- it does not commit or push, so the diff is read by a human first. Untagged pins carry the commit count so `brew upgrade` can tell newer from older; alphabetical comparison of two short ids cannot. The formula lives here as the source of truth and is copied into the tap; contrib/homebrew/README.md says why it is a formula and not a cask, and what still stands between this and homebrew-core.
* make install: put the binary and the examples where they belongPeter Geissler2026-08-26
| | | | | | | | | | | There was no way to install slosh short of copying build/slosh by hand, which packagers cannot do and users should not have to. PREFIX and DESTDIR are honoured because that is the contract every package manager already expects; the Homebrew formula calls exactly this. Only the binary is needed at runtime. The config, layout, themes, chrome and shaders are examples you copy into ~/.config/slosh, so they go to share/slosh and nothing reads them from there.
* install.sh: `make` prints the help text, it does not buildPeter Geissler2026-08-26
| | | | | | | The default goal is `help`, so the installer's bare `make` printed a cheerful list of targets, exited 0, and installed whatever build/slosh happened to be lying around -- or failed at the copy on a clean tree. Name the target.
* cleanup: remove pointless md artifactsuser2026-08-26
|
* 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.
* tests: the greeting is not what the reattach test is aboutPeter Geissler2026-08-26
| | | | | | | | | | | | | | | | | | | | | | | "reattaching repaints the whole screen" greps the client's byte stream for the text a pane was holding, and failed about half the time. The splash assembles from particles scattered over the entire screen, so for the first few hundred milliseconds of an attach any cell can be holding a fragment of the logo. The full repaint that a reattach triggers goes out during exactly that window, so it can carry │marker-alp▄a with a particle where the "h" should be. What restores that cell when the greeting ends is a one-cell diff, cursor-addressed, so the string is never contiguous in the stream again and the substring check fails. Nothing about slosh is wrong here -- the model has the text throughout, and `snapshot text` shows it -- so the fix is to stop asking this file to assert about the bytes of an animation it is not testing. The session under test gets a config with splash_ms 0. The greeting has test_splash. 5/5 where it was roughly half.
* a client that stops reading must not stop the sessionPeter Geissler2026-08-26
| | | | | | | | | | | | | | | | | | | | | | | | | | | Everything the server said went out through msg_send(), which loops on sendmsg() until the kernel has taken all of it. That is a blocking write to whoever is attached. A display whose reader has paused -- C-s at the terminal, a stopped ssh, a client being swapped -- fills its socket buffer, and then the *server* sits inside that loop: every other pane, the config watcher, the control socket and every other connection stop with it. A session that is supposed to outlive its client hangs on one. macOS is where this showed up, because its socket buffers are a fraction of Linux's 64K and it wedges within a screenful rather than after a few hundred. `make test-live` could not get past the forty-connection probe in test_session.py: the server was blocked writing to a pty nobody was draining, so `slosh cmd panes` timed out and took the suite with it. So connections are non-blocking and output is queued, flushed when poll() says the socket will take more. The queue is capped: past 4MB we are not buffering a slow reader any more, we are storing a screen nobody is looking at, and the connection is dropped -- a display that comes back reattaches and gets a full repaint, which is what happens after any detach anyway. Measured, with a pane running `yes` and a display that never reads: before, the first control command hung forever; after, 40 of 40 are answered, the session stays listed, and the server's RSS is flat.
* macos: the kernel still knows, it just spells /proc differentlyuser2026-08-26
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Portability fixes contributed by our macOS developer, applied with one hardening tweak (below). Darwin has no /proc, so everything that read one gets the native answer: live_cwd via proc_pidinfo(PROC_PIDVNODEPATHINFO), the foreground command via sysctl(KERN_PROCARGS2) parsed back into the NUL-separated shape /proc/<pid>/cmdline gives, and SLOSH_BIN via _NSGetExecutablePath plus realpath. The pty gets its winsize set on the slave after opening it, because Darwin attaches no line discipline to the master until then and the earlier TIOCSWINSZ was silently dropped -- a pane born 0x0. The shader repl binds tab in libedit's own words, since macOS ships Python's readline as a libedit wrapper that accepts GNU syntax in silence. Two finds are cross-platform bugs: a toast wider than the screen minus the gap computed a negative x that uint16_t made ~65533, drawing the longest messages -- the ones most worth reading -- off screen; now the middle is elided (a path's tail is the half you needed) and x clamps. And a pane label was 128 bytes, which a Homebrew interpreter's full path exceeds before its arguments even start; dump-layout wrote the truncation down and a restore ran it, so it is 1024 now. Saving a layout learned cwd_as_named: the kernel answers with the physical path, and on macOS /tmp is a symlink to /private/tmp, so a layout that said cwd="/tmp" stopped round-tripping through a save. If realpath(named) is where the pane still is, the named spelling wins. Tests follow: realpath'd tmpdirs, the scroll test reads the shell's first prompt instead of spelling out bash 5.3, and wrapped pane text is joined before matching a long path. One tweak on top of the contribution: the elision needs five columns for a head, the dots and a tail -- below that `avail - 3` goes negative, and negative as size_t is a pointer into the weeds; a terminal that narrow gets a plain clip.
* 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.
* placeholder webbieuser2026-08-25
|
* www: a shimmer nobody sees is not a shimmeruser2026-08-24
| | | | | | | | Measured in a headless browser: the band spent most of its sweep off screen and its eased segment rushed the middle, so the pane lit up for half a second in eight at ten percent white. Now the keyframes cover exactly the stretch where the band crosses the pane, the sweep is linear and 1.6s long, and the sheen is half again as bright.
* www: the shaders paragraph earns its keep, and the pane rings its belluser2026-08-24
| | | | | | | | | | | | The copy no longer reads as eye candy: shaders are how a pane says what is true of it. An unfocused pane dims, a dead shell greys out, scrollback tints, a bell shimmers until it is seen. Scanlines and CRT phosphor stay, admitted as such. And every eight seconds the pane demonstrates the bell: a soft sheen crosses the body, gone within a second, painted as a clipped background sweep so the frame title stays put. Off under prefers-reduced-motion, like everything else here.
* www: the placeholder drops its em dashesuser2026-08-24
| | | | | Full stops and a colon where the dashes were. The title is slosh: coming soon.
* www: the placeholder shows off shaders, and says who made ituser2026-08-24
| | | | | | | | | | Shaders are the thing no other multiplexer has, so the coming-soon page should not keep quiet about them: a line names them, and the pane itself wears two -- zebra band=1 and vignette, redrawn in CSS -- so the claim is demonstrated on the spot. Below the prompt: slosh is a sl0p.foo product, with the Discord invite.
* www: the front door says coming soonuser2026-08-24
| | | | | | | | | | | | The site is not finished -- the demo video is still being cut and the public repo URL does not exist yet -- so what slosh.foo serves is one self-contained page in the session-drawn look: tab strip, a focused pane that runs $ slosh and prints the announcement as its output, status line, the sl0p palette. No scripts, nothing fetched. web/build-site assembles just that page by default now; FULL=1 builds the real site (front page + docs + demo) while it is being finished. Drop the gate when the real thing ships.
* slosh: the config watcher speaks kqueue on macOSuser2026-08-24
| | | | | | | | | | | | There is no inotify to port to, so the watcher grows a second native backend: kqueue's EVFILT_VNODE, feeding the same pollable fd that server_run() already watches. Vnode events are per-inode, so each config file is watched twice -- via its directory (catching the temp-file-then-rename most editors do, and files appearing for the first time) and via the file itself (catching in-place rewrites). Either event triggers the same debounced re-read as on Linux, which re-registers the watches and so survives the rename swapping the inode out from under us.
* make vendor: no xcframework, a CLT-only xcodebuild is a stubuser2026-08-24
| | | | | | | | In lib-vt mode upstream's build.zig auto-enables an xcframework whenever xcodebuild is on PATH. On a Mac with only the Command Line Tools that xcodebuild fails, turning a working build into a broken one -- and we only ever consume libghostty-vt.a anyway. Pass -Demit-xcframework=false.
* vendor: an unanchored build/ ignore was eating libghostty-vt's build toolinguser2026-08-24
| | | | | | | | The build/ pattern matched every directory of that name, not just our build output at the root -- so vendor/libghostty-vt/src/build/ (the library's own build.zig helpers) never made it into git, and make vendor broke on fresh clones. Anchor the ignore to /build/ and restore the 284 files it had silently dropped.
* slosh: a pane arriving in a zoomed tab ends the zoomuser2026-08-22
| | | | | | | | | | A split, the config editor or a new float while zoomed used to land the new pane behind the pane filling the tab -- which looks exactly like the verb having failed. Whoever asked for a new pane asked to see it, so the zoom yields, in place_beside where every such arrival funnels through; the move-pane unzoom was this rule as a special case and folds in. Minimising the zoomed pane ends the zoom too, or the layout would solo a pane the strip now holds.
* DEMO-VIDEO.md: a scene list for the social cutuser2026-08-22
|
* 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: refuse to attach to the session this pane is insideuser2026-08-22
| | | | | | | | | A new client displaces the current display, so `slosh` typed into a pane of "main" displaced the very client showing that pane -- you watched your own session detach, from inside it. SLOSH_SESSION names the pane's own session; that one name is refused with the way out spelled in the message. Any other name is deliberate nesting, and cmd/ls are untouched: they are how a program in a pane is supposed to reach its own session.
* slosh: paint the padding ring, so a float cannot be seen throughuser2026-08-22
| | | | | | | | A tiled pane sits on a cleared screen, so unpainted padding looked fine by accident; a float sits on whatever was composited under it, and every unpainted cell let that show through. draw_frame now fills the ring between border and contents with exactly what screen_clear leaves, and a suspended pane's never-composed content rect gets the same treatment.
* 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: bell_mark defaults to "[!]"user2026-08-21
| | | | | | Plain ASCII that says what it means, over a dot that had to be noticed. The mark was always a short string rather than a glyph (the emoji-width argument in config.kdl stands); the default just stops being shy.
* slosh: the scrolled edge fade earns its nameuser2026-08-20
| | | | | | | | Four rows deep at 60 a row instead of three at 45: the edge row is nearly gone (#0e0e0e from white) and the melt reads as "there is more this way" rather than as a rendering artefact. The quarter-viewport cap, the (above > 0) gate and everything else about the derivation stand; a config that writes its own scrolled chain is untouched, as ever.
* 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.
* fuzz: libFuzzer harness for all six parser surfacesuser2026-08-19
| | | | | | | | | | | | | Targets: kdl, jsonval, expr, proto, osc5577, input — each compiled standalone against its own .c with ASan + UBSan, no vendor deps. Includes seed corpora from shipped configs/themes/chrome, real control API lines, terminal escape sequences, and regression inputs from the bugs already fixed. standalone_main.c replays files without the fuzzer runtime for debugging and CI. Top-level: make fuzz (run all, TIME=15 default), make fuzz-corpus (replay seeds only, fast CI gate).
* parsers: fix seven bugs found by fuzzinguser2026-08-19
| | | | | | | | | | | | kdl: heap-buffer-overflow on trailing backslash in a string (a"\) jsonval: heap-buffer-overflow on truncated \uXXXX escape ("\u) jsonval: UB float-to-long cast on out-of-range numbers (2e22) expr: UB signed overflow in arithmetic ops — wrap as uint32 expr: INT32_MIN / -1 SIGFPE guard in OP_DIV/OP_MOD expr: stack overflow on deep nesting — add MAX_DEPTH 256 expr: 64-bit + saturation in OP_DIST to prevent squared overflow input: NULL memcpy in input_timeout's re-parse path input: signed overflow in CSI and SGR mouse parameter accumulation
* webdemo: the theme symlink is a dependency of the imageuser2026-08-19
| | | | | | | | | | | $(shell find overlay -type f) skips symlinks, and the one symlink in overlay/ is the sl0p theme pointing into contrib/themes -- the file the README promises cannot drift was the one file whose edit did not rebuild the rootfs. find -L includes it, and make stats prereqs through symlinks, so the target's mtime is the one compared. Found while tracing what a src/app.c edit does under make www: that path was already correct (depfile -> object -> guest -> image -> site).
* slosh: the website lives in web/ -- one directory, one build, make wwwuser2026-08-19
| | | | | | | | | | | | | | | | | | | | | | Consolidate everything the website is made of: - web_new/ -> web/: the front page, install.sh, and build-site - contrib/webdemo/ -> web/demo/: the in-browser machine, committed for the first time (it had never been checked in) - make www at the top level assembles the whole site into build/www (NO_DEMO=1 skips the guest cross-compile) - web_plan.md deleted: the plan is fulfilled and web/README.md is now the record Path fallout: the overlay theme symlink gains one ../ (web/demo is a level shallower relative to contrib/themes), PYSRC in the Makefile follows the demo scripts, contrib/README.md drops its webdemo section. ROOT := ../.. in the demo Makefile survives unchanged -- web/demo sits at the same depth contrib/webdemo did. Verified: make www builds front page + docs + demo end to end, web/demo make check boots the image to slosh, tests/test_docs.py and make fmt-check green.
* slosh: 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: the logo flies into place -- a particle engine, statelesslyuser2026-08-19
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Every non-space glyph of the logo is a particle that flies to its cell over the first two fifths of splash_ms, then *is* the logo for the rest. There is deliberately no particle state anywhere: a position is a pure function of (target, elapsed, seed), recomputed every frame like the layout and the pane states and for the same reason -- physics that is stored can drift, physics that is derived cannot. The seed is the splash timestamp, so every attach flies differently and one splash is coherent frame to frame; a per-particle hash staggers departures, and the delay rescaling lands every straggler exactly at the end, because an almost-assembled logo reads as a bug. Five motions, picked by the clock and pinned by {"cmd":"splash", "motion":N}: rain (fall in, column by column), scatter (converge from everywhere), slide (rows enter from alternating sides), vortex (spiral in around the centre, orbit shrinking and unwinding), shuffle (the glyphs trade places, then sort themselves). The colour chains keep playing over the box throughout, so particles catch their hue as they cross into it. expr_sin() is the expression language's own sine exported for C-side animation: a motion sketched in the repl and one compiled into the binary cannot disagree about what sin(90) is. The tests learned two things the flights taught: the logo's opening run appears three times per line and assembly is staggered, so a middle occurrence can land first -- "assembled" is the row whose first block glyph is where the match begins -- and windows need margins that survive a parallel suite on a busy box.
* slosh: the splash colours glyphs, never backgroundsuser2026-08-19
| | | | | | | | | The logo is half-block art: its visible pixels *are* the foreground and the notches are the terminal's own background. A pass that painted backgrounds therefore turned the art into a slab with a logo-shaped hole -- so every splash pass is channel fg now, the effect lands on exactly the art's pixels, and the backdrop stays dark for free (a blank cell has no glyph for a colour to land on).
* slosh: the splash learns colouruser2026-08-19
| | | | | | | | | | | | | | | | | | | | | | | | One tint is one colour, and every effect wore the theme accent -- six effects that were all the same dull blue. An effect is a *chain* now: phase-shifted tints in loud literal colours, mixing per cell, which is how a fixed-colour pass machinery does rainbows. Passes may light the backdrop's background too, so the box is a scene rather than tinted text, and the logo is drawn white -- the canvas that takes every hue at full saturation, where tinting blue towards pink lands on mud. The six: spectrum (rainbow bands), plasma (two interference patterns in clashing hues), fire (heat up the glyphs, embers in the box), rings (rainbow ripples), glitch (a reveal under two colours of static), aurora (green and violet curtains over cyan). The splash is deliberately the one place that does not defer to the theme: it is branding wearing a demo, not chrome. {"cmd":"splash","fx":N} picks an effect by index -- a test needs a deterministic one, and "show me the aurora again" deserved an answer. The new test counts dominant-channel hue families on the logo rows, because two hundred shades of the same blue is exactly the failure it guards against.
* slosh: greet an attach with the logo, wearing a random shaderuser2026-08-19
| | | | | | | | | | | | | | | | | | | | | | | | logo.txt is the source of truth; the Makefile bakes it into build/logo.h the way version.h is baked. When a client attaches (MSG_HELLO turning a connection into the display), the logo appears centered over everything for splash_ms (900, 0 for never), on a cleared backdrop, in the accent -- and one of six effects runs over its rect through the ordinary shader pass: a diagonal sheen, plasma, rings, sparkle, a left-to-right reveal, a breathe. Which one is picked by the splash's own timestamp, so every attach looks a little different without anybody keeping a counter. Every effect reads the clock, so the frame clock runs only while the splash is up (note_animation, same as any animated chain), and `since` in a splash context is milliseconds since it appeared, so one-shot effects sweep exactly once. Any key or click ends it and still lands where it was aimed: a greeting is not a modal. A screen the box does not fit on gets no greeting rather than a cropped one. Headless and scripted sessions never attach, so the whole fast suite never sees it; {"cmd":"splash"} triggers it on demand, which is what makes it testable from a scripted session -- and lets you replay the greeting whenever you like.
* 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: a website that is a page, not a mock sessionuser2026-08-19
| | | | | | | | | web_new/ is the front page slosh.foo serves: nav, install, what is different, docs list. The first draft drew itself as a fake slosh session -- tab-strip nav, box-art panes, a status-line footer -- and that chrome is gone: the page describes the thing instead of impersonating it. web_plan.md is the working notes. build-site assembles it with the rendered docs and the webdemo.
* slosh: the rename is formatting-shaped, walk blame past ituser2026-08-18
|
* 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.