| Commit message (Collapse) | Author | Age |
| ... | |
| |
|
|
|
|
|
|
|
| |
A gradient of the pink's neighbours -- orchid one side, a lighter pink
the other -- clipped to the ink and slid along it, one tile every
fourteen seconds: the logo's own little colour pass, in the spirit of
the shaders it advertises. Horizontal on purpose, because only a 90deg
gradient meets its own edge seamlessly when the loop wraps; animated
only while shown, and not at all under prefers-reduced-motion.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
A fixed top worked at one viewport height and overlapped the pinned
terminal at others: the terminal's top edge is (viewport - the stage's
lift padding) / 2 - half the player, which no constant tracks. Measure
it instead, centre the logo in what remains, and hide it outright when
a short viewport leaves no air -- hiding beats overlapping.
Re-placed on resize and by a ResizeObserver on the player mounts, not
on the pin: the players are zero-height until their casts arrive, and
the pin is a 100vh box that never resizes, so watching it would freeze
the placement at the pre-load height.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
xterm turns a mouse event into a cell with getBoundingClientRect() and
its own unscaled cell metrics, so the scale() that stretched the fixed
grid into the stage put every click on the wrong cell, off by the
scale factor. Desktop kept the factor near 1 -- cols and rows are
derived from the same room the scale fills -- so it passed for
working; a phone is forced onto the 80-column floor, the factor lands
near a half, and a tap selects a pane the finger never touched.
Refit the font size instead: walk it until the grid fills the room
(iterated, cell metrics are not linear in font size), centre the
leftover with translate, which moves rect and clicks together. The
grid stays what the guest was told at boot; only the glyphs move. A
webfont resolving late changes the metrics under the fit, so refit
again when document.fonts settles.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The overlay grows into a first session worth having:
- The config is a playground now -- every theme the repo ships rides
along as an include one word away, and the settings worth flipping
(compact, shaders, keys, dim_unfocused) sit commented in the file,
each one uncomment-and-save from happening on screen.
- C-a e opens vim, not busybox vi: editor "vim" in the config, since
the image always carried the real vim and its kdl syntax without
ever launching them. shortmess in the vimrc keeps the file-info
message from wedging a narrow pane into a hit-enter prompt.
- The config starts open, highlighted, in a pane of its own: the
discovery C-a e would make, made before anyone has to make it.
- A welcome float greets over everything, the slosh logo on top,
centred by the layout engine at whatever size the browser gave the
terminal (the previous commit is its enabling act). It teaches its
own dismissal -- C-a x, or the X it wears -- and it never exits on
its own, so keep_dead has no corpse to keep.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
C-a F drops a throwaway shell over a working layout; a monitor runs in
it, moves with the keys that move boundaries, grows about its centre.
Then C-a ?, the cheatsheet built from the bindings actually in force,
and exit closes the float with the layout exactly as it was.
The monitor is an htop impression with authored numbers, not htop: a
recording that renders differently every run is a bug here, byte-for-
byte is the contract. Phosphor, because every cast in the tour records
under its own theme so the next step reads as a scene change.
|
| |
|
|
|
|
|
|
|
|
|
| |
Block glyphs' ink can round a hair narrower than the monospace advance,
so the ascii logos showed hairline gaps between characters that should
read as solid ink; -0.05em of letter-spacing tucks each glyph into the
last, on the floating logo and the hero's alike.
The floating logo also stops dressing as a link on hover: the global
a:hover underline through seven rows of block glyphs was a mess, and
the colour flash no better. It still is one -- the pointer says so.
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
An ed25519 key made for exactly this -- slosh package signing
<slosh@slosh.foo>, 0F67C909938CFFA8A76B59FD8D69B55A0E9056B9 -- signs every
package and both indexes. The public half travels two ways that would both
have to lie: contrib/arch/slosh.asc in git, and /arch/slosh.asc on the
website; the READMEs walk through pacman-key --add and --lsign-key, and the
[slosh] stanza loses its SigLevel override because pacman's default demands
signatures and now gets them.
contrib/arch-repo pins the fingerprint, refuses to run without the secret
key (an unsigned update would make every enrolled pacman refuse the repo),
and now regenerates the index from the directory every time: repo-add skips
an entry whose version it already has, so after a same-version rebuild an
incremental add left the db describing the previous bytes -- a checksum
mismatch for every client. An index rebuilt from disk cannot disagree with
disk. Verified end to end with a scratch pacman keyring: enrolled key
syncs and downloads under Required DatabaseRequired, unenrolled key is
refused at the database.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
AUR signups are closed while the AUR deals with supply chain attacks, so
the package is published from our own roof -- which is the better offer
anyway: a pacman repository serves built packages where the AUR hands
every user a rebuild. contrib/aur/ becomes contrib/arch/ (the .SRCINFO
goes; it is AUR metadata, regenerated in a minute when signups reopen),
and contrib/arch-repo builds the package and maintains the slosh.db /
slosh.files indexes in dist/arch/<carch>/, which rsyncs to
/opt/slosh/arch/ on the box. The README and the front page carry the
[slosh] stanza; SigLevel is spelled out because the packages are not
signed yet and pacman defaults to demanding it.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
contrib/aur/ is the upstream copy of the AUR package; the AUR repo gets
these files verbatim on a release, and contrib/release now says so in its
checklist. The source is the cgit snapshot of the tag, which carries the
vendored libghostty-vt -- and every zig dependency in its build.zig.zon is
lazy and unused by the VT-only build (verified against a cold
ZIG_GLOBAL_CACHE_DIR), so the build fetches nothing and is clean in an
offline chroot. prepare() writes pkgver into VERSION because the v0.1.1
snapshot predates the release script that keeps the floor bumped; for
later tags the line writes what is already there.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
contrib/release X.Y.Z moves ./VERSION, commits and cuts the annotated tag
together, so the floor and the tag cannot disagree by construction. It
refuses a dirty tree, a detached HEAD, a malformed or non-increasing
version, and an existing tag; notes passed as the second argument land in
the tag message. It prints the push and `make release` commands instead of
running them, because publishing should be deliberate.
contrib/release --check verifies the invariant (VERSION equals the newest
tag) on its own: `make release` runs it before building anything, and the
new pre-push hook runs the same comparison against the commit a v* tag
points at, so a tag made by hand is caught before it escapes the machine.
|
| |
|
|
|
|
|
| |
The floor contrib/version falls back to without git -- a cgit snapshot, a
Homebrew source build -- still said 0.1.0 after v0.1.1 was tagged, so those
builds reported a version that was already released. The tag and the file
are two steps, and nothing made them one; contrib/release now does.
|
| |
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
| |
The install section says what Windows gets (one slosh.exe, ConPTY panes,
AF_UNIX sessions) and points at the docs; the front-page doc list gains the
windows page. install.sh states in its header that Windows is not its job,
and refuses under MSYS/MinGW/Cygwin with a pointer instead of building a
binary that is the wrong shape for the machine.
|
| |
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
watch_config() rebuilds the set of watched directories after every
reload, because a config that just changed may include different files
than it did a second ago. The inotify and kqueue branches unwatch and
re-add. The Windows branch did neither: unwatching was a comment saying
there was nothing to do, and sl_watch_add() appended to an array the
pump thread had already finished reading.
The pump captured `int n = w->ndirs` once, at the top, and armed one
ReadDirectoryChangesW per directory it found. Everything added after
that was stored and never watched. So an include added to a running
session loaded fine and then sat there: editing it reloaded nothing,
while editing the main config still worked, which is the shape of bug
that gets reported as "sometimes".
It was worse than deterministic. `w->ndirs` was written by the server
thread and read by the pump with nothing between them, so whether a
directory made it in depended on which won the start-up race --
_beginthreadex returns before the new thread runs, so the includes
present at startup usually did, and the ones added later never could.
And because nothing was ever closed, each reload re-added the same
directories: handles accumulated until the cap of 8, after which
sl_watch_add() failed quietly and no new directory could be watched at
all for the life of the session.
So the pump owns the handles and the server owns the wish. An
overlapped read can only be cancelled by the thread that issued it,
which settles where the boundary goes: the server writes a directory
list under a lock, bumps a generation and pokes an event; the pump
notices the generation moved, disarms what it had and arms what is
wanted now. Cancelling waits for the cancellation to land before the
slot is reused, because the OVERLAPPED and its buffer live in that
slot. Duplicate directories are dropped on the way in -- a config and
the theme beside it are usually in the same one -- and the compare is
case-insensitive, as the filesystem is.
sl_watch_close() also stopped every watcher in the process rather than
the one it was given, and freed nothing. It now finds its own, joins
the thread, and releases what that leaves behind.
Verified on the Windows box, with a fully backslashed SLOSH_CONFIG:
editing the config reloads; adding an include and then editing it
reloads; repointing the include at an unrelated directory reloads;
editing the file the include used to name does *not*, which is the
rebuild working rather than an append; and after twelve reloads in a
row it is still watching, where the old cap would have given up after
eight.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The functions the last two commits fixed are only interesting on the
platform the suite does not run on, which is how they came to be wrong
in the first place. So the Windows cases are asserted under #ifdef and
the POSIX ones assert the opposite: that a drive letter is an ordinary
name here, that a backslash is an ordinary character, that neither rule
leaked across.
That second half is the one worth having. Both fixes are guarded, and a
guard is exactly the kind of thing a later edit removes without meaning
to -- `\` splitting a POSIX path would invent a directory out of a file
called `a\b`, and nothing else in the suite would notice.
Pure functions, so it joins the C unit tests rather than driving a
session. Run on the Windows box as well, cross-compiled: 28 checks
there, 20 here, the difference being the cases each platform has.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two places split a path by hand, both with strrchr(path, '/'), both
falling back to "." when there was none: include_path() in config.c and
watch_config() in server.c. On Windows "none" is the ordinary case.
`SLOSH_CONFIG=C:\Users\you\slosh\config.kdl` is how a person writes it,
and a path that has been near a native API comes back all backslashes
whatever it started as.
So an include resolved against "." -- `cannot open ./themes/t.kdl` --
and, worse and quietly, the config watcher watched "." too. Not the
config's directory: the current directory of whatever started the
session. Hot reload did not fail, it watched somewhere else and waited.
Saving your config did nothing at all, with no error to go looking for.
The default path escaped this by accident. slosh builds it as
`C:\Users\you/.config/slosh/config.kdl` -- mixed, because a literal
joined to $HOME always is -- and that one embedded '/' is what
strrchr found. It worked for the path slosh writes and not for the one
a user does.
Both callers now use path_dir(), which learns to stop at a backslash
too, and server.c takes path_base() for the other half rather than
keeping its own copy of the arithmetic. Windows-only: a backslash is a
legal character in a POSIX file name, and splitting on it there would
invent a directory out of a file called `a\b`.
path_dir() also keeps the separator when what is left would be a bare
drive, because `C:\config.kdl` lives in `C:\` and `C:` names the
current directory on drive C, which is a different place.
Verified on the Windows box with a fully backslashed SLOSH_CONFIG:
--check passes, the include loads, and editing the config reloads the
running session. Watching a second directory is still unreliable; that
is the watcher's own bug and the next commit.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`include "C:/themes/t.kdl"` on Windows did not fail to find the file. It
found a different one, or rather looked for it: absoluteness was tested
as `path[0] == '/'`, a drive letter is not that, so the path was read as
relative and joined to the including file's directory. The result was
cannot open C:/Users/you/cfgA/C:/Users/you/cfgB/t.kdl
which at least says what happened. A layout's `cwd="C:/srv/api"` took
the same turn through path_resolve() and said nothing at all -- the pane
started in the wrong directory, which is the failure this file's own
header comment is about.
Windows spells absolute three ways and all three now count: rooted on
the current drive (`\dir`), rooted on a named one (`C:\dir`, `C:/dir`),
and a UNC share. A drive letter has to be followed by a separator to
qualify, because `C:file` is relative to the current directory *of that
drive* -- a real thing, and not one we can resolve for anyone.
Windows-only, deliberately. `C:` is an ordinary file name on POSIX and a
path beginning with one is relative there, so reading it as a drive
would break the platform that works in order to fix the one that does
not.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The rule had version.h as an order-only prerequisite of its output
directory, and nothing else. Order-only means "make this first if it is
missing", so the file was written once and then never again: the
version is derived from `git describe`, not from a file, so no
timestamp on disk ever moves when it goes stale, and make had no reason
to look.
Every Windows binary after the first therefore carried whatever the
version had been the day the build directory was created. The one on
the test box reported 0.1.0-2-gba48db9 -- a commit that no longer
exists, since the rebase rewrote it -- while being built from a tree
six commits later. `--version` was not just stale, it was pointing at
history that had been thrown away, which is worse than saying nothing.
So FORCE, and write through a temp file that replaces the real one only
when the text differs -- the same shape the main Makefile already uses
for build/version.h, and for the same reason. `make` after a commit
rebuilds; `make` twice in a row still does nothing.
Makefile.macos does not generate a version.h of its own -- it takes the
main Makefile's -- so it never had this.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
slosh builds and runs on Windows 10/11, x86-64 and ARM64, as one exe
with no runtime dependency beyond the OS: panes, splits, tabs,
detachable sessions, the config file and its live reload, the control
socket, the mouse.
The port is a compatibility layer rather than a fork. compat/win holds
stub <poll.h>, <sys/socket.h>, <termios.h> and friends that redirect to
include/compat_win.h, which is force-included ahead of every translation
unit -- so the existing sources keep their POSIX includes and their
POSIX call sites, and the platform differences are answered in one
place.
Two decisions carry most of it.
Everything the event loop waits on is a socket. On POSIX a pty master,
an inotify descriptor and a unix socket are all file descriptors, so one
poll() covers them; Windows has no such union, and WSAPoll waits on
SOCKETs and nothing else. Rather than rewrite server.c's loop around
WaitForMultipleObjects -- which caps at 64 objects and cannot express
POLLOUT -- each non-socket source gets a pump thread that copies it into
one end of a loopback socketpair. The loop is then the POSIX one
unchanged, and only what sits behind the descriptor differs.
Socket descriptors are tagged rather than guessed: a wrapped SOCKET
comes back as SL_SOCK_BASE + slot, so read, write, close and poll can
tell a socket from a CRT descriptor with certainty instead of inferring
it from the numeric range. That is why socket() and accept() are shimmed
too -- an untagged descriptor escaping into the loop is the one failure
mode this scheme has, and it is worth closing completely.
Three things bit hard enough to be worth naming.
A pseudoconsole does not stop the child inheriting your stdio.
CreateProcess copies the parent's standard handles into the child when
STARTF_USESTDHANDLES is absent, and those win. With slosh's own stdout
redirected -- a pipe under ssh, a file under --script, a log under the
daemon -- the shell wrote its banner into slosh's output instead of into
the pane, and the pane had a correct title (ConPTY emits that itself)
over an empty body. The three handles are cleared for the duration of
the call, leaving the child nothing to inherit.
A ConPTY never reports EOF. The output pipe belongs to the
pseudoconsole, not to the child, so it stays open after the child exits
and pane_pump()'s read() -- the entire mechanism by which a pane learns
its program is gone -- never returns 0. A pane whose shell had been told
`exit` sat there looking alive forever. So the exit is waited for on the
process handle, and ClosePseudoConsole is what then produces the EOF.
That third thread is also why the per-pty state is reference counted: a
pump blocked on a dead pipe must not be waited for, or pty_close would
hang the session on the one program that misbehaves, so whoever finishes
last frees.
cmd.exe does not use CommandLineToArgvW's quoting. A literal quote is
\" to an ordinary program and a backslash-then-end-of-quoted-section to
cmd, so building `cmd /c notepad "C:\...\config.kdl"` the first way
handed the editor \"C:\...\config.kdl\", backslashes and all, and
notepad answered "Not a valid file name" about a path that existed.
cmd /c now gets the rest of the line verbatim -- the caller composed a
complete command with its own quoting, exactly as it does for sh -c.
Paths are mixed, and only some programs mind. slosh joins with '/' while
the environment supplies '\', so the config landed at
C:\Users\you/.config/slosh/config.kdl. The CRT opens that without
complaint, which is why --check reported it fine, but a GUI file dialog
rejects it outright. Anything handed to another program now goes through
sl_path_native() first; the internal representation is left alone.
The rest is small. Panes run %ComSpec%, sessions live in
%LOCALAPPDATA%\slosh rather than $XDG_RUNTIME_DIR because a session
socket must not follow a roaming profile to another host, there is no
SIGWINCH so a resize is noticed by comparing the console size on a
bounded poll, no fork so the daemon is a fresh detached process that
reconstructs its arguments on the command line, and no process group on
a pty so the foreground program is the youngest descendant of the pane's
shell. C-a e prefers whichever console editor is actually installed --
notepad is a GUI program, so a pane running it draws nothing and looks
broken -- and says so when it has to fall back.
make -f Makefile.windows ARCH=x86_64 (or aarch64) cross-compiles from a
mac or Linux, and builds natively on the box with zig on PATH.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The zip held a bare binary while the tarballs ship README, LICENSE,
THIRD-PARTY-LICENSES and both manpages. The licenses are not decoration
on either platform -- libghostty-vt and stb_image are statically linked
into the mac binary too, and THIRD-PARTY-LICENSES exists precisely for
redistributed builds -- and a manpage is as useful on macOS as anywhere.
The archive now stages slosh-VERSION-macos-ARCH/ with the binary and
the same doc set, mirroring the tarballs' layout. The list is read out
of the main Makefile's REL_DOCS rather than copied here, so the two
artifacts cannot drift; both Makefiles travel to the builder together.
Notarization is unchanged: the ticket is keyed to the binary, and inert
documents beside it are nothing to Apple's scan.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
web/ and tests/fuzz/ are excluded from the sync (hundreds of megabytes,
none of it an input to 'make all') but both are tracked, and .git goes
over whole -- so the moment a commit touched web/, the builder's stale
copy read as a local edit and git describe stamped a clean release tree
-dirty. slosh-0.1.1-dirty-macos-arm64.zip was signed, notarized and
correctly labelled as a tree nobody had: the exclusion's debris, not
the release.
After the rsync, restore the excluded tracked paths from the .git that
just went over: no network, no bandwidth lost to the exclusion, and a
tree that is really dirty still says so.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
On Darwin a pty master has no line discipline until its slave has been
opened: every TIOCSWINSZ on the master until then fails with ENOTTY and
the size is dropped without a word. A pane is born 1x1 and told how big
it really is by the layout an instant after the fork -- and with the
child doing the open(), that message raced the child and usually lost.
Nothing ever sent it again, so the program in the pane spent its whole
life believing it had one row and one column. `C-a e` hit it every
time, because the editor's pane is always brand new: nvim drew its
screen into two rows in the corner of a 94x15 pane.
The old code knew half of this -- the child set the winsize on the
slave it had just opened, which is what kept a fresh pane from being
0x0 -- but that only fixed the *spawn* size. Every resize after it
still went to a master that might not have a slave yet.
So open the slave here, before the fork, and let the child inherit it.
The fork copies the fd table, so the slave is open from the child's
first instruction and every resize the parent sends afterwards lands;
BSD's forkpty() does the same thing for the same reason. The winsize is
set on that slave before the fork, which orders it ahead of anything
the parent sends later, and the child no longer sets it at all: by the
time the child ran, the size it was spawned with could already be
stale, and writing it back would undo the very resize this is about.
test_pty_size.py asks the pane's own program what size it thinks it is
-- for the first pane, for a pane from a split, and after a resize --
and compares it to the pane. It printed "1 1" against a 74x18 pane
before this.
live_m0.py asked `tput cols`, which needs a terminfo entry for
xterm-ghostty and therefore fails on most macs for a reason that has
nothing to do with resizing. `stty size` asks the tty itself, which is
the thing under test.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The vendored tree has no .git, so upstream's version detection runs
git describe in *our* repository -- and the moment this repo grew its
first release tag (v0.1.0), ghostty's build refused it: 'tagged
releases must be in vX.Y.Z format matching build.zig'. Our v0.1.0 is
not ghostty's 1.3.2-dev, and upstream is right not to let a stranger's
tag name its build.
-Dversion-string is upstream's own escape hatch for exactly this. The
value is read from the vendored build.zig.zon rather than written here,
so a re-vendor updates it in the one place it already lives. Both
invocations (the host vendor target and the cross-built release legs)
get it through one VT_FLAGS.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
'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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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'.
|
| |
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
| |
Sixteen lines of box-drawing was the biggest thing on the page and the
least true: a static sketch of two frames says nothing the first
paragraph had not already said, and the real look -- themes, compact
borders, shaders -- is not something a code block can be honest about.
The docs site keeps its copy; the README gets to the install command
sooner.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|