<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/.githooks, branch docs-new</title>
<subtitle>an opinionated terminal multiplexer in C with programmable pane chrome</subtitle>
<id>https://git.sl0p.foo/slosh.git/atom/.githooks?h=docs-new</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/.githooks?h=docs-new'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-08-28T13:44:01Z</updated>
<entry>
<title>release: one command to bump, commit and tag</title>
<updated>2026-08-28T13:44:01Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-28T13:44:01Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=79df05ec1a3c38751956e8f6b122333ed3737efc'/>
<id>urn:sha1:79df05ec1a3c38751956e8f6b122333ed3737efc</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>sl0ppty: the make tools the last commit described</title>
<updated>2026-08-17T12:52:14Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-17T12:52:14Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=c9f357d0a138bb77ec344adb3de3ff0889796c51'/>
<id>urn:sha1:c9f357d0a138bb77ec344adb3de3ff0889796c51</id>
<content type='text'>
The previous commit shipped the documentation for `make tools` and the `.gitignore`
line for the venv it creates, and not the target itself. A `git reset --hard`,
undoing a throwaway probe commit, took the working tree with it -- which is what
`--hard` does. The `.gitignore` line was noticed missing and rewritten; the three
files it was standing next to were not, so `docs/design.md` has been describing a
target that did not exist. This is that target.

`ruff` is not a system package on most machines, and `pip install --user ruff` is
refused outright on a distro python (PEP 668):

    $ make tools
      ruff 0.16.3  in .venv
      survives make clean; rm -rf .venv to start over

clang-format stays a system package, because everywhere it is one; `make tools`
says so when it is missing rather than pretending it handled it.

**The venv is looked in before `$PATH`, and that order is the point.** `ruff.toml`
now pins the version with ruff's own `required-version`, so a ruff of the wrong one
refuses to run and names both versions. Preferring `$PATH` would then strand
somebody with 0.14 from their package manager and 0.16.3 sitting in the `.venv` the
repo just built them -- refused by the tool, with the fix on disk and nothing
looking at it. Tested with exactly that, for `make fmt-check` and for the hook.

The pin exists because a formatter whose output moves between releases turns
"formatted" into "formatted by whoever committed last", and the diff lands on the
next person to touch the file. Bumping it is one line in `ruff.toml`, one in the
Makefile, and a reformat commit -- the honest cost of a new version.

The hook resolves ruff the same way, off `git rev-parse --show-toplevel` so it works
from a subdirectory, and its message when ruff is missing now says `make tools`.

Verified from a deleted `.venv`: `make fmt` stops with `ruff not found. run: make
tools`; after `make tools` both `fmt` and `fmt-check` work with nothing on `$PATH`;
`make fmt` over the whole tree changes nothing, so the pinned version agrees with
what is committed; the hook formats a staged Python file through the venv, and still
does with 0.14.0 ahead of it on `$PATH`.

The hook was tested by running `.githooks/pre-commit` directly against a staged
file. The obvious way -- a real probe commit -- needs the working tree clean to be
meaningful, and getting there is what caused the mess this commit is repairing.
</content>
</entry>
<entry>
<title>sl0ppty: ruff formats the Python, and sorts its imports</title>
<updated>2026-08-17T12:35:59Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-17T12:35:59Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b1fe06633d68ec611d0b1269f4c95db82ac456f5'/>
<id>urn:sha1:b1fe06633d68ec611d0b1269f4c95db82ac456f5</id>
<content type='text'>
The other half of the pre-commit hook. C goes through clang-format, Python through
ruff: `ruff check --select I --fix` to sort the imports, then `ruff format` to lay
them out. `make fmt` does both, `make fmt-check` reports, `make hooks` installs it.

ruff rather than black because it is one static binary instead of a Python
dependency chain, and fast enough that a hook is free. Its formatter is
black-compatible, so this is the ecosystem's formatting rather than a dialect --
measured at 9,935 changed lines against black's 9,953, which is the same answer.

**Left entirely at the defaults**, which is the opposite of what `.clang-format`
does. That file argues with LLVM in five places because the C here was
hand-formatted into a house style worth keeping. The Python was not, and a
formatter of this kind is only worth having when there is nothing left to argue
about: every knob turned is an argument somebody has to have again later. 88
columns, double quotes, ruff's own line breaking.

**Import sorting is on, and C include sorting stays off.** Not an inconsistency: a
C include order can be load-bearing -- `_GNU_SOURCE` ahead of the system headers,
and a grouping somebody wrote by hand -- while Python import order is not. Where it
genuinely is, a statement between imports ends the block ruff will sort, and the
test files lean on exactly that:

    import sys
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    from harness import Session, check, report

`from harness import` cannot be hoisted above the line that makes it importable,
because the `sys.path.insert` between them is a barrier. Verified before touching
anything, since getting it wrong would have broken every test in the suite at once.

Two details in the hook worth naming. `--select I` is passed on the command line as
well as set in `ruff.toml`, so a stray `select` in somebody's own config cannot
turn a formatting hook into a linter that refuses a commit over an unused variable.
And the two tools are named together when either is missing, so setting up a
machine installs both at once instead of finding the second one on the next commit.

`set -e` and `[ -n "$c_files" ] &amp;&amp; clang-format ...` do not mix: when the list is
empty the AND-list returns non-zero and the shell exits, so a Python-only commit
would have aborted with no message at all. They are `if` blocks. Found by testing
the case rather than by reading it.

Verified in a scratch clone, seven paths: Python alone, C alone, both in one
commit, imports sorted with the `sys.path` barrier intact, a `contrib/` script that
is Python with no extension on it, a partly staged Python file refused with `HEAD`
unchanged, and a commit with no code in it drawing no comment.
</content>
</entry>
<entry>
<title>sl0ppty: clang-format, and a hook that runs it on what you staged</title>
<updated>2026-08-17T12:21:40Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-17T12:21:40Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=1f52c39ab0875162be013e6d78720a780df1d889'/>
<id>urn:sha1:1f52c39ab0875162be013e6d78720a780df1d889</id>
<content type='text'>
`make hooks` points `core.hooksPath` at `.githooks`, and from then on committing C
formats it first, so what lands in git is already formatted. `make fmt` does the
whole tree, `make fmt-check` only reports. The reformat itself is the next commit.

**The config was chosen by measuring, not by taste.** LLVM is the base because it
is what this code was already written in by hand -- two spaces, 80 columns,
`char *p`, braces attached -- but LLVM alone rewrote 38% of the lines. Three
disagreements account for most of that, and each is a real house idiom rather than
an accident:

- `if (!p) return false;` on one line, which is the guard idiom throughout. LLVM
  splits every one of them in two, two thirds of the churn on its own.
  `WithoutElse`, so an `if` that has an `else` still opens up: a one-line `if`
  hiding an `else` is the arrangement that hides a bug.
- Comments are not reflowed. They are prose, wrapped by hand, and the breaks
  sometimes carry meaning -- a paragraph, a list, a diagram of a pane. A comment
  that runs long is a thing for a person to fix, not for a formatter to even out.
- Include order is left alone. It is grouped by hand here and is not alphabetical
  within a group; sorting it would be churn at best and, at worst, a build that
  depends on an order nobody wrote down.

Those three, plus one-line `case` labels and loops, bring the reformat from 38% to
17% -- and what is left is alignment and the hand-packed tables, which is the part
a formatter is *for*.

The vendored terminal core is excluded everywhere: `.githooks/pre-commit`, `make
fmt` and `make fmt-check` all filter `^vendor/`. It is pinned by commit (D12) and
reformatting it would turn every future re-vendor into a merge.

**The hook refuses a partly staged file rather than widening the commit.**
`clang-format -i` rewrites the working tree, so re-staging afterwards would commit
work somebody deliberately left out with `git add -p`. It says which files and what
to do instead. It also says when it formatted something, because a hook that edits
your commit silently is a hook people learn to distrust, and it fails with the
package name when `clang-format` is missing rather than letting unformatted C
through the one check meant to stop it. `--no-verify` still escapes.

Verified in a scratch clone, all four paths: a badly formatted file is committed
formatted; a partly staged one is refused with `HEAD` unchanged and the index
untouched; a commit with no C in it draws no comment; and `--no-verify` puts
unformatted code in, which is what asking for it means.
</content>
</entry>
</feed>
