aboutsummaryrefslogtreecommitdiffstats
path: root/contrib (follow)
Commit message (Collapse)AuthorAge
* brew: pin the build to zig@0.16HEADmasterPeter Geissler8 days
| | | | | | | | | | | 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.
* release: v0.1.8 paperwork -- formula and PKGBUILD pinneduser2026-09-22
|
* theme: a name and a directory, not sixty colours in your configuser2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* tabs: idle tabs you can actually read across the roomuser2026-09-22
| | | | | | | | | | | | | | | | | | | | | | 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.
* sidebar: a status colour mixed from the theme, not the chrome's flat greyuser2026-09-10
| | | | | | | | | | | | | | | | | | 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.
* sidebar: statuses wear italic instead of an indent, and can wear colouruser2026-09-10
| | | | | | | | | | | | | | | | | | | | | | | 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.
* build: a vendor bump rebuilds libghostty-vt, everywhere it is consumeduser2026-09-10
| | | | | | | | | | | | | | | | | | | | | 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.
* release: v0.1.7 paperwork -- formula and PKGBUILD pinneduser2026-09-10
|
* release: v0.1.6 paperwork -- formula and PKGBUILD pinneduser2026-09-07
|
* docs: a way back to slosh.foo from every page; README links the siteuser2026-09-02
| | | | | | 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.
* arch: point at ghostty-terminfo, the pacman spelling of the entryuser2026-09-02
| | | | | | | | 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.
* release: v0.1.5 paperwork -- formula and PKGBUILD pinneduser2026-09-02
|
* slosh --install-terminfo: the binary carries its own terminfo entryuser2026-09-02
| | | | | | | | | | | | The xterm-256color fallback works everywhere; the real entry is better, and a tarball user on a bare box had no easy way to get it. The entry is generated from the vendored tree (contrib/terminfo, so it describes the terminal core we actually ship), compiled with tic, embedded as bytes, and written to ~/.terminfo under every name it answers to, letter and Darwin-hashed layouts both. No root, no package, no conflict with a ghostty installed later. Proven end to end in a mount namespace: bare box falls back, installs, and the next pane says xterm-ghostty.
* version: never answer for somebody else's git; arch: 0.1.4-3user2026-09-02
| | | | | | | | | | | | | | | | 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.
* ship: paperwork before the website, so the demo is not stamped -dirtyuser2026-09-02
| | | | | | | 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.
* release: v0.1.4 paperwork -- formula and PKGBUILD pinneduser2026-09-02
|
* contrib/vendor-check: watching upstream is one command nowuser2026-09-02
| | | | | | | | | | | | 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.
* arch-repo: find zig on PATH itself; release: v0.1.3 paperworkuser2026-09-02
| | | | | | | | | 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.
* gen-docs: the page title separator is a middle dotuser2026-09-02
|
* themes: pin the attach tag to each palette's muted chrome toneuser2026-09-02
| | | | | | 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.
* webdemo: version from contrib/version, published by shipuser2026-09-02
| | | | | | | | | | 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.
* shaders: remove the .so plugin loaderuser2026-09-02
| | | | | | | | | | | | The static musl release builds cannot dlopen -- musl compiles it out -- so loadable shader plugins were a feature most shipped binaries could not use. Nothing we ship used it either: every shader in contrib/shaders is an expression. Gone: the dlopen/glob loader in shader.c, shader_dir in the config, the plugin ABI table and entry macro, the Windows dl*/glob shims that existed only for it, the contrib/shader-plugin skeleton, its tests, and every doc mention. shader_abi.h stays as the shader model (cell_t, shade_ctx_t, shader_t) with a note about what it used to be.
* contrib/ship: the whole release as one resumable commanduser2026-09-02
| | | | | | | | | | | | 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).
* public links: point at github.com/sl0p-foo/slosh instead of cgituser2026-09-02
| | | | | | | | GitHub is the public interaction point now. The formula and PKGBUILD fetch GitHub's /archive/ tarballs (same git-archive bytes, per-ref stable); the PKGBUILD is bumped to 0.1.2 while at it, and its cd's drop the v -- GitHub tag archives extract to slosh-X.Y.Z/. The cgit remote stays for pushing; only published URLs change.
* brew: pin formula to v0.1.2user2026-09-02
|
* point homebrew tap at github.com/sl0p-foo/homebrew-sloshuser2026-09-02
| | | | | brew auto-resolves GitHub taps, so the explicit URL is no longer needed: 'brew tap sl0p-foo/slosh' is all users type.
* arch: sign the packages and the database, and say whose keyuser2026-08-29
| | | | | | | | | | | | | | | | | | | | 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.
* arch: a self-hosted pacman repository, since the AUR is shutuser2026-08-29
| | | | | | | | | | | | | 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.
* aur: a PKGBUILD, building offline from the cgit snapshotuser2026-08-28
| | | | | | | | | | | | 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.
* release: one command to bump, commit and taguser2026-08-28
| | | | | | | | | | | | | | 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.
* macos-remote-build: a synced tree must not read as dirtyuser2026-08-27
| | | | | | | | | | | | | | 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.
* slosh(5): the file formats get their manpage; slosh(1) learns shadersuser2026-08-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | | Section 5 is where unix keeps its file formats, and slosh has two documents sharing one syntax -- the configuration and the layouts -- so they share a page the way they share a parser. docs/slosh.5 covers the KDL subset, where the file lives and how reload refuses a broken one, include and its precedence, every top-level setting grouped the way config.md groups them, the theme block, the shader vocabulary with the expression language summarised, the states and their ranking, the keys block including direct bindings, and the layout schema (tab, pane, and their properties). Held to the source like slosh.1 before it: every name in config.c's KNOWN_TOP -- the list the loader refuses unknown settings against, itself held to the loader by test_config.py -- must appear in the page, so a knob cannot be added without this page hearing about it. The pair must cross-reference each other, and both must be pure ASCII and lint clean under groff -ww. slosh(1) grows a SHADERS section -- the one feature a synopsis reader would never guess a multiplexer had -- and its SEE ALSO now names slosh(5) and nothing else: the other multiplexers can advertise themselves. Install puts the page under share/man/man5, the release tarballs carry both pages, and the Homebrew formula installs what the commit it is building has, guarded as before.
* a manpage: slosh(1), installed, shipped, and held to the sourceuser2026-08-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | The distributables were a bare binary and a directory of Markdown, which is fine until somebody types 'man slosh' -- the unix-traditional place to ask. docs/slosh.1 is hand-written man(7), pure ASCII roff, no toolchain: the synopsis, the options, the session commands, the default keys, the mouse (including the new drag-past-an-edge scrolling), where the configuration lives, what SLOSH_* means in a pane's environment, files, exit status. The prose docs stay the long form; the page says so and points at them. 'make install' puts it under PREFIX/share/man/man1 (and uninstall takes it away), the release tarballs carry it beside the LICENSE -- a tarball is what a packager starts from -- and the Homebrew formula installs it when the commit it is building has one, guarded because the formula must be able to build commits older than the page. Prose about a program drifts unless something holds them together, so test_docs.py holds the page to the source the way it already holds the site to docs/nav: every option main.c's parser compares against and every word command must appear in the page, every SLOSH_* variable the code reads or sets must be documented, the roff must be pure ASCII (it travels through implementations that never heard of preconv), and where groff exists the page must lint clean at CHECKSTYLE=3 with -ww. Adding a flag without documenting it is now a failing test, which is the only kind of documentation policy that survives contact with a deadline.
* macos-remote-build: tolerate notarization ticket propagation laguser2026-08-27
| | | | | | | | | | | | | | | | | | 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.
* release: one 'make release' for the three target binariesuser2026-08-27
| | | | | | | | | | | | | | | | | | | | | | | | 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: derive from git tags instead of a hand-bumped constantuser2026-08-27
| | | | | | | | | | | | | | | | | | | 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.
* macos-remote-build: stop rsyncing 850M of caches to the builderuser2026-08-27
| | | | | | | | | | | | | | | 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.
* macos: the pipeline is the project's, the identity is yoursPeter Geissler2026-08-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | Makefile.macos was kept out of git because it named a team, a person and a mac. But almost all of it is mechanism -- build, sign, zip, notarize, staple, unlock a headless keychain -- and that is the project's, not the releaser's. Keeping the whole file out to protect four values meant the one machine that had it was the only place a release could be cut from, and that nobody could read how signing worked without being handed it. So the split is inverted. Makefile.macos, the builder doc and the scripts are tracked; macos-release.mk holds TEAM_ID, SIGNER and BUILDER and is the only thing gitignored. macos-release.mk.example is the template. The identity strings are now assembled from SIGNER and TEAM_ID rather than written out, which means an unconfigured checkout composes "Developer ID Application: ()" -- plausible, wrong, and diagnosed only by codesign a minute into a build. require-identity and require-builder catch it before anything runs and name the file to create; `help` prints "(unset)" rather than the empty shape. macos-remote-build now pushes macos-release.mk instead of Makefile.macos. That is the better arrangement anyway: the builder gets its pipeline from git at the commit being released, so a release is signed by the flow belonging to it and not by whatever the laptop was carrying. MACOS-BUILDER.md was in the same position -- local, and referenced by a file that is now tracked -- so it becomes docs/macos-release.md with the certificate name and notary key id replaced by their shapes.
* brew-release: cut a release from the Linux box it is cut fromPeter Geissler2026-08-26
| | | | | | | | | | | | | | | | | | | | 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.
* macos: sign and notarize on a mac that is not this onePeter Geissler2026-08-26
| | | | | | | | | | | | | | | | | | | | | | An unsigned mac binary is "damaged and can't be opened", so a release has to be signed with a Developer ID and notarized by Apple, and that needs a mac holding a private key. That mac is not the machine you develop on -- under `make all` it never is -- so `make macos-dist` builds nothing here: it syncs the tree to the builder over ssh, has it build, sign, notarize and staple, verifies the result there and fetches the zip back. The near end needs only ssh, rsync and a hasher, so it works from Linux. contrib/macos-builder-setup turns a bare mac into that builder and is re-runnable, with --check to say what is missing. The two things that make an unattended builder different from a laptop are handled rather than worked around: a keychain of its own, unlocked from a 0600 password file because a headless mac has no unlocked login keychain and codesign dies with "User interaction is not allowed"; and exactly one copy of the identity in the search list, because two make codesign ambiguous. No identity is in the repo. Who signs, which team, which mac -- that is Makefile.macos, which stays out of git along with dist/, and macos-dist tells you so when it is not there.
* homebrew: a formula, and a script that keeps it honestPeter Geissler2026-08-26
| | | | | | | | | | | | | | | | | | | | `brew install slosh` is the shortest install there is, and the formula is small because `make install` already does the work: zig as the only build dependency, both zig caches redirected into the build directory so the sandbox does not deny them and the user's real cache is not polluted, and the commit id handed to the Makefile because a snapshot tarball has no .git for `git describe` to read. The url/version/sha256 triple is the part that rots. contrib/brew-release resolves a ref to a full commit id, fetches the cgit snapshot, hashes what it actually got, and rewrites those three stanzas -- it does not commit or push, so the diff is read by a human first. Untagged pins carry the commit count so `brew upgrade` can tell newer from older; alphabetical comparison of two short ids cannot. The formula lives here as the source of truth and is copied into the tap; contrib/homebrew/README.md says why it is a formula and not a cask, and what still stands between this and homebrew-core.
* macos: the kernel still knows, it just spells /proc differentlyuser2026-08-26
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Portability fixes contributed by our macOS developer, applied with one hardening tweak (below). Darwin has no /proc, so everything that read one gets the native answer: live_cwd via proc_pidinfo(PROC_PIDVNODEPATHINFO), the foreground command via sysctl(KERN_PROCARGS2) parsed back into the NUL-separated shape /proc/<pid>/cmdline gives, and SLOSH_BIN via _NSGetExecutablePath plus realpath. The pty gets its winsize set on the slave after opening it, because Darwin attaches no line discipline to the master until then and the earlier TIOCSWINSZ was silently dropped -- a pane born 0x0. The shader repl binds tab in libedit's own words, since macOS ships Python's readline as a libedit wrapper that accepts GNU syntax in silence. Two finds are cross-platform bugs: a toast wider than the screen minus the gap computed a negative x that uint16_t made ~65533, drawing the longest messages -- the ones most worth reading -- off screen; now the middle is elided (a path's tail is the half you needed) and x clamps. And a pane label was 128 bytes, which a Homebrew interpreter's full path exceeds before its arguments even start; dump-layout wrote the truncation down and a restore ran it, so it is 1024 now. Saving a layout learned cwd_as_named: the kernel answers with the physical path, and on macOS /tmp is a symlink to /private/tmp, so a layout that said cwd="/tmp" stopped round-tripping through a save. If realpath(named) is where the pane still is, the named spelling wins. Tests follow: realpath'd tmpdirs, the scroll test reads the shell's first prompt instead of spelling out bash 5.3, and wrapped pane text is joined before matching a long path. One tweak on top of the contribution: the elision needs five columns for a head, the dots and a tail -- below that `avail - 3` goes negative, and negative as size_t is a pointer into the weeds; a terminal that narrow gets a plain clip.
* slosh: the website lives in web/ -- one directory, one build, make wwwuser2026-08-19
| | | | | | | | | | | | | | | | | | | | | | Consolidate everything the website is made of: - web_new/ -> web/: the front page, install.sh, and build-site - contrib/webdemo/ -> web/demo/: the in-browser machine, committed for the first time (it had never been checked in) - make www at the top level assembles the whole site into build/www (NO_DEMO=1 skips the guest cross-compile) - web_plan.md deleted: the plan is fulfilled and web/README.md is now the record Path fallout: the overlay theme symlink gains one ../ (web/demo is a level shallower relative to contrib/themes), PYSRC in the Makefile follows the demo scripts, contrib/README.md drops its webdemo section. ROOT := ../.. in the demo Makefile survives unchanged -- web/demo sits at the same depth contrib/webdemo did. Verified: make www builds front page + docs + demo end to end, web/demo make check boots the image to slosh, tests/test_docs.py and make fmt-check green.
* slosh: layout files are *.layout, not *.layout.kdluser2026-08-19
| | | | | | | | | | | | | | | | | | | | | The old name argued that the extension should say how to read the file. The argument lost to a collision: zellij speaks the same KDL syntax under that same extension with a different schema, and to anyone arriving from there .kdl said *whose* file it is -- naming the wrong owner is worse than not naming the parser. So a layout is x.layout, a project's is slosh.layout, the name says what the file is and --check says how it was read. (It is still KDL underneath; point your editor's highlighting at *.layout.) Nobody's checked-in workspace vanishes on upgrade: the project scan still finds slosh.layout.kdl, never writes it, and where both spellings exist the new one wins, because two files disagreeing needs one answer. The unparseable-file heuristic in --check learned the new suffix -- it matched '.layout.' with a trailing dot, which the new name no longer has. The webdemo overlay moves with it (demo.layout), rebuilt and boot-checked.
* slosh: defaults most people can live in, said by shadersuser2026-08-19
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The out-of-the-box look, rethought as one piece: - the compiled-in palette calms down: muted blue #7aa2f7 on the same neutral darks, with the colours that mean something kept semantic -- amber bell, red-orange dead. sl0p stays one include away as the house style, and contrib/themes/default.kdl is the starting point written down (theme-tour shows it). - derived state defaults now actually follow the theme: the scrolled wash is rebuilt from scroll_bg after the theme block (it used to bake the compiled accent), and declaring a state -- in any file of an include chain -- still wins. The dump writes derived states as comments so C-a e cannot pin what is meant to keep following. - a bell you can see: the frame blinks three times in theme's bell colour then breathes until answered, and a soft diagonal shimmer -- the bell colour halved towards default_fg, width breathing on a sine -- crosses the body for the same 900ms. bell_indicator false silences all of it; states { bell { } } replaces it like any state. since carries the clock dependency, so an idle session pays nothing. - scrollback says where it continues: while scrolled, the viewport's top and bottom rows melt towards an edge with content past it and turn solid at the buffer's end. New expression variables above/below (lines hidden past each edge, own dependency bit, cached per scroll position), carried in shade_ctx_t -- SLOSH_SHADER_ABI 3 -> 4. - expressions survive the dump: a program keeps its source and cb_chain writes it back, so amount="(y % 2) * 40" no longer dumps as a number. - headers move to include/, -Iinclude everywhere; the plugin Makefile's SLOSH var becomes SLOSH_DIR because slosh exports SLOSH=1 into every pane it runs, and building from inside a session read that as a path. - comment drift culled where features outran the prose, and the derivation boilerplate folded into derived_pass()/ctx_scroll().
* slosh: rename the project from sl0pptyuser2026-08-18
| | | | | | | | | | | | | | | | | | | | | sl0p + pty was error-prone to type and the zero made it worse; slosh (slop + sh) is one syllable, impossible to typo, and slosh.foo is ours. Mechanical rename everywhere it appears: binary, env vars (SLOSH, SLOSH_SESSION, SLOSH_BIN, SLOSH_CONFIG), socket dir (/tmp/slosh-<uid>, $XDG_RUNTIME_DIR/slosh), config path (~/.config/slosh), header guards, src/sl0ppty.h -> src/slosh.h, contrib/sl0ppty-dev -> contrib/slosh-dev, the driving-slosh skill, docs and tests. The sl0ppi fork references and the sl0p theme stay: one is history, the other is the brand. The rename surfaced a latent bug in test_osc5577: the hello-reply answer always appeared on screen twice (ECHOCTL echo plus cat -v), but a seven-character implementation name happened to wrap the second copy across the 60-column boundary, so counting substrings in wrapped text found one and passed for the wrong reason. A five-character name moved the wrap and exposed it. The loop guard itself is correct -- with echo off before the request there is exactly one reply -- so the test now counts on de-wrapped text and tolerates the pane's own echo.
* sl0ppty: ruff format the Pythonuser2026-08-17
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0ppty: a skill for driving it, and the two variables that made one possibleuser2026-08-17
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `.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.
* sl0ppty: layouts are `*.layout.kdl`, and each loader says which it gotuser2026-08-16
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `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.
* sl0ppty: the prompt speaks config, not a dialect of ituser2026-08-16
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.