aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPeter Geissler <peter@haxx.in>2026-08-26 23:29:54 +0200
committerPeter Geissler <peter@haxx.in>2026-08-26 23:30:58 +0200
commit91526fe1f3482f610b38caa214a91f78564f7d50 (patch)
tree5bc1005f0a6838bdb093877d59c0615ffaab23fc
parentmake install: put the binary and the examples where they belong (diff)
downloadslosh-91526fe1f3482f610b38caa214a91f78564f7d50.tar.gz
slosh-91526fe1f3482f610b38caa214a91f78564f7d50.tar.xz
slosh-91526fe1f3482f610b38caa214a91f78564f7d50.zip
homebrew: a formula, and a script that keeps it honest
`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.
-rw-r--r--README.md11
-rwxr-xr-xcontrib/brew-release120
-rw-r--r--contrib/homebrew/README.md69
-rw-r--r--contrib/homebrew/slosh.rb86
4 files changed, 284 insertions, 2 deletions
diff --git a/README.md b/README.md
index ec1e636..bbd6887 100644
--- a/README.md
+++ b/README.md
@@ -21,8 +21,15 @@ C on top of [libghostty-vt](https://github.com/ghostty-org/ghostty).
## Install
-Needs [zig](https://ziglang.org) 0.16 and nothing else. The result is one static
-binary.
+With [Homebrew](https://brew.sh) (macOS or Linux):
+
+```bash
+brew tap sl0p/slosh https://git.sl0p.foo/homebrew-slosh.git
+brew install slosh
+```
+
+From source it needs [zig](https://ziglang.org) 0.16 and nothing else. The
+result is one static binary.
```bash
make vendor # the vendored terminal core, once
diff --git a/contrib/brew-release b/contrib/brew-release
new file mode 100755
index 0000000..603ed88
--- /dev/null
+++ b/contrib/brew-release
@@ -0,0 +1,120 @@
+#!/bin/sh
+# brew-release -- point the Homebrew formula at a commit or tag, with the
+# sha256 that the cgit snapshot actually has right now.
+#
+# contrib/brew-release # HEAD of origin/master
+# contrib/brew-release v0.2.0 # a tag (must be pushed already)
+# contrib/brew-release 1368b7f # any ref the server can resolve
+# contrib/brew-release --tap ~/src/homebrew-slosh v0.2.0
+#
+# It edits contrib/homebrew/slosh.rb in place (url, version, sha256) and, with
+# --tap, copies the result into <tap>/Formula/slosh.rb. It never commits or
+# pushes: read the diff, then commit both repos yourself.
+#
+# Publishing a new version is therefore:
+#
+# 1. bump VERSION in the Makefile, commit, tag, push (tag included)
+# 2. contrib/brew-release --tap ../homebrew-slosh v0.2.0
+# 3. brew install --build-from-source contrib/homebrew/slosh.rb # prove it
+# 4. commit the formula here and in the tap, push both
+
+set -eu
+
+REPO_URL="${SLOSH_REPO_URL:-https://git.sl0p.foo/slosh.git}"
+SNAP_BASE="${SLOSH_SNAPSHOT_BASE:-https://git.sl0p.foo/slosh.git/snapshot/slosh}"
+root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
+FORMULA="$root/contrib/homebrew/slosh.rb"
+TAP=""
+
+fail() { printf 'brew-release: %s\n' "$*" >&2; exit 1; }
+say() { printf ' %s\n' "$*"; }
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --tap) [ $# -ge 2 ] || fail "--tap needs a directory"; TAP="$2"; shift 2 ;;
+ --tap=*) TAP="${1#--tap=}"; shift ;;
+ -h|--help) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
+ -*) fail "unknown option $1" ;;
+ *) break ;;
+ esac
+done
+
+REF="${1:-}"
+[ -f "$FORMULA" ] || fail "no formula at $FORMULA"
+
+# Resolve whatever was asked for to a full commit id, so the URL names one
+# immutable tree. A tag keeps its own name in the version string; a commit
+# gets the short id.
+if [ -z "$REF" ]; then
+ REF="$(git -C "$root" rev-parse origin/master)"
+ label=""
+else
+ label="$REF"
+fi
+
+full="$(git -C "$root" ls-remote "$REPO_URL" "refs/tags/$REF^{}" "refs/tags/$REF" \
+ "refs/heads/$REF" 2>/dev/null | head -1 | cut -f1)"
+if [ -z "$full" ]; then
+ # Not a remote ref: assume it is a commit id, and let the local clone
+ # expand a short one. Then check the server really has it.
+ full="$(git -C "$root" rev-parse --verify "${REF}^{commit}" 2>/dev/null || echo "$REF")"
+fi
+case "$full" in
+ ????????????????????????????????????????) ;;
+ *) fail "could not resolve '$REF' to a commit id (got '$full')" ;;
+esac
+
+# The version brew shows. A tag is the honest answer when there is one;
+# otherwise the Makefile's VERSION, the number of commits, and the short id --
+# in that order so that `brew upgrade` sees a later build as later. Without the
+# count, "0.1.0-b744a90" vs "0.1.0-1368b7f" would compare alphabetically, which
+# has nothing to do with which one came first.
+# A tag must look like a version (digits with a dot); "1368b7f" starts with a
+# digit but is a commit, and calling it version 1368b7f would be a lie brew
+# could never climb down from.
+mkver="$(sed -n 's/^VERSION *:*= *//p' "$root/Makefile" | head -1)"
+[ -n "$mkver" ] || fail "no VERSION in $root/Makefile"
+short="$(printf '%s' "$full" | cut -c1-7)"
+count="$(git -C "$root" rev-list --count "$full" 2>/dev/null || echo 0)"
+case "$label" in
+ v[0-9]*.[0-9]*) version="${label#v}" ;;
+ [0-9]*.[0-9]*) version="$label" ;;
+ *) version="$mkver-$count-$short" ;;
+esac
+
+url="$SNAP_BASE-$full.tar.gz"
+say "ref $REF -> $full"
+say "version $version"
+say "url $url"
+
+tmp="$(mktemp -d)"
+trap 'rm -rf "$tmp"' EXIT INT TERM
+
+code="$(curl -sL -o "$tmp/snap.tar.gz" -w '%{http_code}' "$url")"
+[ "$code" = 200 ] || fail "snapshot fetch returned HTTP $code -- is the ref pushed?"
+tar tzf "$tmp/snap.tar.gz" >/dev/null 2>&1 || fail "snapshot is not a valid tarball"
+sha="$(shasum -a 256 "$tmp/snap.tar.gz" | cut -d' ' -f1)"
+say "sha256 $sha"
+
+# Rewrite exactly the three stanzas, leaving every comment in place.
+awk -v url="$url" -v version="$version" -v sha="$sha" '
+ /^ url "/ { print " url \"" url "\""; next }
+ /^ version "/ { print " version \"" version "\""; next }
+ /^ sha256 "/ { print " sha256 \"" sha "\""; next }
+ { print }
+' "$FORMULA" > "$tmp/slosh.rb"
+grep -q "$sha" "$tmp/slosh.rb" || fail "rewrite did not take -- formula stanzas changed shape?"
+mv "$tmp/slosh.rb" "$FORMULA"
+say "wrote $FORMULA"
+
+if [ -n "$TAP" ]; then
+ [ -d "$TAP" ] || fail "no such tap directory: $TAP"
+ mkdir -p "$TAP/Formula"
+ cp "$FORMULA" "$TAP/Formula/slosh.rb"
+ say "copied $TAP/Formula/slosh.rb"
+fi
+
+printf '\nnow prove it builds:\n brew install --build-from-source %s\n' \
+ "contrib/homebrew/slosh.rb"
+printf 'then commit the formula here%s\n' \
+ "${TAP:+ and in $TAP}"
diff --git a/contrib/homebrew/README.md b/contrib/homebrew/README.md
new file mode 100644
index 0000000..4f62299
--- /dev/null
+++ b/contrib/homebrew/README.md
@@ -0,0 +1,69 @@
+# Homebrew
+
+`slosh.rb` is a **formula**, which is the right kind of thing. The distinction,
+since it is the first question anyone asks:
+
+| | what it is for | what it does |
+|---|---|---|
+| **formula** | command-line software | brew builds it (or fetches a prebuilt "bottle") into `Cellar/` and symlinks `bin/` |
+| **cask** | prebuilt macOS apps: `.app`, `.dmg`, `.pkg` | brew unpacks the drop into `/Applications`, builds nothing |
+
+slosh is a binary you run in a terminal and it builds from source in under a
+minute, so: formula. A cask would be the answer only if we shipped the signed,
+notarized zip that `Makefile.macos` produces and wanted brew to just drop it in
+place.
+
+## The tap
+
+A formula has to live in a *tap* — a git repo named `homebrew-<tapname>` full
+of `Formula/*.rb`. Ours is `homebrew-slosh`, so users say:
+
+```bash
+brew tap sl0p/slosh https://git.sl0p.foo/homebrew-slosh.git
+brew install slosh
+```
+
+The explicit URL is only needed because the tap is not on GitHub; brew
+remembers it after the first tap. `brew install --HEAD slosh` builds the tip of
+master instead of the pinned release.
+
+(Getting into `homebrew-core` — plain `brew install slosh`, no tap — has a
+higher bar: a stable tagged release, a LICENSE, notable usage, and no
+network access during the build. See "known rough edges" below.)
+
+## Releasing
+
+`slosh.rb` here is the source of truth; the tap repo gets a copy.
+
+```bash
+contrib/brew-release --tap ../homebrew-slosh # pin origin/master
+contrib/brew-release --tap ../homebrew-slosh v0.2.0 # pin a pushed tag
+brew install --build-from-source contrib/homebrew/slosh.rb
+brew test slosh
+```
+
+The script resolves the ref to a full commit id, downloads the cgit snapshot,
+hashes it, and rewrites `url` / `version` / `sha256`. Untagged pins are
+versioned `0.1.0-<commits>-<short>` so brew can tell which build is newer;
+a tag is used verbatim.
+
+Before pushing a formula change:
+
+```bash
+brew style sl0p/slosh
+brew audit --strict --tap=sl0p/slosh
+```
+
+## Known rough edges
+
+- **The build fetches.** `zig build` downloads the pinned libghostty-vt
+ dependencies from `deps.files.ghostty.org` during `make vendor`. Homebrew's
+ sandbox allows this (verified), but homebrew-core forbids it — that would
+ need the zig packages vendored into the tarball or declared as `resource`s.
+- **No LICENSE.** The formula therefore has no `license` stanza. Taps do not
+ care; homebrew-core would refuse.
+- **Snapshot tarballs.** cgit generates them on demand; the bytes have been
+ stable across requests and days for a given ref, which is what the `sha256`
+ relies on. If that ever changes, switch the formula to
+ `url "https://git.sl0p.foo/slosh.git", using: :git, revision: "<sha>"`,
+ which is content-addressed and needs no hash.
diff --git a/contrib/homebrew/slosh.rb b/contrib/homebrew/slosh.rb
new file mode 100644
index 0000000..c71fa9a
--- /dev/null
+++ b/contrib/homebrew/slosh.rb
@@ -0,0 +1,86 @@
+# slosh — Homebrew formula (a *formula*, not a cask: casks are for prebuilt
+# .app/.pkg/.dmg drops, formulae are for things brew builds and puts in bin).
+#
+# This file is the source of truth. `contrib/brew-release` rewrites the three
+# stanzas below (url/version/sha256) for a new commit or tag and copies the
+# result into the tap repo, which is what users actually tap:
+#
+# brew tap sl0p/slosh https://git.sl0p.foo/homebrew-slosh.git
+# brew install slosh
+#
+# Try it without publishing anything:
+#
+# brew install --build-from-source contrib/homebrew/slosh.rb
+class Slosh < Formula
+ desc "Terminal multiplexer with detachable sessions and programmable pane chrome"
+ homepage "https://sl0p.foo"
+ # cgit serves a deterministic snapshot per ref: same ref, same bytes, so the
+ # sha256 below stays honest. Pinned to a full commit id rather than a branch,
+ # because a branch tarball changes under you and brew would keep the stale one.
+ url "https://git.sl0p.foo/slosh.git/snapshot/slosh-b744a90859bf248bfbea97f990668301b5fa9537.tar.gz"
+ version "0.1.0-185-b744a90"
+ sha256 "facafd86fc656a1c4fd925faa90b56b6932f7fb816bcdf3a1c55f2f43cb91c10"
+ head "https://git.sl0p.foo/slosh.git", branch: "master"
+
+ # zig is the whole toolchain: it compiles the C, builds the vendored
+ # libghostty-vt, and links it statically. Nothing is needed at runtime, which
+ # is why there is no `depends_on` without `=> :build`.
+ depends_on "zig" => :build
+
+ def install
+ # zig caches in ~/.cache/zig and ~/.zig-cache by default. Under brew that is
+ # either a sandbox denial or pollution of the user's real cache, so both
+ # caches go into the build directory and die with it.
+ ENV["ZIG_GLOBAL_CACHE_DIR"] = buildpath/".zig-global-cache"
+ ENV["ZIG_LOCAL_CACHE_DIR"] = buildpath/".zig-local-cache"
+
+ args = ["ZIG=#{formula_opt_bin("zig")}/zig"]
+ # In a snapshot tarball there is no .git, so the Makefile's `git describe`
+ # would bake in "unknown" and `slosh --version` could not tell you which
+ # build you are looking at. Hand it the commit the tarball came from.
+ args << "GITDESC=#{version.to_s.split("-").last}" unless build.head?
+
+ system "make", "vendor", *args
+ system "make", "all", *args
+
+ # Deliberately not `make install`: the formula must be able to build any
+ # commit, including ones older than that target, and the layout is three
+ # lines here. `make install PREFIX=...` exists for everyone not using brew
+ # and puts the same files in the same places.
+ bin.install "build/slosh"
+ pkgshare.install "config/config.kdl", "config/example.layout"
+ %w[themes chrome shaders].each do |d|
+ pkgshare.install Dir["contrib/#{d}"] if File.directory?("contrib/#{d}")
+ end
+ doc.install "README.md"
+ doc.install Dir["docs/*.md"]
+ end
+
+ def caveats
+ <<~EOS
+ Example configuration, themes, chrome and shaders are installed in:
+ #{opt_share}/slosh
+
+ Start from the shipped config:
+ mkdir -p ~/.config/slosh && cp #{opt_share}/slosh/config.kdl ~/.config/slosh/
+
+ Then: slosh (attach to session "main", creating it if needed)
+ C-a ? (the keys) C-a p (every command by name)
+ EOS
+ end
+
+ test do
+ # The binary knows its own version, and it is the version brew installed.
+ assert_match "slosh 0.1.0", shell_output("#{bin}/slosh --version")
+
+ # The configuration we ship must parse with the binary we ship.
+ assert_match "ok", shell_output("#{bin}/slosh --check #{share}/slosh/config.kdl")
+
+ # And it composes a real screen headlessly: no tty, no session, no socket.
+ # This exercises the vendored terminal core, the layout and the renderer.
+ screen = shell_output("#{bin}/slosh --headless --cols 40 --rows 12 -- " \
+ "/bin/sh -c 'echo hello-brew'")
+ assert_match "hello-brew", screen
+ assert_match "pane 1/1", screen
+ end
+end