| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
| |
homebrew-core moved "zig" to 0.17.0, and the vendored libghostty-vt
rejects any mismatched major/minor, so every brew install broke with
"Your Zig version v0.17.0 does not meet the required build version of
v0.16.0". Installing zig@0.16 by hand did not help: it is keg-only, so
it is never on PATH, and the formula took the "zig" opt path regardless.
Also stop hardcoding the version in the test block; it had gone stale at
0.1.0 and failed against the 0.1.8 binary.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Wearing a theme meant pasting its `theme { }` block into config.kdl, or
including a path to a file you had copied there yourself. Both make the
palette a literal in your config, and a literal is the one thing you cannot
change cheaply: not from a script, not from a running session, not by a list
of what is available, and never again from upstream -- the colours you pasted
are the colours that theme had the day you pasted them.
So a theme is a file in a directory now, and a config names it:
theme_name "phosphor"
theme { frame_focus "#00ff88" } // ...but that one colour is mine
theme_name is applied after the includes and before everything the file says
itself, which is the rule includes already follow and for the same reason: the
loader reads a document by asking it for names rather than walking it in order,
so "here" is not a position it could honour, and "what you asked for is the
base, what you wrote is yours" is the rule worth having.
Two directories, yours first: `themes/` beside the root config -- beside the
file that was loaded, so a config somewhere else (SLOSH_CONFIG, or one handed
to --check) takes its themes with it -- and then <prefix>/share/slosh/themes,
compiled in from PREFIX. install(1) has been putting themes there all along
and nothing could read them; now a shipped theme is nameable without being
copied, and a file of yours with the same name shadows it, which is how a
root-owned theme gets edited.
A name, not a path. The directory is what makes a theme switchable, listable
and shadowable, and a path silently does none of those, so `theme_name
"../x.kdl"` is refused with a line pointing at `include`, which still works
and is still the answer for a file somewhere a directory of names cannot
reach. A theme that is not there is one complaint and no more (D9): the rest
of the config applies, because losing your keybindings over a mistyped colour
scheme would be the wrong trade. A theme file is read by the same loader, so
it is watched like any other: saving one reloads the session wearing it, and
the derivations (tab_status_fg, tab_idle_bg) run over the theme's own colours
exactly as before.
Switching without editing anything, over the socket a session already has:
slosh -s main cmd theme # amber default* ...
slosh -s main cmd '{"cmd":"theme","name":"amber"}' # now
slosh -s main cmd '{"cmd":"theme","name":"amber","save":true}'
The session holds the name over later reloads, so saving your config for an
unrelated reason does not put the old colours back; `save` writes the
theme_name line into the file, replacing the one already there and leaving
every other line and comment alone -- dumping the whole config back over
itself would reformat a hand-written file and drop its comments, which is not
a fair price for changing a colour scheme. A name is tried on a throwaway
config first, so a typo costs nothing. An empty name gives the answer back to
the file.
--dump-config stops writing colours: it writes the theme's name and the
colours a config overrode by hand, so a dumped config keeps following its
theme instead of freezing it. --dump-theme is the other half and is new: the
resolved palette as a theme file, which is how you start one from the session
in front of you rather than from an empty file. It is also what keeps
contrib/themes/default.kdl honest -- that file is now the annotated reference
(the commented palette moved there out of config.kdl, where it was 119 lines
of colour in the middle of the prose), and a test compares it against the
compiled-in one.
While in there: tests/test_themes.py's "every theme sets every colour" guard
had been grepping for `&c->`, the spelling before the colour table moved to
offsetof, so it had been finding zero colours and passing for months. Fixed,
with the derived and opt-in colours exempted for the reasons they are derived
and opt-in, and with a check that the guard found a table at all.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Seen next to the accent, the first plate was too polite to do the job it was
added for: a 30% lift off the background under labels in the same #45454a the
idle frames use. From another monitor the strip still read as one dark band
with a blue block in it.
Both ends move. tab_idle leaves the ambient chrome grey and becomes #8a8a95,
because a tab you are not in is a place to go and has to be legible at a
glance, where a dimmed pane border only has to be findable. The plate follows
it to 43%, which is not a taste constant but a contrast ratio: the same step
taken from a dark background upward and a light one downward lands at about
1.9:1 either way, so paper's #bdbebe on near-white and the default's #3b3b40
on black are the same amount of plate.
The six themes in contrib named their idle tab colour as their frame grey, so
each gets the same move in its own family -- 40% of the way from that grey to
the colour the theme writes text in -- and their plates follow by derivation:
amber #a67c26, mono #909090, paper #666a6e, phosphor #4a9c5d, slate #70789c,
sl0p the house #8a8a95. Nobody who named a colour loses it; the derivation
only ever fills in a theme's silence.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
tab_status_fg defaulted to the ambient grey on the argument that the
slant already carried the distinction. True, and it looked it: the
frame, the inactive labels, the pane count and the statuses were one
colour, and the liveliest thing in the sidebar read as furniture.
It is now 40% of the way from tab_count to tab_hover -- not the grey of
the chrome round it, not the full accent it would shout in. Derived the
same way rather than picked, so it lands in each theme's own family: a
monochrome theme mixes two greys and stays monochrome, phosphor stays
green, amber stays amber, and none of the seven in contrib needs a line
adding to it. That the mix cannot invent a hue is the same argument the
italic rests on, so both signals survive a colourless palette.
tab_status_bg is still unset: a band is opt-in, and none is what a
translucent terminal wants.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Three columns of indent is a fifth of a sixteen-column sidebar, spent
saying 'this belongs to the row above'. The slant says it in none: a
status now starts in the same column its tab's name starts in, and is
set in italic. That also survives the monochrome themes in contrib,
where a colour-only distinction would have had nothing to spend.
tab_status_fg / tab_status_bg are there for themes that do have colour
to spend. The fg follows tab_count when a theme does not name it -- the
same declared/derived split apply_scrolled makes -- so the six themes
in contrib get coherent status rows without a line added to any of
them. The bg is unset by default, because a band is opt-in and 'none'
is what a translucent terminal wants; the row is padded so a band that
*is* set covers the row rather than the words on it.
Two things fell out. The padding loop existed three times and is now
pad_cells. And cb_color wrote #000000 for an unset colour, so
quietly pinned attach_bg -- 'gray
ink on the terminal's own background', says its comment -- to a black
band. Unset colours are now written commented out, the same trick the
states block uses for what it wants re-derived rather than pinned.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Three stale-build landmines, all found by shipping v0.1.7 -- the first
release where new code (GhosttySearch) actually needed the vendor bump
of 2026-09-01:
- $(VT_LIB) had no prerequisites, so an existing .a *was* the build:
the macOS builder compiled pane.c against zig-out headers from before
the bump. The vendor manifest is now the prerequisite; a re-vendor
rewrites it, so a bump rebuilds the lib and its installed headers.
- web/demo had the same guard and also never passed -Dversion-string:
upstream's build.zig ran git describe in our repo, and on the tag
checkout web/publish builds from, a slosh tag is a fatal version
mismatch. Both fixed; both had hidden behind a stale build/vt.
- The builder's zig-pkg (excluded from the sync, and left half-empty by
a once-interrupted push) is an input, not a cache: it is now synced
separately and additively, so a bump's new packages arrive and a
partial local tree cannot maim the builder's copy.
|
| | |
|
| | |
|
| |
|
|
|
|
| |
The docs header gains a right-aligned slosh.foo link (absolute, so a
locally rendered build/docs points somewhere real too), and the
README says the website exists, which it never did.
|
| |
|
|
|
|
|
|
| |
optdepends says it at install time, the README and the install tab
say it before. Our package deliberately does not ship the file:
extra/ghostty-terminfo owns that path, and two packages must not.
Without either, slosh's own ~/.terminfo copy covers it. pkgrel=2,
both architectures rebuilt and republished.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The xterm-256color fallback works everywhere; the real entry is
better, and a tarball user on a bare box had no easy way to get it.
The entry is generated from the vendored tree (contrib/terminfo, so
it describes the terminal core we actually ship), compiled with tic,
embedded as bytes, and written to ~/.terminfo under every name it
answers to, letter and Darwin-hashed layouts both. No root, no
package, no conflict with a ghostty installed later. Proven end to
end in a mount namespace: bare box falls back, installs, and the
next pane says xterm-ghostty.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The v0.1.4 arch packages introduced themselves as 0.1.4-dirty: the
tag's tarball has no .git, makepkg builds it inside the release
box's repository, and git describe walked up and answered for the
outer tree -- mid-release, when the paperwork is honestly dirty.
Found at 0.1.2, diagnosed, and then left unfixed by yours truly.
Two fences now. contrib/version only trusts a .git whose toplevel is
its own root (helps every future tarball). The PKGBUILD exports
GIT_CEILING_DIRECTORIES for build and package (helps regardless of
which contrib/version the pinned tarball carries, which is why the
first rebuild still said -dirty: the fix was outside the tarball).
pkgrel=3; both architectures rebuilt, verified 'slosh 0.1.4', and
republished.
|
| |
|
|
|
|
|
| |
Steps 6 and 7 rewrite the formula and PKGBUILD; building the site
from the tag checkout with those uncommitted made git describe say
-dirty, and v0.1.4's demo introduced itself as 0.1.4-dirty+webdemo.
Commit the pins first, build the site from a clean tag second.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Vendoring by pinned commit stays (D12) -- the package channels build
from source archives where submodules are empty, the arch build is
offline-clean because the core is in the tarball, and upstream has no
stable release channel to depend on anyway. What was missing is the
watching: vendor-check reads the pin from vendor.json, asks the
GitHub API what happened since, and lists the commits touching the
terminal core. First run found 120 relevant commits in three weeks,
including upstream fixing the same class of kitty-graphics bug our
patch 0001 fixes -- which is the argument for finally sending it.
|
| |
|
|
|
|
|
|
|
| |
ship calls arch-repo without the ZIG= prefix the by-hand runs had,
so the leg died on makepkg's dependency check mid-release. arch-repo
now derives ZIG from PATH when pacman has no zig, and exports it.
The formula and PKGBUILD pins ride along: ship's steps 6 and 7 had
already written them when the leg failed, and its preflight rightly
refuses a dirty tree.
|
| | |
|
| |
|
|
|
|
| |
Every shipped theme names attach_fg explicitly, reusing its own
minbar colour -- the default gray suits the dark themes and nothing
else, and paper's light ground proved it.
|
| |
|
|
|
|
|
|
|
|
| |
The demo's Makefile said VERSION := 0.1.0 by hand, so the in-browser
slosh announced a release two tags old no matter what was built into
it. It now derives from contrib/version like every other artifact,
suffixed +webdemo so the guest build names itself. And ship gains a
website step: the site (demo included) is rebuilt from the tag
checkout and published as part of a release, so the demo cannot fall
behind again.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The static musl release builds cannot dlopen -- musl compiles it out
-- so loadable shader plugins were a feature most shipped binaries
could not use. Nothing we ship used it either: every shader in
contrib/shaders is an expression. Gone: the dlopen/glob loader in
shader.c, shader_dir in the config, the plugin ABI table and entry
macro, the Windows dl*/glob shims that existed only for it, the
contrib/shader-plugin skeleton, its tests, and every doc mention.
shader_abi.h stays as the shader model (cell_t, shade_ctx_t,
shader_t) with a note about what it used to be.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
contrib/release cuts the tag and stops -- publishing is on purpose.
ship is that purpose: push, artifacts (linux/windows here, macOS on
the builder), the GitHub release, the brew tap, the arch repo, the
paperwork commit. Each step skips when its result already exists, so
a run that died mid-way resumes instead of rebuilding artifacts whose
checksums are already published. Preflight checks every credential
and machine before anything is cut, because half-published is the
failure mode this exists to remove (v0.1.2 shipped while the arch
repo still served v0.1.1).
|
| |
|
|
|
|
|
|
| |
GitHub is the public interaction point now. The formula and PKGBUILD
fetch GitHub's /archive/ tarballs (same git-archive bytes, per-ref
stable); the PKGBUILD is bumped to 0.1.2 while at it, and its cd's
drop the v -- GitHub tag archives extract to slosh-X.Y.Z/. The cgit
remote stays for pushing; only published URLs change.
|
| | |
|
| |
|
|
|
| |
brew auto-resolves GitHub taps, so the explicit URL is no longer
needed: 'brew tap sl0p-foo/slosh' is all users type.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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 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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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().
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The bumpy one, again. Every `.py` outside `vendor/` plus the three `contrib/`
scripts that are Python with no extension: 60 files, 6,714 lines rewritten, no
behaviour changed. Nothing here is worth reading -- the next commit adds it to
`.git-blame-ignore-revs` so `git blame` walks past it.
It is a bigger reformat than the C was, 60% of the lines against 17%, and for a
reason worth writing down rather than tuning away: ruff and black break a call that
does not fit onto one argument per line, where this code aligned the continuation
under the opening paren. The test suite is thousands of `check("some prose", cond,
detail)` calls, so nearly every one of them changed shape and the suite grew 3,308
lines. Line length was the one lever that moved it -- 120 columns would have been a
40% reformat and no growth at all -- and it was left at 88, because picking an
unusual width to dodge a one-time diff is paying forever to avoid paying once.
Import sorting came with it. Two files had `import os, pty, uuid, select, struct,
sys, termios, time, fcntl, signal` on one line and now have ten.
Verified after, not before, because a formatter that alters behaviour would be
worse than none:
- `make retest`, 1801 checks green -- and this reformats the suite that does the
checking, including `harness.py`, which every one of those files imports.
- `make test-live`, the tty tests the fast suite skips. They matter here more than
usual: `live_input.py` and `live_m0.py` are two of the files whose comma-imports
were split, and nothing in the fast suite would have noticed if that broke them.
- All 60 files still compile, `make fmt` twice in a row changes nothing the second
time, and `ruff check --select I` has nothing left to say.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`.agents/skills/driving-sl0ppty/SKILL.md`. The convention is
`.agents/skills/<name>/SKILL.md` with YAML frontmatter -- already in this tree
under `vendor/libghostty-vt`, so it is not an invention -- and it means an agent
working in a checkout picks the skill up without being told, while a directory per
skill leaves room for adjuncts later. `contrib/README.md` and
`docs/scripting.md` say where it is and how to use it elsewhere: copy or symlink
the directory into wherever your agent keeps skills.
**Writing it turned up the reason there was no skill worth writing.** A pane is
told `SL0PPTY=1` and nothing else, so a program inside one knows it is in a
session and cannot find out *which* -- `ls` lists them all and none of them is
labelled "the one you are in". So the whole most valuable case, an agent driving
the session it is running in, was guesswork. The server now sets
`SL0PPTY_SESSION` and `SL0PPTY_BIN` in its own environment, which every pane
inherits through fork: one place, no signatures threaded, and unset under
`--script`, which has no socket to name. `SL0PPTY_BIN` is read from
`/proc/self/exe` rather than `argv[0]`, and exists because the failure it prevents
is the one I hit writing the examples -- a session started from a build tree, and
`sl0ppty` not on the pane's `PATH`, so the skill's own commands did not run.
What the skill actually teaches, none of which `docs/scripting.md` says because a
reference is not allowed to have opinions:
- Work belongs in a pane that was **given** a command, not typed into somebody's
shell. Then `alive` and `exit_code` off `panes` are how you wait, and the output
survives the program (D14) so it is still there to read.
- There is **no wait primitive over the socket**: `settle` is headless-only, and
`deadline` is about repaints and says nothing about program output. Poll.
- `purpose` is the handle. Titles change underneath you; a declared purpose cannot
be relabelled by the program in the pane, which is what makes it safe to address.
- `snapshot` is the whole composited session, and only what is on screen. Cut a
pane out with its `content_*` rect; do not expect scrollback.
- **The echoed command line matches your own marker.** `echo DONE` puts `DONE` on
screen twice. Count, or use a marker you did not send.
- Write `\e`, never `\033`: the unescaper takes `\0` first, so `\033` arrives as
NUL followed by `33` and nothing complains. That bug was live in this repo's own
test suite until last week.
- A program in a pane can draw its own status and real buttons with OSC 5577 and
read the click off its stdin, which is how it asks a question instead of printing
a prompt and hoping.
- Close what you made; do not `quit` a session you did not start.
`tests/test_skill.py` is the part that keeps it true. A skill is documentation an
agent acts on without a human reading it first, so a stale one is worse than a
missing one: a wrong verb is a failed tool call somebody else has to debug. Every
verb the file names is sent to a live session and must not answer `unknown cmd`;
every `panes` field its central recipe polls must exist; the environment it
promises is read out of a real pane; the recipe itself is run end to end and must
report exit status 3 with its output still on screen. It also greps the file for
paths out of a home directory, because "plug and play" is a property somebody has
to keep.
That test found `close-pane` in the draft, which is the *keybinding* action name --
the verb is `close`. Exactly the class of error the test exists for, found before
anybody's agent ran it.
Verified: 1795 checks, all green, 43 of them in `tests/test_skill.py`. Then the
skill's recipe run verbatim against a real session -- tagged pane, polled to
`alive:false`, `exit status: 3`, `compiling`/`built`/`exited: status 3` still
readable -- and from inside a pane, `session=agentdemo bin=/…/sl0ppty` and a
`building 3/7` status drawn in its own frame.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`config/layout.example.kdl` becomes `config/example.layout.kdl`, and the
convention for the files people write is `project.layout.kdl`.
Not a new extension, and the reason matters: the syntax genuinely *is* the same
hand-rolled KDL subset for both documents -- one parser reads them -- so calling
layouts something other than KDL would trade one wrong signal for another and
lose every editor that highlights it. The confusion is about *schema*, so the
schema goes in the stem: the extension says how to read the file, `.layout` says
what is in it. `docker-compose.yml` and `tsconfig.json` settled this convention
long ago.
**But a filename cannot enforce anything, and that turned out to be the real
bug.** `sl0ppty --check` on a layout file printed `ok` and reported "bindings 53"
-- it had read a document containing nothing it understood and said so was fine.
A config that `include`d a layout linted clean for the same reason. So did
`rouded true`. The loader reads a document by *asking it for the keys it knows*,
which is fast and clean and never notices what it was given.
So it notices now. `KNOWN_TOP` is every node the loader reads, and anything else
at the top level is a complaint with a file and line:
s.kdl:2: unknown setting: rouded
example.layout.kdl:14: this is a layout, not a config: `sl0ppty --layout` reads those
sl0ppty: config.kdl: this is a config, not a layout: `gap` is a setting
The `layout` node gets its own message because it is the mistake people actually
make, and the reverse direction is the same list read backwards -- the layout
loader asks `config_is_setting()` rather than keeping a guess of its own, so the
two answers cannot disagree about which file is which. A layout whose own tabs
are misspelled still gets "layout declares no tabs", which is a different
mistake.
D2 records it, since it is the decision that two schemas share one syntax.
Verified: `tests/test_config.py` gained thirteen checks -- the unknown setting
with its line, both wrong-schema messages, the misspelled-tabs case keeping its
own, the shipped config linting clean and the shipped layout loading as one, and
a drift guard that greps `kdl_child(root, ...)` out of the loader and fails if a
setting is missing from `KNOWN_TOP` (verified by removing `gap_aspect` and
watching it fail). Full suite green; my own config and the reference config both
still lint clean, which is the thing this change could most easily have broken.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
chrome> include "contrib/chrome/heartbeat.kdl"
heartbeat.kdl: 1 chrome, 0 content
chrome> shaders {
...> tint where="chrome" color="#ff5fd7" amount="abs(sin(t / 5))"
...> dim where="content" amount=80
...> }
1 chrome, 1 content
Both of those were errors an hour ago, and the second one was my fault twice
over: `:paste` printed `include "..."` and printing something you cannot type
back is a prompt lying about its own output. Pasting it answered `unexpected
argument for include`, and `load "..."` answered the same about `load`.
So the grammar is now the config's, all of it: an entry, several with `;`, an
entry with its own `where=`, a whole `shaders { }` block over as many lines as it
takes, `include "f.kdl"`, `load f`. There is a test that reads `:paste`'s block
off the screen, clears the pane, types it back line by line and compares the
cells -- round-tripping is the property, not a nice-to-have.
**The text is a document.** What it says replaces both chains, exactly as naming
`shaders { }` in a config replaces the block rather than adding to it, and an
entry's `where=` decides which rect it lands on. That drops a rule I invented last
week -- "one chain, one rect", which refused an entry that named the other one --
because it made the prompt's mode a law rather than a default and made a two-rect
block, the thing `:paste` prints, impossible to paste back. The prompt now keeps
the document and sends all of it, so a line aimed at the frame leaves the contents
alone, and the reply says `1 chrome, 1 content` rather than `ok`: with routing
visible, the counts are the answer worth reading.
`route_entries` is the one place routing is decided now -- config file, preset
file, typed line, same function. `config_parse_chain_doc` takes the wrapper or the
bare entries, because both are things a config contains and therefore both are
things somebody will paste.
Still nothing in the prompt parses KDL. A file goes by path; a pasted block goes
via a temporary file, since an OSC payload cannot carry a newline -- a control byte
ends the sequence, by the spec -- and joining lines with `;` would need exactly the
comment-and-quote scanner I refused to write. The only syntax the script looks at
is counting braces to know whether you have finished typing, and an empty line
gives up on that.
One hint, for the mistake that started this: when the session says `unknown
shader`, the prompt adds `(a file? :load <path>, or include "<path>")`. After the
fact rather than before, so a plugin's shader name is never second-guessed.
Verified: `tests/test_shader_repl.py` is 41 checks -- every input shape, the
multi-line paste and giving up on one, the round trip, the hint -- and
`tests/test_inband_shaders.py` gained the document rule in both directions,
including that a document naming only the frame takes the contents pass off. Full
clean suite green.
|