| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
The h-centered check raced the assembly it was measuring. landed_row()
answered as soon as one occurrence of the opening run had a block glyph
at its start -- but the logo's first line contains that run three times,
assembly is staggered, and under a parallel suite's load the middle
section can finish whole checks before the leftmost. The test then
measured a middle occurrence's x (24) and called the logo off-centre.
The row is 'landed' when all three occurrences are present -- its final
form -- and the first block glyph is still required to sit where the
match begins, so a particle passing to the left keeps us waiting rather
than shifting the answer.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Text taller than the viewport was not selectable in one gesture: the
drag stopped dead at the pane's border. Two faults, one visible and one
waiting. The visible one: DRAG_SELECT motion subtracted the content
origin in uint16_t, so a pointer above or left of the pane underflowed,
the grid lookup failed, and the selection froze. The waiting one: the
anchor was a pair of *viewport* coordinates re-resolved on every extend,
so the moment anything scrolled mid-drag the anchor would have drifted
with the viewport, re-anchoring to whatever slid under the press.
The anchor is now a tracked grid ref -- the thing lib-vt documents as
being for 'long-lived anchors such as selections' -- so it follows its
cell across scrolling, pruning and reflow, and only the near end of the
selection moves. If a program flooding output prunes the anchored row
out of scrollback entirely, the selection keeps what it had; there is no
cell left to anchor a new answer to.
Motion clamps into the content rect (a pointer past an edge is pointing
at the nearest cell), and holding past the top or bottom edge scrolls on
a clock rather than on motion, because a resting pointer sends no events
and 'keep going' is exactly what resting it there means. app_tick() does
the step, app_next_deadline_ms() wakes the poll loop for it -- the same
mechanism toasts and hover guides already use -- and both front ends
call the tick, so the behaviour exists under the server and the headless
harness alike. Rows per step is how far past the edge the pointer sits:
one row past creeps, further hurries, so the hand already dragging is
also the throttle. The metronome is select_scroll_ms, a knob so the test
can configure the clock down instead of sleeping.
The test drags to one row above the pane and simply holds: the viewport
walks to the top of scrollback (clamping makes the endpoint exact), the
copy starts at a line that was never on screen with the press, and still
ends at the pressed row -- that last check is the tracked anchor's.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The post-notarization Gatekeeper check ran spctl exactly once, immediately.
A bare CLI binary cannot be stapled (stapler only takes .pkg/.dmg/.app), so
there is nothing on disk to carry the ticket and Gatekeeper must fetch it
online from Apple's CloudKit -- which lags the notary's own 'Accepted' by
seconds to a couple of minutes. The single check raced that propagation and
failed the whole release with 'Unnotarized Developer ID' for a build Apple
had already accepted (status: Accepted), taking the good linux tarballs and
the SHA256SUMS down with it.
Poll spctl instead: pass the instant the ticket appears, and if it still has
not after ~2 min, report it as pending and let the release stand -- the notary
verdict is authoritative and codesign already vouched for the signature. Any
other rejection still fails hard, so a real signing problem is still caught.
The accepted vs pending states are told apart by 'Notarized' vs 'Unnotarized'
so there is no false positive.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Streamline the release into a single target that stages all three binaries
in dist/ with a SHA256SUMS:
linux x86_64 } cross-built here with 'zig cc -target ... -s', static musl
linux aarch64 } (runs on any distro), packaged as a .tar.gz with the binary
+ README + LICENSE + THIRD-PARTY-LICENSES
macos arm64 handed to 'macos-dist', which signs/notarizes on the builder
zig is already the compiler, so the two linux cross-builds need no extra
toolchain -- just the vendored libghostty-vt rebuilt per -Dtarget. Verified
both: static, stripped (~2MB), and both run (aarch64 under qemu-user).
Factor the version ladder out of the Makefile into contrib/version, and call
it from both Makefile and Makefile.macos, so the linux tarballs, the signed
zip and the binary's own --version are all named from the same tag instead
of Makefile.macos using its own 'git describe --always' scheme (which would
have named the mac artifact after a bare commit hash while linux used the
tag).
make release-linux # just the two linux tarballs
make release REF=<tag> # all three + SHA256SUMS
|
| |
|
|
|
|
|
|
|
|
| |
slosh had no license declared -- effectively all-rights-reserved, which
blocks redistributing binaries. License it MIT (c) 2026 Peter Geissler.
The binary statically links libghostty-vt (MIT) and compiles in stb_image
(MIT/public-domain dual), both of which require their copyright notice to
travel with any redistribution. THIRD-PARTY-LICENSES carries both, and will
be bundled into the release archives alongside LICENSE and README.md.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
VERSION was a constant in the Makefile that had to be bumped by hand and
could silently disagree with the git tag a release was cut from. Derive it
instead, with a committed ./VERSION as the floor and no-git fallback:
on a tag v0.2.0 -> 0.2.0
past a tag v0.2.0-5-gabc123 -> 0.2.0-5-gabc123 (sorts after 0.2.0)
before any tag 0.1.0-197-g2d1815 -> ./VERSION + commit count + short id
no .git at all 0.1.0 -> ./VERSION verbatim (cgit snapshot ->
Homebrew source build)
A dirty tree appends -dirty. The commit count keeps 'brew upgrade' ordering
monotonic before the first tag exists.
version.h now bakes just VERSION (dropping the redundant +GITDESC, which the
derived string already carries). contrib/brew-release reads the base from
./VERSION rather than grepping the Makefile's VERSION line, which is now a
$(shell ...) expression it could not parse.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The working-tree sync excluded two of the three gitignored
vendor/libghostty-vt zig caches but not zig-pkg/ (354M), so every
run silently re-pushed it. With rsync -a and no progress output that
looked like a hang on the rsync step.
Add the missing zig-pkg/ exclude (keeping the list in step with
.gitignore), plus web/ (~430M landing page/demo) and tests/fuzz/
(~120M corpus), neither of which is an input to `make all` -- all
the builder runs. Cuts the sync from ~1.2G/48k files to 350M/9.6k.
Only affects the default working-tree mode; REF=<tag> builds check
out on the builder via git and were unaffected.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Nothing about updating the formula needs a mac or needs brew -- it
resolves a ref, fetches a tarball, hashes it, rewrites three stanzas.
But it hashed with `shasum`, which is perl's and only usually present
on Linux, where `sha256sum` is coreutils and always is. Take whichever
exists, as macos-remote-build already did.
The version count no longer falls back to 0 when the commit is not in
this clone. `0.1.0-0-<short>` is older than every version published, so
a release cut from a clone that had not fetched yet would have looked
like a downgrade and never installed; say so and stop instead.
Also written down: why the formula builds from source rather than
shipping the notarized zip the mac builder makes -- brew's curl sets no
quarantine attribute, so notarization gains a formula nothing, and the
build is a minute with zig as its only dependency. If that ever changes
the answer is bottles, which are additive and cost a per-macOS-version
matrix, not a cask.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`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.
|
| |
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
| |
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.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
"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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| | |
|
| |
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
| |
Full stops and a colon where the dashes were. The title is
slosh: coming soon.
|
| |
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
| |
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.
|
| | |
|
| |
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
| |
$(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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|