summary refs log tree commit diff
path: root/www
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--www/causal.agency/.gitignore3
-rw-r--r--www/causal.agency/Makefile20
-rw-r--r--www/causal.agency/index.770
-rw-r--r--www/causal.agency/style.css23
-rw-r--r--www/git.causal.agency/.gitignore6
-rw-r--r--www/git.causal.agency/Makefile20
-rw-r--r--www/git.causal.agency/about-filter.sh15
-rw-r--r--www/git.causal.agency/cgit/.gitignore13
-rw-r--r--www/git.causal.agency/cgit/.mailmap (renamed from .mailmap)0
-rw-r--r--www/git.causal.agency/cgit/AUTHORS (renamed from AUTHORS)0
-rw-r--r--www/git.causal.agency/cgit/COPYING (renamed from COPYING)0
-rw-r--r--www/git.causal.agency/cgit/Makefile (renamed from Makefile)4
-rw-r--r--www/git.causal.agency/cgit/README (renamed from README)13
-rw-r--r--www/git.causal.agency/cgit/cache.c (renamed from cache.c)7
-rw-r--r--www/git.causal.agency/cgit/cache.h (renamed from cache.h)0
-rw-r--r--www/git.causal.agency/cgit/cgit.c (renamed from cgit.c)9
-rw-r--r--www/git.causal.agency/cgit/cgit.css (renamed from cgit.css)28
-rw-r--r--www/git.causal.agency/cgit/cgit.h (renamed from cgit.h)1
-rw-r--r--www/git.causal.agency/cgit/cgit.mk (renamed from cgit.mk)27
-rw-r--r--www/git.causal.agency/cgit/cgit.png (renamed from cgit.png)bin1366 -> 1366 bytes
-rw-r--r--www/git.causal.agency/cgit/cgitrc.5.txt (renamed from cgitrc.5.txt)34
-rw-r--r--www/git.causal.agency/cgit/cmd.c (renamed from cmd.c)0
-rw-r--r--www/git.causal.agency/cgit/cmd.h (renamed from cmd.h)0
-rw-r--r--www/git.causal.agency/cgit/configfile.c (renamed from configfile.c)0
-rw-r--r--www/git.causal.agency/cgit/configfile.h (renamed from configfile.h)0
-rwxr-xr-xwww/git.causal.agency/cgit/contrib/hooks/post-receive.agefile (renamed from contrib/hooks/post-receive.agefile)0
-rw-r--r--www/git.causal.agency/cgit/favicon.ico (renamed from favicon.ico)bin1078 -> 1078 bytes
-rw-r--r--www/git.causal.agency/cgit/filter.c222
-rwxr-xr-xwww/git.causal.agency/cgit/filters/about-formatting.sh (renamed from filters/about-formatting.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/filters/commit-links.sh (renamed from filters/commit-links.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/filters/email-gravatar.py (renamed from filters/email-gravatar.py)3
-rwxr-xr-xwww/git.causal.agency/cgit/filters/html-converters/man2html (renamed from filters/html-converters/man2html)0
-rwxr-xr-xwww/git.causal.agency/cgit/filters/html-converters/md2html (renamed from filters/html-converters/md2html)0
-rwxr-xr-xwww/git.causal.agency/cgit/filters/html-converters/rst2html (renamed from filters/html-converters/rst2html)0
-rwxr-xr-xwww/git.causal.agency/cgit/filters/html-converters/txt2html (renamed from filters/html-converters/txt2html)0
-rwxr-xr-xwww/git.causal.agency/cgit/filters/syntax-highlighting.py (renamed from filters/syntax-highlighting.py)0
-rwxr-xr-xwww/git.causal.agency/cgit/filters/syntax-highlighting.sh (renamed from filters/syntax-highlighting.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/gen-version.sh (renamed from gen-version.sh)0
-rw-r--r--www/git.causal.agency/cgit/html.c (renamed from html.c)2
-rw-r--r--www/git.causal.agency/cgit/html.h (renamed from html.h)0
-rw-r--r--www/git.causal.agency/cgit/parsing.c (renamed from parsing.c)0
-rw-r--r--www/git.causal.agency/cgit/robots.txt (renamed from robots.txt)1
-rw-r--r--www/git.causal.agency/cgit/scan-tree.c (renamed from scan-tree.c)0
-rw-r--r--www/git.causal.agency/cgit/scan-tree.h (renamed from scan-tree.h)0
-rw-r--r--www/git.causal.agency/cgit/shared.c (renamed from shared.c)0
-rw-r--r--www/git.causal.agency/cgit/tests/.gitignore (renamed from tests/.gitignore)0
-rw-r--r--www/git.causal.agency/cgit/tests/Makefile (renamed from tests/Makefile)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/filters/dump.sh (renamed from tests/filters/dump.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/setup.sh (renamed from tests/setup.sh)19
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0001-validate-git-versions.sh (renamed from tests/t0001-validate-git-versions.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0010-validate-html.sh (renamed from tests/t0010-validate-html.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0020-validate-cache.sh (renamed from tests/t0020-validate-cache.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0101-index.sh (renamed from tests/t0101-index.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0102-summary.sh (renamed from tests/t0102-summary.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0103-log.sh (renamed from tests/t0103-log.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0104-tree.sh (renamed from tests/t0104-tree.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0105-commit.sh (renamed from tests/t0105-commit.sh)6
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0106-diff.sh (renamed from tests/t0106-diff.sh)4
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0107-snapshot.sh (renamed from tests/t0107-snapshot.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0108-patch.sh (renamed from tests/t0108-patch.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0109-gitconfig.sh (renamed from tests/t0109-gitconfig.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0110-rawdiff.sh (renamed from tests/t0110-rawdiff.sh)0
-rwxr-xr-xwww/git.causal.agency/cgit/tests/t0111-filter.sh (renamed from tests/t0111-filter.sh)3
-rwxr-xr-xwww/git.causal.agency/cgit/tests/valgrind/bin/cgit (renamed from tests/valgrind/bin/cgit)0
-rw-r--r--www/git.causal.agency/cgit/ui-atom.c (renamed from ui-atom.c)0
-rw-r--r--www/git.causal.agency/cgit/ui-atom.h (renamed from ui-atom.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-blame.c (renamed from ui-blame.c)4
-rw-r--r--www/git.causal.agency/cgit/ui-blame.h (renamed from ui-blame.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-blob.c (renamed from ui-blob.c)0
-rw-r--r--www/git.causal.agency/cgit/ui-blob.h (renamed from ui-blob.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-clone.c (renamed from ui-clone.c)0
-rw-r--r--www/git.causal.agency/cgit/ui-clone.h (renamed from ui-clone.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-commit.c (renamed from ui-commit.c)7
-rw-r--r--www/git.causal.agency/cgit/ui-commit.h (renamed from ui-commit.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-diff.c (renamed from ui-diff.c)30
-rw-r--r--www/git.causal.agency/cgit/ui-diff.h (renamed from ui-diff.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-log.c (renamed from ui-log.c)4
-rw-r--r--www/git.causal.agency/cgit/ui-log.h (renamed from ui-log.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-patch.c (renamed from ui-patch.c)0
-rw-r--r--www/git.causal.agency/cgit/ui-patch.h (renamed from ui-patch.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-plain.c (renamed from ui-plain.c)0
-rw-r--r--www/git.causal.agency/cgit/ui-plain.h (renamed from ui-plain.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-refs.c (renamed from ui-refs.c)0
-rw-r--r--www/git.causal.agency/cgit/ui-refs.h (renamed from ui-refs.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-repolist.c (renamed from ui-repolist.c)4
-rw-r--r--www/git.causal.agency/cgit/ui-repolist.h (renamed from ui-repolist.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-shared.c (renamed from ui-shared.c)44
-rw-r--r--www/git.causal.agency/cgit/ui-shared.h (renamed from ui-shared.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-snapshot.c (renamed from ui-snapshot.c)3
-rw-r--r--www/git.causal.agency/cgit/ui-snapshot.h (renamed from ui-snapshot.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-ssdiff.c (renamed from ui-ssdiff.c)0
-rw-r--r--www/git.causal.agency/cgit/ui-ssdiff.h (renamed from ui-ssdiff.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-stats.c (renamed from ui-stats.c)0
-rw-r--r--www/git.causal.agency/cgit/ui-stats.h (renamed from ui-stats.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-summary.c (renamed from ui-summary.c)0
-rw-r--r--www/git.causal.agency/cgit/ui-summary.h (renamed from ui-summary.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-tag.c (renamed from ui-tag.c)4
-rw-r--r--www/git.causal.agency/cgit/ui-tag.h (renamed from ui-tag.h)0
-rw-r--r--www/git.causal.agency/cgit/ui-tree.c (renamed from ui-tree.c)40
-rw-r--r--www/git.causal.agency/cgit/ui-tree.h (renamed from ui-tree.h)0
-rw-r--r--www/git.causal.agency/cgitrc29
-rw-r--r--www/git.causal.agency/custom.css86
-rw-r--r--www/git.causal.agency/owner-filter.sh6
-rw-r--r--www/git.causal.agency/source-filter.sh25
-rw-r--r--www/temp.causal.agency/.gitignore1
-rw-r--r--www/temp.causal.agency/Makefile16
-rw-r--r--www/temp.causal.agency/up.c156
-rw-r--r--www/text.causal.agency/.gitignore3
-rw-r--r--www/text.causal.agency/001-make.7159
-rw-r--r--www/text.causal.agency/002-writing-mdoc.7138
-rw-r--r--www/text.causal.agency/003-pleasant-c.7120
-rw-r--r--www/text.causal.agency/004-uloc.764
-rw-r--r--www/text.causal.agency/005-testing-c.773
-rw-r--r--www/text.causal.agency/006-some-libs.796
-rw-r--r--www/text.causal.agency/007-cgit-setup.7271
-rw-r--r--www/text.causal.agency/008-how-irc.7193
-rw-r--r--www/text.causal.agency/009-casual-update.7127
-rw-r--r--www/text.causal.agency/010-irc-suite.7409
-rw-r--r--www/text.causal.agency/011-libretls.7220
-rw-r--r--www/text.causal.agency/012-inability.739
-rw-r--r--www/text.causal.agency/013-hot-tips.7156
-rw-r--r--www/text.causal.agency/014-using-vi.7135
-rw-r--r--www/text.causal.agency/015-reusing-tags.7155
-rw-r--r--www/text.causal.agency/016-using-openbsd.7505
-rw-r--r--www/text.causal.agency/017-unpasswords.7153
-rw-r--r--www/text.causal.agency/018-operating-systems.786
-rw-r--r--www/text.causal.agency/019-mailing-list.7287
-rw-r--r--www/text.causal.agency/020-c-style.7172
-rw-r--r--www/text.causal.agency/021-time-machine.7144
-rw-r--r--www/text.causal.agency/022-swans-are-dead.7164
-rw-r--r--www/text.causal.agency/Makefile47
-rw-r--r--www/text.causal.agency/feed.sh55
-rw-r--r--www/text.causal.agency/igp.149
-rw-r--r--www/text.causal.agency/igp.c143
134 files changed, 5003 insertions, 172 deletions
diff --git a/www/causal.agency/.gitignore b/www/causal.agency/.gitignore
new file mode 100644
index 00000000..7935a3c1
--- /dev/null
+++ b/www/causal.agency/.gitignore
@@ -0,0 +1,3 @@
+*.html
+scheme.css
+scheme.png
diff --git a/www/causal.agency/Makefile b/www/causal.agency/Makefile
new file mode 100644
index 00000000..95f52120
--- /dev/null
+++ b/www/causal.agency/Makefile
@@ -0,0 +1,20 @@
+WEBROOT = /usr/local/www/causal.agency
+
+FILES = index.html style.css scheme.css scheme.png
+
+all: ${FILES}
+
+index.html: index.7
+	mandoc -T html -O style=style.css index.7 > index.html
+
+scheme.css:
+	scheme -st > scheme.css
+
+scheme.png:
+	scheme -g > scheme.png
+
+install: ${FILES}
+	install -C -m 644 ${FILES} ${WEBROOT}
+
+clean:
+	rm -f index.html scheme.css scheme.png
diff --git a/www/causal.agency/index.7 b/www/causal.agency/index.7
new file mode 100644
index 00000000..489b0b53
--- /dev/null
+++ b/www/causal.agency/index.7
@@ -0,0 +1,70 @@
+.Dd May 19, 2021
+.Dt CAUSAL.AGENCY 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm june
+.Nd computer enthusiast!!
+.
+.Sh SYNOPSIS
+.Nm mail
+.Mt june@causal.agency
+.Nm
+in
+.Li #ascii.town
+on tilde.chat
+.
+.Sh DESCRIPTION
+I make mostly IRC software in C.
+I like
+.Fx
+and
+.Ox
+but also the GPL.
+I want to learn to talk to strangers
+and seek out as much magic as I can.
+.
+.Pp
+.Lk https://git.causal.agency code
+\(em
+.Lk https://text.causal.agency words
+.Pq Lk gopher://text.causal.agency gopher
+\(em
+.Lk /list/ mailist
+\(em
+.Lk https://liberapay.com/june/ liberapay
+.
+.Pp
+These are some things I've done:
+.Bl -tag -width Ds
+.It Lk https://git.causal.agency/pounce/about pounce
+a multi-client-first IRC bouncer
+.It Lk https://git.causal.agency/catgirl/about catgirl
+a cosy IRC client
+.It Lk https://git.causal.agency/litterbox/about litterbox
+a full-text search IRC logger
+.It Lk https://git.causal.agency/scooper/about scooper
+a web interface for litterbox
+.It Lk https://git.causal.agency/catsit/about catsit
+a process supervisor
+.It Lk https://git.causal.agency/imbox/about "imbox & git-fetch-email"
+a tool to pull patches out of IMAP
+.It Lk https://git.causal.agency/bubger/about bubger
+a mailing list archive generator for IMAP
+.It Lk https://git.causal.agency/notemap/about notemap
+a tool to mirror text files to IMAP notes
+.It Lk https://ascii.town/explore.html torus@ascii.town
+a collaborative ASCII art project
+.It Lk ssh://play@ascii.town play@ascii.town
+a 2048 and snake game over
+.Xr ssh 1
+.It Lk https://git.causal.agency/cards/about cards
+a
+.Pa CARDS.DLL
+loader for SDL
+.It Lk scheme.png scheme
+an earthy terminal colour scheme
+.El
+.
+.Sh SEE ALSO
+.Lk /bin/ bin
diff --git a/www/causal.agency/style.css b/www/causal.agency/style.css
new file mode 100644
index 00000000..368d8da1
--- /dev/null
+++ b/www/causal.agency/style.css
@@ -0,0 +1,23 @@
+@import url("scheme.css");
+
+table.head, table.foot { width: 100%; }
+td.head-rtitle, td.foot-os { text-align: right; }
+td.head-vol { text-align: center; }
+div.Pp { margin: 1ex 0ex; }
+div.Nd, div.Bf, div.Op { display: inline; }
+span.Pa, span.Ad { font-style: italic; }
+span.Ms { font-weight: bold; }
+dl.Bl-diag > dt { font-weight: bold; }
+code.Nm, code.Fl, code.Cm, code.Ic, code.In, code.Fd, code.Fn,
+code.Cd { font-weight: bold; font-family: inherit; }
+
+html { font-family: monospace; line-height: 1.25em; }
+body { max-width: 80ch; margin: 1em auto; padding: 0 1ch; }
+table { border-collapse: collapse; }
+table.Nm code.Nm { padding-right: 1ch; }
+table.foot { margin-top: 1em; }
+
+html { background-color: var(--ansi16); color: var(--ansi17); }
+a { color: var(--ansi4); }
+a:visited { color: var(--ansi5); }
+a.permalink { color: var(--ansi3); text-decoration: none; }
diff --git a/www/git.causal.agency/.gitignore b/www/git.causal.agency/.gitignore
new file mode 100644
index 00000000..a0ae074c
--- /dev/null
+++ b/www/git.causal.agency/.gitignore
@@ -0,0 +1,6 @@
+about-filter
+hilex
+htagml
+mtags
+owner-filter
+source-filter
diff --git a/www/git.causal.agency/Makefile b/www/git.causal.agency/Makefile
new file mode 100644
index 00000000..638c21e7
--- /dev/null
+++ b/www/git.causal.agency/Makefile
@@ -0,0 +1,20 @@
+ETC = /usr/local/etc
+WWW = /usr/local/www/cgit
+LIBEXEC = /usr/local/libexec
+
+BIN = ../../bin
+BINS = about-filter source-filter owner-filter hilex htagml mtags
+
+all: ${BINS}
+
+install: cgitrc custom.css ${BINS}
+	install -m 644 cgitrc ${ETC}
+	install -m 644 custom.css ${WWW}
+	install ${BINS} ${LIBEXEC}
+
+hilex htagml mtags::
+	${MAKE} -C ${BIN} $@
+	ln -f ${BIN}/$@ $@
+
+clean:
+	rm -f ${BINS}
diff --git a/www/git.causal.agency/about-filter.sh b/www/git.causal.agency/about-filter.sh
new file mode 100644
index 00000000..2ff645e2
--- /dev/null
+++ b/www/git.causal.agency/about-filter.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+options=fragment,man=%N.%S,includes=../tree/%I
+
+case "$1" in
+	(README.[1-9])
+		exec /usr/bin/mandoc -T html -O $options
+		;;
+	(*.[1-9])
+		exec /usr/bin/mandoc -T html -O $options,toc
+		;;
+	(*)
+		exec /usr/local/libexec/hilex -l text -f html -o pre
+		;;
+esac
diff --git a/www/git.causal.agency/cgit/.gitignore b/www/git.causal.agency/cgit/.gitignore
new file mode 100644
index 00000000..bca98fcf
--- /dev/null
+++ b/www/git.causal.agency/cgit/.gitignore
@@ -0,0 +1,13 @@
+# Files I don't care to see in git-status/commit
+/cgit
+/git
+cgit.conf
+CGIT-CFLAGS
+VERSION
+cgitrc.5
+cgitrc.5.fo
+cgitrc.5.html
+cgitrc.5.pdf
+cgitrc.5.xml
+*.o
+*.d
diff --git a/.mailmap b/www/git.causal.agency/cgit/.mailmap
index 03b54796..03b54796 100644
--- a/.mailmap
+++ b/www/git.causal.agency/cgit/.mailmap
diff --git a/AUTHORS b/www/git.causal.agency/cgit/AUTHORS
index 031de338..031de338 100644
--- a/AUTHORS
+++ b/www/git.causal.agency/cgit/AUTHORS
diff --git a/COPYING b/www/git.causal.agency/cgit/COPYING
index d159169d..d159169d 100644
--- a/COPYING
+++ b/www/git.causal.agency/cgit/COPYING
diff --git a/Makefile b/www/git.causal.agency/cgit/Makefile
index d13c5bd1..358a3c8e 100644
--- a/Makefile
+++ b/www/git.causal.agency/cgit/Makefile
@@ -15,7 +15,7 @@ pdfdir = $(docdir)
 mandir = $(prefix)/share/man
 SHA1_HEADER = <openssl/sha.h>
 GIT_VER = 2.32.0
-GIT_URL = https://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.xz
+GIT_URL = https://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.gz
 INSTALL = install
 COPYTREE = cp -r
 MAN5_TXT = $(wildcard *.5.txt)
@@ -157,7 +157,7 @@ clean-doc:
 	$(RM) cgitrc.5 cgitrc.5.html cgitrc.5.pdf cgitrc.5.xml cgitrc.5.fo
 
 get-git:
-	curl -L $(GIT_URL) | tar -xJf - && rm -rf git && mv git-$(GIT_VER) git
+	curl -L $(GIT_URL) | tar -xzf - && rm -rf git && mv git-$(GIT_VER) git
 
 tags:
 	$(QUIET_TAGS)find . -name '*.[ch]' | xargs ctags
diff --git a/README b/www/git.causal.agency/cgit/README
index 7a6b4a40..371cf21f 100644
--- a/README
+++ b/www/git.causal.agency/cgit/README
@@ -32,18 +32,6 @@ This will install `cgit.cgi` and `cgit.css` into `/var/www/htdocs/cgit`. You
 can configure this location (and a few other things) by providing a `cgit.conf`
 file (see the Makefile for details).
 
-If you'd like to compile without Lua support, you may use:
-
-    $ make NO_LUA=1
-
-And if you'd like to specify a Lua implementation, you may use:
-
-    $ make LUA_PKGCONFIG=lua5.1
-
-If this is not specified, the Lua implementation will be auto-detected,
-preferring LuaJIT if many are present. Acceptable values are generally "lua",
-"luajit", "lua5.1", and "lua5.2".
-
 
 Dependencies
 ------------
@@ -51,7 +39,6 @@ Dependencies
 * libzip
 * libcrypto (OpenSSL)
 * libssl (OpenSSL)
-* optional: luajit or lua, most reliably used when pkg-config is available
 
 Apache configuration
 --------------------
diff --git a/cache.c b/www/git.causal.agency/cgit/cache.c
index 55199e8f..578b73b0 100644
--- a/cache.c
+++ b/www/git.causal.agency/cgit/cache.c
@@ -265,6 +265,13 @@ static int process_slot(struct cache_slot *slot)
 {
 	int err;
 
+	/*
+	 * Make sure any buffered data is flushed before we redirect,
+	 * do sendfile(2) or write(2)
+	 */
+	if (fflush(stdout))
+		return errno;
+
 	err = open_slot(slot);
 	if (!err && slot->match) {
 		if (is_expired(slot)) {
diff --git a/cache.h b/www/git.causal.agency/cgit/cache.h
index 470da4fc..470da4fc 100644
--- a/cache.h
+++ b/www/git.causal.agency/cgit/cache.h
diff --git a/cgit.c b/www/git.causal.agency/cgit/cgit.c
index 08d81a1d..a86970de 100644
--- a/cgit.c
+++ b/www/git.causal.agency/cgit/cgit.c
@@ -674,7 +674,7 @@ static inline void authenticate_post(void)
 		len = MAX_AUTHENTICATION_POST_BYTES;
 	if ((len = read(STDIN_FILENO, buffer, len)) < 0)
 		die_errno("Could not read POST from stdin");
-	if (write(STDOUT_FILENO, buffer, len) < 0)
+	if (fwrite(buffer, 1, len, stdout) < len)
 		die_errno("Could not write POST to stdout");
 	cgit_close_filter(ctx.cfg.auth_filter);
 	exit(0);
@@ -964,12 +964,6 @@ static void cgit_parse_args(int argc, const char **argv)
 	for (i = 1; i < argc; i++) {
 		if (!strcmp(argv[i], "--version")) {
 			printf("CGit %s | https://git.zx2c4.com/cgit/\n\nCompiled in features:\n", CGIT_VERSION);
-#ifdef NO_LUA
-			printf("[-] ");
-#else
-			printf("[+] ");
-#endif
-			printf("Lua scripting\n");
 #ifndef HAVE_LINUX_SENDFILE
 			printf("[-] ");
 #else
@@ -1051,7 +1045,6 @@ int cmd_main(int argc, const char **argv)
 	const char *path;
 	int err, ttl;
 
-	cgit_init_filters();
 	atexit(cgit_cleanup_filters);
 
 	prepare_context();
diff --git a/cgit.css b/www/git.causal.agency/cgit/cgit.css
index dfa144d0..f3dbb7a9 100644
--- a/cgit.css
+++ b/www/git.causal.agency/cgit/cgit.css
@@ -75,7 +75,7 @@ div#cgit table.tabs td {
 }
 
 div#cgit table.tabs td a {
-	padding: 2px 0.75em;
+	padding: 2px 0.25em;
 	color: #777;
 	font-size: 110%;
 }
@@ -437,11 +437,6 @@ div#cgit div.commit-subject {
 	padding: 0em;
 }
 
-div#cgit div.commit-msg {
-	white-space: pre;
-	font-family: monospace;
-}
-
 div#cgit div.notes-header {
 	font-weight: bold;
 	padding-top: 1.5em;
@@ -538,26 +533,20 @@ div#cgit table.diff {
 	width: 100%;
 }
 
-div#cgit table.diff td {
-	font-family: monospace;
-	white-space: pre;
-}
-
-div#cgit table.diff td div.head {
+div#cgit table.diff td span.head {
 	font-weight: bold;
-	margin-top: 1em;
 	color: black;
 }
 
-div#cgit table.diff td div.hunk {
+div#cgit table.diff td span.hunk {
 	color: #009;
 }
 
-div#cgit table.diff td div.add {
+div#cgit table.diff td span.add {
 	color: green;
 }
 
-div#cgit table.diff td div.del {
+div#cgit table.diff td span.del {
 	color: red;
 }
 
@@ -581,7 +570,6 @@ div#cgit table.list td.reposection {
 
 div#cgit a.button {
 	font-size: 80%;
-	padding: 0em 0.5em;
 }
 
 div#cgit a.primary {
@@ -671,7 +659,6 @@ div#cgit div.footer a:hover {
 
 div#cgit a.branch-deco {
 	color: #000;
-	margin: 0px 0.5em;
 	padding: 0px 0.25em;
 	background-color: #88ff88;
 	border: solid 1px #007700;
@@ -679,7 +666,6 @@ div#cgit a.branch-deco {
 
 div#cgit a.tag-deco {
 	color: #000;
-	margin: 0px 0.5em;
 	padding: 0px 0.25em;
 	background-color: #ffff88;
 	border: solid 1px #777700;
@@ -687,7 +673,6 @@ div#cgit a.tag-deco {
 
 div#cgit a.tag-annotated-deco {
 	color: #000;
-	margin: 0px 0.5em;
 	padding: 0px 0.25em;
 	background-color: #ffcc88;
 	border: solid 1px #777700;
@@ -695,7 +680,6 @@ div#cgit a.tag-annotated-deco {
 
 div#cgit a.remote-deco {
 	color: #000;
-	margin: 0px 0.5em;
 	padding: 0px 0.25em;
 	background-color: #ccccff;
 	border: solid 1px #000077;
@@ -703,7 +687,6 @@ div#cgit a.remote-deco {
 
 div#cgit a.deco {
 	color: #000;
-	margin: 0px 0.5em;
 	padding: 0px 0.25em;
 	background-color: #ff8888;
 	border: solid 1px #770000;
@@ -714,7 +697,6 @@ div#cgit div.commit-subject a.tag-deco,
 div#cgit div.commit-subject a.tag-annotated-deco,
 div#cgit div.commit-subject a.remote-deco,
 div#cgit div.commit-subject a.deco {
-	margin-left: 1em;
 	font-size: 75%;
 }
 
diff --git a/cgit.h b/www/git.causal.agency/cgit/cgit.h
index 69b5c132..72fcd849 100644
--- a/cgit.h
+++ b/www/git.causal.agency/cgit/cgit.h
@@ -385,7 +385,6 @@ extern void cgit_fprintf_filter(struct cgit_filter *filter, FILE *f, const char
 extern void cgit_exec_filter_init(struct cgit_exec_filter *filter, char *cmd, char **argv);
 extern struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype);
 extern void cgit_cleanup_filters(void);
-extern void cgit_init_filters(void);
 
 extern void cgit_prepare_repo_env(struct cgit_repo * repo);
 
diff --git a/cgit.mk b/www/git.causal.agency/cgit/cgit.mk
index 3fcc1ca3..5b9ed5be 100644
--- a/cgit.mk
+++ b/www/git.causal.agency/cgit/cgit.mk
@@ -27,32 +27,6 @@ ifdef NO_C99_FORMAT
 	CFLAGS += -DNO_C99_FORMAT
 endif
 
-ifdef NO_LUA
-	LUA_MESSAGE := linking without specified Lua support
-	CGIT_CFLAGS += -DNO_LUA
-else
-ifeq ($(LUA_PKGCONFIG),)
-	LUA_PKGCONFIG := $(shell for pc in luajit lua lua5.2 lua5.1; do \
-			$(PKG_CONFIG) --exists $$pc 2>/dev/null && echo $$pc && break; \
-			done)
-	LUA_MODE := autodetected
-else
-	LUA_MODE := specified
-endif
-ifneq ($(LUA_PKGCONFIG),)
-	LUA_MESSAGE := linking with $(LUA_MODE) $(LUA_PKGCONFIG)
-	LUA_LIBS := $(shell $(PKG_CONFIG) --libs $(LUA_PKGCONFIG) 2>/dev/null)
-	LUA_CFLAGS := $(shell $(PKG_CONFIG) --cflags $(LUA_PKGCONFIG) 2>/dev/null)
-	CGIT_LIBS += $(LUA_LIBS)
-	CGIT_CFLAGS += $(LUA_CFLAGS)
-else
-	LUA_MESSAGE := linking without autodetected Lua support
-	NO_LUA := YesPlease
-	CGIT_CFLAGS += -DNO_LUA
-endif
-
-endif
-
 # Add -ldl to linker flags on systems that commonly use GNU libc.
 ifneq (,$(filter $(uname_S),Linux GNU GNU/kFreeBSD))
 	CGIT_LIBS += -ldl
@@ -130,7 +104,6 @@ $(CGIT_OBJS): %.o: %.c GIT-CFLAGS $(CGIT_PREFIX)CGIT-CFLAGS $(missing_dep_dirs)
 	$(QUIET_CC)$(CC) -o $*.o -c $(dep_args) $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $(CGIT_CFLAGS) $<
 
 $(CGIT_PREFIX)cgit: $(CGIT_OBJS) GIT-LDFLAGS $(GITLIBS)
-	@echo 1>&1 "    * $(LUA_MESSAGE)"
 	$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS) $(CGIT_LIBS)
 
 CGIT_SP_OBJS := $(patsubst %.o,%.sp,$(CGIT_OBJS))
diff --git a/cgit.png b/www/git.causal.agency/cgit/cgit.png
index 425528ee..425528ee 100644
--- a/cgit.png
+++ b/www/git.causal.agency/cgit/cgit.png
Binary files differdiff --git a/cgitrc.5.txt b/www/git.causal.agency/cgit/cgitrc.5.txt
index 33a6a8c0..8d663952 100644
--- a/cgitrc.5.txt
+++ b/www/git.causal.agency/cgit/cgitrc.5.txt
@@ -632,37 +632,6 @@ specification with the relevant string; available values are:
 'exec:'::
 	The default "one process per filter" mode.
 
-'lua:'::
-	Executes the script using a built-in Lua interpreter. The script is
-	loaded once per execution of cgit, and may be called multiple times
-	during cgit's lifetime, making it a good choice for repeated filters
-	such as the 'email filter'. It responds to three functions:
-
-	'filter_open(argument1, argument2, argument3, ...)'::
-		This is called upon activation of the filter for a particular
-		set of data.
-	'filter_write(buffer)'::
-		This is called whenever cgit writes data to the webpage.
-	'filter_close()'::
-		This is called when the current filtering operation is
-		completed. It must return an integer value. Usually 0
-		indicates success.
-
-	Additionally, cgit exposes to the Lua the following built-in functions:
-
-	'html(str)'::
-		Writes 'str' to the webpage.
-	'html_txt(str)'::
-		HTML escapes and writes 'str' to the webpage.
-	'html_attr(str)'::
-		HTML escapes for an attribute and writes "str' to the webpage.
-	'html_url_path(str)'::
-		URL escapes for a path and writes 'str' to the webpage.
-	'html_url_arg(str)'::
-		URL escapes for an argument and writes 'str' to the webpage.
-	'html_include(file)'::
-		Includes 'file' in webpage.
-
 
 Parameters are provided to filters as follows.
 
@@ -696,9 +665,6 @@ auth filter::
 	with a 302 redirect, and write to output one or more "Set-Cookie"
 	HTTP headers, each followed by a newline.
 
-	Please see `filters/simple-authentication.lua` for a clear example
-	script that may be modified.
-
 commit filter::
 	This filter is given no arguments. The commit message text that is to
 	be filtered is available on standard input and the filtered text is
diff --git a/cmd.c b/www/git.causal.agency/cgit/cmd.c
index 0eb75b1d..0eb75b1d 100644
--- a/cmd.c
+++ b/www/git.causal.agency/cgit/cmd.c
diff --git a/cmd.h b/www/git.causal.agency/cgit/cmd.h
index 6249b1d8..6249b1d8 100644
--- a/cmd.h
+++ b/www/git.causal.agency/cgit/cmd.h
diff --git a/configfile.c b/www/git.causal.agency/cgit/configfile.c
index e0391091..e0391091 100644
--- a/configfile.c
+++ b/www/git.causal.agency/cgit/configfile.c
diff --git a/configfile.h b/www/git.causal.agency/cgit/configfile.h
index af7ca197..af7ca197 100644
--- a/configfile.h
+++ b/www/git.causal.agency/cgit/configfile.h
diff --git a/contrib/hooks/post-receive.agefile b/www/git.causal.agency/cgit/contrib/hooks/post-receive.agefile
index 2f72ae9c..2f72ae9c 100755
--- a/contrib/hooks/post-receive.agefile
+++ b/www/git.causal.agency/cgit/contrib/hooks/post-receive.agefile
diff --git a/favicon.ico b/www/git.causal.agency/cgit/favicon.ico
index 56ff5938..56ff5938 100644
--- a/favicon.ico
+++ b/www/git.causal.agency/cgit/favicon.ico
Binary files differdiff --git a/www/git.causal.agency/cgit/filter.c b/www/git.causal.agency/cgit/filter.c
new file mode 100644
index 00000000..2b6c838e
--- /dev/null
+++ b/www/git.causal.agency/cgit/filter.c
@@ -0,0 +1,222 @@
+/* filter.c: filter framework functions
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "html.h"
+
+static inline void reap_filter(struct cgit_filter *filter)
+{
+	if (filter && filter->cleanup)
+		filter->cleanup(filter);
+}
+
+void cgit_cleanup_filters(void)
+{
+	int i;
+	reap_filter(ctx.cfg.about_filter);
+	reap_filter(ctx.cfg.commit_filter);
+	reap_filter(ctx.cfg.source_filter);
+	reap_filter(ctx.cfg.email_filter);
+	reap_filter(ctx.cfg.owner_filter);
+	reap_filter(ctx.cfg.auth_filter);
+	for (i = 0; i < cgit_repolist.count; ++i) {
+		reap_filter(cgit_repolist.repos[i].about_filter);
+		reap_filter(cgit_repolist.repos[i].commit_filter);
+		reap_filter(cgit_repolist.repos[i].source_filter);
+		reap_filter(cgit_repolist.repos[i].email_filter);
+		reap_filter(cgit_repolist.repos[i].owner_filter);
+	}
+}
+
+static int open_exec_filter(struct cgit_filter *base, va_list ap)
+{
+	struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
+	int pipe_fh[2];
+	int i;
+
+	for (i = 0; i < filter->base.argument_count; i++)
+		filter->argv[i + 1] = va_arg(ap, char *);
+
+	chk_zero(fflush(stdout), "unable to flush STDOUT");
+	filter->old_stdout = chk_positive(dup(STDOUT_FILENO),
+		"Unable to duplicate STDOUT");
+	chk_zero(pipe(pipe_fh), "Unable to create pipe to subprocess");
+	filter->pid = chk_non_negative(fork(), "Unable to create subprocess");
+	if (filter->pid == 0) {
+		close(pipe_fh[1]);
+		chk_non_negative(dup2(pipe_fh[0], STDIN_FILENO),
+			"Unable to use pipe as STDIN");
+		execvp(filter->cmd, filter->argv);
+		die_errno("Unable to exec subprocess %s", filter->cmd);
+	}
+	close(pipe_fh[0]);
+	chk_non_negative(dup2(pipe_fh[1], STDOUT_FILENO),
+		"Unable to use pipe as STDOUT");
+	close(pipe_fh[1]);
+	return 0;
+}
+
+static int close_exec_filter(struct cgit_filter *base)
+{
+	struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
+	int i, exit_status = 0;
+
+	chk_zero(fflush(stdout), "unable to flush STDOUT");
+	chk_non_negative(dup2(filter->old_stdout, STDOUT_FILENO),
+		"Unable to restore STDOUT");
+	close(filter->old_stdout);
+	if (filter->pid < 0)
+		goto done;
+	waitpid(filter->pid, &exit_status, 0);
+	if (WIFEXITED(exit_status))
+		goto done;
+	die("Subprocess %s exited abnormally", filter->cmd);
+
+done:
+	for (i = 0; i < filter->base.argument_count; i++)
+		filter->argv[i + 1] = NULL;
+	return WEXITSTATUS(exit_status);
+
+}
+
+static void fprintf_exec_filter(struct cgit_filter *base, FILE *f, const char *prefix)
+{
+	struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
+	fprintf(f, "%sexec:%s\n", prefix, filter->cmd);
+}
+
+static void cleanup_exec_filter(struct cgit_filter *base)
+{
+	struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
+	if (filter->argv) {
+		free(filter->argv);
+		filter->argv = NULL;
+	}
+	if (filter->cmd) {
+		free(filter->cmd);
+		filter->cmd = NULL;
+	}
+}
+
+static struct cgit_filter *new_exec_filter(const char *cmd, int argument_count)
+{
+	struct cgit_exec_filter *f;
+	int args_size = 0;
+
+	f = xmalloc(sizeof(*f));
+	/* We leave argv for now and assign it below. */
+	cgit_exec_filter_init(f, xstrdup(cmd), NULL);
+	f->base.argument_count = argument_count;
+	args_size = (2 + argument_count) * sizeof(char *);
+	f->argv = xmalloc(args_size);
+	memset(f->argv, 0, args_size);
+	f->argv[0] = f->cmd;
+	return &f->base;
+}
+
+void cgit_exec_filter_init(struct cgit_exec_filter *filter, char *cmd, char **argv)
+{
+	memset(filter, 0, sizeof(*filter));
+	filter->base.open = open_exec_filter;
+	filter->base.close = close_exec_filter;
+	filter->base.fprintf = fprintf_exec_filter;
+	filter->base.cleanup = cleanup_exec_filter;
+	filter->cmd = cmd;
+	filter->argv = argv;
+	/* The argument count for open_filter is zero by default, unless called from new_filter, above. */
+	filter->base.argument_count = 0;
+}
+
+int cgit_open_filter(struct cgit_filter *filter, ...)
+{
+	int result;
+	va_list ap;
+	if (!filter)
+		return 0;
+	va_start(ap, filter);
+	result = filter->open(filter, ap);
+	va_end(ap);
+	return result;
+}
+
+int cgit_close_filter(struct cgit_filter *filter)
+{
+	if (!filter)
+		return 0;
+	return filter->close(filter);
+}
+
+void cgit_fprintf_filter(struct cgit_filter *filter, FILE *f, const char *prefix)
+{
+	filter->fprintf(filter, f, prefix);
+}
+
+
+
+static const struct {
+	const char *prefix;
+	struct cgit_filter *(*ctor)(const char *cmd, int argument_count);
+} filter_specs[] = {
+	{ "exec", new_exec_filter },
+};
+
+struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype)
+{
+	char *colon;
+	int i;
+	size_t len;
+	int argument_count;
+
+	if (!cmd || !cmd[0])
+		return NULL;
+
+	colon = strchr(cmd, ':');
+	len = colon - cmd;
+	/*
+	 * In case we're running on Windows, don't allow a single letter before
+	 * the colon.
+	 */
+	if (len == 1)
+		colon = NULL;
+
+	switch (filtertype) {
+		case AUTH:
+			argument_count = 12;
+			break;
+
+		case EMAIL:
+			argument_count = 2;
+			break;
+
+		case OWNER:
+			argument_count = 0;
+			break;
+
+		case SOURCE:
+		case ABOUT:
+			argument_count = 1;
+			break;
+
+		case COMMIT:
+		default:
+			argument_count = 0;
+			break;
+	}
+
+	/* If no prefix is given, exec filter is the default. */
+	if (!colon)
+		return new_exec_filter(cmd, argument_count);
+
+	for (i = 0; i < ARRAY_SIZE(filter_specs); i++) {
+		if (len == strlen(filter_specs[i].prefix) &&
+		    !strncmp(filter_specs[i].prefix, cmd, len))
+			return filter_specs[i].ctor(colon + 1, argument_count);
+	}
+
+	die("Invalid filter type: %.*s", (int) len, cmd);
+}
diff --git a/filters/about-formatting.sh b/www/git.causal.agency/cgit/filters/about-formatting.sh
index 85daf9c2..85daf9c2 100755
--- a/filters/about-formatting.sh
+++ b/www/git.causal.agency/cgit/filters/about-formatting.sh
diff --git a/filters/commit-links.sh b/www/git.causal.agency/cgit/filters/commit-links.sh
index 796ac308..796ac308 100755
--- a/filters/commit-links.sh
+++ b/www/git.causal.agency/cgit/filters/commit-links.sh
diff --git a/filters/email-gravatar.py b/www/git.causal.agency/cgit/filters/email-gravatar.py
index d70440ea..012113c5 100755
--- a/filters/email-gravatar.py
+++ b/www/git.causal.agency/cgit/filters/email-gravatar.py
@@ -1,8 +1,5 @@
 #!/usr/bin/env python3
 
-# Please prefer the email-gravatar.lua using lua: as a prefix over this script. This
-# script is very slow, in comparison.
-#
 # This script may be used with the email-filter or repo.email-filter settings in cgitrc.
 #
 # The following environment variables can be used to retrieve the configuration
diff --git a/filters/html-converters/man2html b/www/git.causal.agency/cgit/filters/html-converters/man2html
index 0ef78841..0ef78841 100755
--- a/filters/html-converters/man2html
+++ b/www/git.causal.agency/cgit/filters/html-converters/man2html
diff --git a/filters/html-converters/md2html b/www/git.causal.agency/cgit/filters/html-converters/md2html
index 59f43a84..59f43a84 100755
--- a/filters/html-converters/md2html
+++ b/www/git.causal.agency/cgit/filters/html-converters/md2html
diff --git a/filters/html-converters/rst2html b/www/git.causal.agency/cgit/filters/html-converters/rst2html
index 02d90f81..02d90f81 100755
--- a/filters/html-converters/rst2html
+++ b/www/git.causal.agency/cgit/filters/html-converters/rst2html
diff --git a/filters/html-converters/txt2html b/www/git.causal.agency/cgit/filters/html-converters/txt2html
index 495eeceb..495eeceb 100755
--- a/filters/html-converters/txt2html
+++ b/www/git.causal.agency/cgit/filters/html-converters/txt2html
diff --git a/filters/syntax-highlighting.py b/www/git.causal.agency/cgit/filters/syntax-highlighting.py
index e912594c..e912594c 100755
--- a/filters/syntax-highlighting.py
+++ b/www/git.causal.agency/cgit/filters/syntax-highlighting.py
diff --git a/filters/syntax-highlighting.sh b/www/git.causal.agency/cgit/filters/syntax-highlighting.sh
index 840bc34f..840bc34f 100755
--- a/filters/syntax-highlighting.sh
+++ b/www/git.causal.agency/cgit/filters/syntax-highlighting.sh
diff --git a/gen-version.sh b/www/git.causal.agency/cgit/gen-version.sh
index 80cf49af..80cf49af 100755
--- a/gen-version.sh
+++ b/www/git.causal.agency/cgit/gen-version.sh
diff --git a/html.c b/www/git.causal.agency/cgit/html.c
index 7f81965f..cefcf5e7 100644
--- a/html.c
+++ b/www/git.causal.agency/cgit/html.c
@@ -80,7 +80,7 @@ char *fmtalloc(const char *format, ...)
 
 void html_raw(const char *data, size_t size)
 {
-	if (write(STDOUT_FILENO, data, size) != size)
+	if (fwrite(data, 1, size, stdout) != size)
 		die_errno("write error on html output");
 }
 
diff --git a/html.h b/www/git.causal.agency/cgit/html.h
index fa4de775..fa4de775 100644
--- a/html.h
+++ b/www/git.causal.agency/cgit/html.h
diff --git a/parsing.c b/www/git.causal.agency/cgit/parsing.c
index 72b59b3c..72b59b3c 100644
--- a/parsing.c
+++ b/www/git.causal.agency/cgit/parsing.c
diff --git a/robots.txt b/www/git.causal.agency/cgit/robots.txt
index 4ce948fe..1b33266d 100644
--- a/robots.txt
+++ b/www/git.causal.agency/cgit/robots.txt
@@ -1,3 +1,4 @@
 User-agent: *
 Disallow: /*/snapshot/*
+Disallow: /*/blame/*
 Allow: /
diff --git a/scan-tree.c b/www/git.causal.agency/cgit/scan-tree.c
index 6a2f65a8..6a2f65a8 100644
--- a/scan-tree.c
+++ b/www/git.causal.agency/cgit/scan-tree.c
diff --git a/scan-tree.h b/www/git.causal.agency/cgit/scan-tree.h
index 1afbd4bb..1afbd4bb 100644
--- a/scan-tree.h
+++ b/www/git.causal.agency/cgit/scan-tree.h
diff --git a/shared.c b/www/git.causal.agency/cgit/shared.c
index 8115469a..8115469a 100644
--- a/shared.c
+++ b/www/git.causal.agency/cgit/shared.c
diff --git a/tests/.gitignore b/www/git.causal.agency/cgit/tests/.gitignore
index 3fd2e965..3fd2e965 100644
--- a/tests/.gitignore
+++ b/www/git.causal.agency/cgit/tests/.gitignore
diff --git a/tests/Makefile b/www/git.causal.agency/cgit/tests/Makefile
index 65e11173..65e11173 100644
--- a/tests/Makefile
+++ b/www/git.causal.agency/cgit/tests/Makefile
diff --git a/tests/filters/dump.sh b/www/git.causal.agency/cgit/tests/filters/dump.sh
index da6f7a1b..da6f7a1b 100755
--- a/tests/filters/dump.sh
+++ b/www/git.causal.agency/cgit/tests/filters/dump.sh
diff --git a/tests/setup.sh b/www/git.causal.agency/cgit/tests/setup.sh
index 8db810ff..31e7d5bb 100755
--- a/tests/setup.sh
+++ b/www/git.causal.agency/cgit/tests/setup.sh
@@ -60,12 +60,6 @@ fi
 
 FILTER_DIRECTORY=$(cd ../filters && pwd)
 
-if cgit --version | grep -F -q "[+] Lua scripting"; then
-	export CGIT_HAS_LUA=1
-else
-	export CGIT_HAS_LUA=0
-fi
-
 mkrepo() {
 	name=$1
 	count=$2
@@ -144,19 +138,6 @@ repo.email-filter=exec:$FILTER_DIRECTORY/dump.sh
 repo.source-filter=exec:$FILTER_DIRECTORY/dump.sh
 repo.readme=master:a+b
 EOF
-
-	if [ $CGIT_HAS_LUA -eq 1 ]; then
-		cat >>cgitrc <<EOF
-repo.url=filter-lua
-repo.path=$PWD/repos/filter/.git
-repo.desc=filtered repo
-repo.about-filter=lua:$FILTER_DIRECTORY/dump.lua
-repo.commit-filter=lua:$FILTER_DIRECTORY/dump.lua
-repo.email-filter=lua:$FILTER_DIRECTORY/dump.lua
-repo.source-filter=lua:$FILTER_DIRECTORY/dump.lua
-repo.readme=master:a+b
-EOF
-	fi
 }
 
 cgit_query()
diff --git a/tests/t0001-validate-git-versions.sh b/www/git.causal.agency/cgit/tests/t0001-validate-git-versions.sh
index dd84fe3f..dd84fe3f 100755
--- a/tests/t0001-validate-git-versions.sh
+++ b/www/git.causal.agency/cgit/tests/t0001-validate-git-versions.sh
diff --git a/tests/t0010-validate-html.sh b/www/git.causal.agency/cgit/tests/t0010-validate-html.sh
index ca08d69d..ca08d69d 100755
--- a/tests/t0010-validate-html.sh
+++ b/www/git.causal.agency/cgit/tests/t0010-validate-html.sh
diff --git a/tests/t0020-validate-cache.sh b/www/git.causal.agency/cgit/tests/t0020-validate-cache.sh
index 657765d8..657765d8 100755
--- a/tests/t0020-validate-cache.sh
+++ b/www/git.causal.agency/cgit/tests/t0020-validate-cache.sh
diff --git a/tests/t0101-index.sh b/www/git.causal.agency/cgit/tests/t0101-index.sh
index 82ef9b04..82ef9b04 100755
--- a/tests/t0101-index.sh
+++ b/www/git.causal.agency/cgit/tests/t0101-index.sh
diff --git a/tests/t0102-summary.sh b/www/git.causal.agency/cgit/tests/t0102-summary.sh
index b8864cb1..b8864cb1 100755
--- a/tests/t0102-summary.sh
+++ b/www/git.causal.agency/cgit/tests/t0102-summary.sh
diff --git a/tests/t0103-log.sh b/www/git.causal.agency/cgit/tests/t0103-log.sh
index bdf1435a..bdf1435a 100755
--- a/tests/t0103-log.sh
+++ b/www/git.causal.agency/cgit/tests/t0103-log.sh
diff --git a/tests/t0104-tree.sh b/www/git.causal.agency/cgit/tests/t0104-tree.sh
index 2e140f59..2e140f59 100755
--- a/tests/t0104-tree.sh
+++ b/www/git.causal.agency/cgit/tests/t0104-tree.sh
diff --git a/tests/t0105-commit.sh b/www/git.causal.agency/cgit/tests/t0105-commit.sh
index 1a12ee39..cfed1e7d 100755
--- a/tests/t0105-commit.sh
+++ b/www/git.causal.agency/cgit/tests/t0105-commit.sh
@@ -11,7 +11,7 @@ test_expect_success 'find commit subject' '
 	grep "<div class=.commit-subject.>commit 5<" tmp
 '
 
-test_expect_success 'find commit msg' 'grep "<div class=.commit-msg.></div>" tmp'
+test_expect_success 'find commit msg' 'grep "<pre class=.commit-msg.></pre>" tmp'
 test_expect_success 'find diffstat' 'grep "<table summary=.diffstat. class=.diffstat.>" tmp'
 
 test_expect_success 'find diff summary' '
@@ -29,8 +29,8 @@ test_expect_success 'root commit contains diffstat' '
 '
 
 test_expect_success 'root commit contains diff' '
-	grep ">diff --git a/file-1 b/file-1<" tmp &&
-	grep "<div class=.add.>+1</div>" tmp
+	grep ">diff --git a/file-1 b/file-1" tmp &&
+	grep "<span class=.add.>+1</span>" tmp
 '
 
 test_done
diff --git a/tests/t0106-diff.sh b/www/git.causal.agency/cgit/tests/t0106-diff.sh
index 82b645ec..62a0a74a 100755
--- a/tests/t0106-diff.sh
+++ b/www/git.causal.agency/cgit/tests/t0106-diff.sh
@@ -9,11 +9,11 @@ test_expect_success 'find blob link' 'grep "<a href=./foo/tree/file-5?id=" tmp'
 test_expect_success 'find added file' 'grep "new file mode 100644" tmp'
 
 test_expect_success 'find hunk header' '
-	grep "<div class=.hunk.>@@ -0,0 +1 @@</div>" tmp
+	grep "<span class=.hunk.>@@ -0,0 +1 @@</span>" tmp
 '
 
 test_expect_success 'find added line' '
-	grep "<div class=.add.>+5</div>" tmp
+	grep "<span class=.add.>+5</span>" tmp
 '
 
 test_done
diff --git a/tests/t0107-snapshot.sh b/www/git.causal.agency/cgit/tests/t0107-snapshot.sh
index 0811ec40..0811ec40 100755
--- a/tests/t0107-snapshot.sh
+++ b/www/git.causal.agency/cgit/tests/t0107-snapshot.sh
diff --git a/tests/t0108-patch.sh b/www/git.causal.agency/cgit/tests/t0108-patch.sh
index 013d6802..013d6802 100755
--- a/tests/t0108-patch.sh
+++ b/www/git.causal.agency/cgit/tests/t0108-patch.sh
diff --git a/tests/t0109-gitconfig.sh b/www/git.causal.agency/cgit/tests/t0109-gitconfig.sh
index 189ef281..189ef281 100755
--- a/tests/t0109-gitconfig.sh
+++ b/www/git.causal.agency/cgit/tests/t0109-gitconfig.sh
diff --git a/tests/t0110-rawdiff.sh b/www/git.causal.agency/cgit/tests/t0110-rawdiff.sh
index 66fa7d5d..66fa7d5d 100755
--- a/tests/t0110-rawdiff.sh
+++ b/www/git.causal.agency/cgit/tests/t0110-rawdiff.sh
diff --git a/tests/t0111-filter.sh b/www/git.causal.agency/cgit/tests/t0111-filter.sh
index 2fdc3669..e5d35750 100755
--- a/tests/t0111-filter.sh
+++ b/www/git.causal.agency/cgit/tests/t0111-filter.sh
@@ -4,9 +4,6 @@ test_description='Check filtered content'
 . ./setup.sh
 
 prefixes="exec"
-if [ $CGIT_HAS_LUA -eq 1 ]; then
-	prefixes="$prefixes lua"
-fi
 
 for prefix in $prefixes
 do
diff --git a/tests/valgrind/bin/cgit b/www/git.causal.agency/cgit/tests/valgrind/bin/cgit
index dcdfbe53..dcdfbe53 100755
--- a/tests/valgrind/bin/cgit
+++ b/www/git.causal.agency/cgit/tests/valgrind/bin/cgit
diff --git a/ui-atom.c b/www/git.causal.agency/cgit/ui-atom.c
index 1056f363..1056f363 100644
--- a/ui-atom.c
+++ b/www/git.causal.agency/cgit/ui-atom.c
diff --git a/ui-atom.h b/www/git.causal.agency/cgit/ui-atom.h
index dda953bb..dda953bb 100644
--- a/ui-atom.h
+++ b/www/git.causal.agency/cgit/ui-atom.h
diff --git a/ui-blame.c b/www/git.causal.agency/cgit/ui-blame.c
index 03136f78..4adec2b9 100644
--- a/ui-blame.c
+++ b/www/git.causal.agency/cgit/ui-blame.c
@@ -152,6 +152,10 @@ static void print_object(const struct object_id *oid, const char *path,
 	cgit_tree_link("tree", NULL, NULL, ctx.qry.head, rev, path);
 	html(")\n");
 
+	if (buffer_is_binary(buf, size)) {
+		html("<div class='error'>blob is binary.</div>");
+		goto cleanup;
+	}
 	if (ctx.cfg.max_blob_size && size / 1024 > ctx.cfg.max_blob_size) {
 		htmlf("<div class='error'>blob size (%ldKB)"
 		      " exceeds display size limit (%dKB).</div>",
diff --git a/ui-blame.h b/www/git.causal.agency/cgit/ui-blame.h
index 5b97e035..5b97e035 100644
--- a/ui-blame.h
+++ b/www/git.causal.agency/cgit/ui-blame.h
diff --git a/ui-blob.c b/www/git.causal.agency/cgit/ui-blob.c
index c10ae42e..c10ae42e 100644
--- a/ui-blob.c
+++ b/www/git.causal.agency/cgit/ui-blob.c
diff --git a/ui-blob.h b/www/git.causal.agency/cgit/ui-blob.h
index 16847b20..16847b20 100644
--- a/ui-blob.h
+++ b/www/git.causal.agency/cgit/ui-blob.h
diff --git a/ui-clone.c b/www/git.causal.agency/cgit/ui-clone.c
index 5dccb639..5dccb639 100644
--- a/ui-clone.c
+++ b/www/git.causal.agency/cgit/ui-clone.c
diff --git a/ui-clone.h b/www/git.causal.agency/cgit/ui-clone.h
index 3e460a3d..3e460a3d 100644
--- a/ui-clone.h
+++ b/www/git.causal.agency/cgit/ui-clone.h
diff --git a/ui-commit.c b/www/git.causal.agency/cgit/ui-commit.c
index 948118c4..b49259e6 100644
--- a/ui-commit.c
+++ b/www/git.causal.agency/cgit/ui-commit.c
@@ -39,10 +39,11 @@ void cgit_print_commit(char *hex, const char *prefix)
 	}
 	info = cgit_parse_commit(commit);
 
-	format_display_notes(&oid, &notes, PAGE_ENCODING, 0);
+	format_display_notes(&oid, &notes, PAGE_ENCODING, 1);
 
 	load_ref_decorations(NULL, DECORATE_FULL_REFS);
 
+	ctx.page.title = fmtalloc("%s - %s", info->subject, ctx.page.title);
 	cgit_print_layout_start();
 	cgit_print_diff_ctrls();
 	html("<table summary='commit info' class='commit-info'>\n");
@@ -120,11 +121,11 @@ void cgit_print_commit(char *hex, const char *prefix)
 	cgit_close_filter(ctx.repo->commit_filter);
 	show_commit_decorations(commit);
 	html("</div>");
-	html("<div class='commit-msg'>");
+	html("<pre class='commit-msg'>");
 	cgit_open_filter(ctx.repo->commit_filter);
 	html_txt(info->msg);
 	cgit_close_filter(ctx.repo->commit_filter);
-	html("</div>");
+	html("</pre>");
 	if (notes.len != 0) {
 		html("<div class='notes-header'>Notes</div>");
 		html("<div class='notes'>");
diff --git a/ui-commit.h b/www/git.causal.agency/cgit/ui-commit.h
index 8198b4ba..8198b4ba 100644
--- a/ui-commit.h
+++ b/www/git.causal.agency/cgit/ui-commit.h
diff --git a/ui-diff.c b/www/git.causal.agency/cgit/ui-diff.c
index 5ed5990c..2a64ae8f 100644
--- a/ui-diff.c
+++ b/www/git.causal.agency/cgit/ui-diff.c
@@ -231,11 +231,11 @@ static void print_line(char *line, int len)
 	else if (line[0] == '@')
 		class = "hunk";
 
-	htmlf("<div class='%s'>", class);
+	htmlf("<span class='%s'>", class);
 	line[len-1] = '\0';
 	html_txt(line);
-	html("</div>");
 	line[len-1] = c;
+	html("</span>\n");
 }
 
 static void header(const struct object_id *oid1, char *path1, int mode1,
@@ -245,22 +245,23 @@ static void header(const struct object_id *oid1, char *path1, int mode1,
 	int subproject;
 
 	subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2));
-	html("<div class='head'>");
+	html("<span class='head'>");
 	html("diff --git a/");
 	html_txt(path1);
 	html(" b/");
 	html_txt(path2);
+	html("\n");
 
 	if (mode1 == 0)
-		htmlf("<br/>new file mode %.6o", mode2);
+		htmlf("new file mode %.6o\n", mode2);
 
 	if (mode2 == 0)
-		htmlf("<br/>deleted file mode %.6o", mode1);
+		htmlf("deleted file mode %.6o\n", mode1);
 
 	if (!subproject) {
 		abbrev1 = xstrdup(find_unique_abbrev(oid1, DEFAULT_ABBREV));
 		abbrev2 = xstrdup(find_unique_abbrev(oid2, DEFAULT_ABBREV));
-		htmlf("<br/>index %s..%s", abbrev1, abbrev2);
+		htmlf("index %s..%s", abbrev1, abbrev2);
 		free(abbrev1);
 		free(abbrev2);
 		if (mode1 != 0 && mode2 != 0) {
@@ -268,28 +269,31 @@ static void header(const struct object_id *oid1, char *path1, int mode1,
 			if (mode2 != mode1)
 				htmlf("..%.6o", mode2);
 		}
+		html("\n");
 		if (is_null_oid(oid1)) {
 			path1 = "dev/null";
-			html("<br/>--- /");
+			html("--- /");
 		} else
-			html("<br/>--- a/");
+			html("--- a/");
 		if (mode1 != 0)
 			cgit_tree_link(path1, NULL, NULL, ctx.qry.head,
 				       oid_to_hex(old_rev_oid), path1);
 		else
 			html_txt(path1);
+		html("\n");
 		if (is_null_oid(oid2)) {
 			path2 = "dev/null";
-			html("<br/>+++ /");
+			html("+++ /");
 		} else
-			html("<br/>+++ b/");
+			html("+++ b/");
 		if (mode2 != 0)
 			cgit_tree_link(path2, NULL, NULL, ctx.qry.head,
 				       oid_to_hex(new_rev_oid), path2);
 		else
 			html_txt(path2);
+		html("\n");
 	}
-	html("</div>");
+	html("</span>");
 }
 
 static void filepair_cb(struct diff_filepair *pair)
@@ -488,12 +492,12 @@ void cgit_print_diff(const char *new_rev, const char *old_rev,
 		html("<table summary='ssdiff' class='ssdiff'>");
 	} else {
 		html("<table summary='diff' class='diff'>");
-		html("<tr><td>");
+		html("<tr><td><pre>");
 	}
 	cgit_diff_tree(old_rev_oid, new_rev_oid, filepair_cb, prefix,
 		       ctx.qry.ignorews);
 	if (!use_ssdiff)
-		html("</td></tr>");
+		html("</pre></td></tr>");
 	html("</table>");
 
 	if (show_ctrls)
diff --git a/ui-diff.h b/www/git.causal.agency/cgit/ui-diff.h
index 39264a16..39264a16 100644
--- a/ui-diff.h
+++ b/www/git.causal.agency/cgit/ui-diff.h
diff --git a/ui-log.c b/www/git.causal.agency/cgit/ui-log.c
index 20774bf8..b443ca73 100644
--- a/ui-log.c
+++ b/www/git.causal.agency/cgit/ui-log.c
@@ -75,11 +75,13 @@ void show_commit_decorations(struct commit *commit)
 			 * don't display anything. */
 			break;
 		case DECORATION_REF_LOCAL:
+			html(" ");
 			cgit_log_link(buf, NULL, "branch-deco", buf, NULL,
 				ctx.qry.vpath, 0, NULL, NULL,
 				ctx.qry.showmsg, 0);
 			break;
 		case DECORATION_REF_TAG:
+			html(" ");
 			if (!read_ref(deco->name, &oid_tag) && !peel_iterated_oid(&oid_tag, &peeled))
 				is_annotated = !oideq(&oid_tag, &peeled);
 			cgit_tag_link(buf, NULL, is_annotated ? "tag-annotated-deco" : "tag-deco", buf);
@@ -87,12 +89,14 @@ void show_commit_decorations(struct commit *commit)
 		case DECORATION_REF_REMOTE:
 			if (!ctx.repo->enable_remote_branches)
 				break;
+			html(" ");
 			cgit_log_link(buf, NULL, "remote-deco", NULL,
 				oid_to_hex(&commit->object.oid),
 				ctx.qry.vpath, 0, NULL, NULL,
 				ctx.qry.showmsg, 0);
 			break;
 		default:
+			html(" ");
 			cgit_commit_link(buf, NULL, "deco", ctx.qry.head,
 					oid_to_hex(&commit->object.oid),
 					ctx.qry.vpath);
diff --git a/ui-log.h b/www/git.causal.agency/cgit/ui-log.h
index 325607cd..325607cd 100644
--- a/ui-log.h
+++ b/www/git.causal.agency/cgit/ui-log.h
diff --git a/ui-patch.c b/www/git.causal.agency/cgit/ui-patch.c
index 4ac03cbe..4ac03cbe 100644
--- a/ui-patch.c
+++ b/www/git.causal.agency/cgit/ui-patch.c
diff --git a/ui-patch.h b/www/git.causal.agency/cgit/ui-patch.h
index 7a6cacd5..7a6cacd5 100644
--- a/ui-patch.h
+++ b/www/git.causal.agency/cgit/ui-patch.h
diff --git a/ui-plain.c b/www/git.causal.agency/cgit/ui-plain.c
index 65a205fa..65a205fa 100644
--- a/ui-plain.c
+++ b/www/git.causal.agency/cgit/ui-plain.c
diff --git a/ui-plain.h b/www/git.causal.agency/cgit/ui-plain.h
index 5bff07b8..5bff07b8 100644
--- a/ui-plain.h
+++ b/www/git.causal.agency/cgit/ui-plain.h
diff --git a/ui-refs.c b/www/git.causal.agency/cgit/ui-refs.c
index 456f610d..456f610d 100644
--- a/ui-refs.c
+++ b/www/git.causal.agency/cgit/ui-refs.c
diff --git a/ui-refs.h b/www/git.causal.agency/cgit/ui-refs.h
index 1d4a54a2..1d4a54a2 100644
--- a/ui-refs.h
+++ b/www/git.causal.agency/cgit/ui-refs.h
diff --git a/ui-repolist.c b/www/git.causal.agency/cgit/ui-repolist.c
index 529a2038..97b11c5f 100644
--- a/ui-repolist.c
+++ b/www/git.causal.agency/cgit/ui-repolist.c
@@ -321,7 +321,7 @@ void cgit_print_repolist(void)
 		}
 		htmlf("<tr><td class='%s'>",
 		      !sorted && section ? "sublevel-repo" : "toplevel-repo");
-		cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL);
+		cgit_summary_link(ctx.repo->name, NULL, NULL, NULL);
 		html("</td><td>");
 		repourl = cgit_repourl(ctx.repo->url);
 		html_link_open(repourl, NULL, NULL);
@@ -353,8 +353,10 @@ void cgit_print_repolist(void)
 		if (ctx.cfg.enable_index_links) {
 			html("<td>");
 			cgit_summary_link("summary", NULL, "button", NULL);
+			html(" ");
 			cgit_log_link("log", NULL, "button", NULL, NULL, NULL,
 				      0, NULL, NULL, ctx.qry.showmsg, 0);
+			html(" ");
 			cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL);
 			html("</td>");
 		}
diff --git a/ui-repolist.h b/www/git.causal.agency/cgit/ui-repolist.h
index 1b6b3227..1b6b3227 100644
--- a/ui-repolist.h
+++ b/www/git.causal.agency/cgit/ui-repolist.h
diff --git a/ui-shared.c b/www/git.causal.agency/cgit/ui-shared.c
index acd8ab55..dfaf5952 100644
--- a/ui-shared.c
+++ b/www/git.causal.agency/cgit/ui-shared.c
@@ -835,7 +835,7 @@ void cgit_print_docend(void)
 	if (ctx.cfg.footer)
 		html_include(ctx.cfg.footer);
 	else {
-		htmlf("<div class='footer'>generated by <a href='https://git.zx2c4.com/cgit/about/'>cgit %s</a> "
+		htmlf("<div class='footer'>generated by <a href='https://git.causal.agency/src/log/www/git.causal.agency/cgit'>cgit %s</a> "
 			"(<a href='https://git-scm.com/'>git %s</a>) at ", cgit_version, git_version_string);
 		html_txt(show_date(time(NULL), 0, cgit_date_mode(DATE_ISO8601)));
 		html("</div>\n");
@@ -894,6 +894,15 @@ void cgit_add_clone_urls(void (*fn)(const char *))
 		add_clone_urls(fn, ctx.cfg.clone_prefix, ctx.repo->url);
 }
 
+static int print_this_commit_option(void)
+{
+	struct object_id oid;
+	if (!ctx.qry.head || get_oid(ctx.qry.head, &oid))
+		return 1;
+	html_option(oid_to_hex(&oid), "this commit", ctx.qry.head);
+	return 0;
+}
+
 static int print_branch_option(const char *refname, const struct object_id *oid,
 			       int flags, void *cb_data)
 {
@@ -995,15 +1004,18 @@ static void print_header(void)
 	if (ctx.repo) {
 		cgit_index_link("index", NULL, NULL, NULL, NULL, 0, 1);
 		html(" : ");
-		cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL);
+		cgit_summary_link(ctx.repo->name, NULL, NULL, NULL);
 		if (ctx.env.authenticated) {
 			html("</td><td class='form'>");
 			html("<form method='get'>\n");
 			cgit_add_hidden_formfields(0, 1, ctx.qry.page);
 			html("<select name='h' onchange='this.form.submit();'>\n");
+			print_this_commit_option();
+			html("<optgroup label='branches'>");
 			for_each_branch_ref(print_branch_option, ctx.qry.head);
 			if (ctx.repo->enable_remote_branches)
 				for_each_remote_ref(print_branch_option, ctx.qry.head);
+			html("</optgroup>");
 			html("</select> ");
 			html("<input type='submit' value='switch'/>");
 			html("</form>");
@@ -1016,7 +1028,13 @@ static void print_header(void)
 	if (ctx.repo) {
 		html_txt(ctx.repo->desc);
 		html("</td><td class='sub right'>");
-		html_txt(ctx.repo->owner);
+		if (ctx.repo->owner_filter) {
+			cgit_open_filter(ctx.repo->owner_filter);
+			html_txt(ctx.repo->owner);
+			cgit_close_filter(ctx.repo->owner_filter);
+		} else {
+			html_txt(ctx.repo->owner);
+		}
 	} else {
 		if (ctx.cfg.root_desc)
 			html_txt(ctx.cfg.root_desc);
@@ -1032,32 +1050,41 @@ void cgit_print_pageheader(void)
 
 	html("<table class='tabs'><tr><td>\n");
 	if (ctx.env.authenticated && ctx.repo) {
-		if (ctx.repo->readme.nr)
+		if (ctx.repo->readme.nr) {
 			reporevlink("about", "about", NULL,
 				    hc("about"), ctx.qry.head, NULL,
 				    NULL);
+			html(" ");
+		}
 		cgit_summary_link("summary", NULL, hc("summary"),
 				  ctx.qry.head);
+		html(" ");
 		cgit_refs_link("refs", NULL, hc("refs"), ctx.qry.head,
 			       ctx.qry.oid, NULL);
+		html(" ");
 		cgit_log_link("log", NULL, hc("log"), ctx.qry.head,
 			      NULL, ctx.qry.vpath, 0, NULL, NULL,
 			      ctx.qry.showmsg, ctx.qry.follow);
+		html(" ");
 		if (ctx.qry.page && !strcmp(ctx.qry.page, "blame"))
 			cgit_blame_link("blame", NULL, hc("blame"), ctx.qry.head,
 				        ctx.qry.oid, ctx.qry.vpath);
 		else
 			cgit_tree_link("tree", NULL, hc("tree"), ctx.qry.head,
 				       ctx.qry.oid, ctx.qry.vpath);
+		html(" ");
 		cgit_commit_link("commit", NULL, hc("commit"),
 				 ctx.qry.head, ctx.qry.oid, ctx.qry.vpath);
+		html(" ");
 		cgit_diff_link("diff", NULL, hc("diff"), ctx.qry.head,
 			       ctx.qry.oid, ctx.qry.oid2, ctx.qry.vpath);
-		if (ctx.repo->max_stats)
+		if (ctx.repo->max_stats) {
+			html(" ");
 			cgit_stats_link("stats", NULL, hc("stats"),
 					ctx.qry.head, ctx.qry.vpath);
+		}
 		if (ctx.repo->homepage) {
-			html("<a href='");
+			html(" <a href='");
 			html_attr(ctx.repo->homepage);
 			html("'>homepage</a>");
 		}
@@ -1201,9 +1228,12 @@ void cgit_set_title_from_path(const char *path)
 	if (!path)
 		return;
 
-	for (last_slash = path + strlen(path); (slash = memrchr(path, '/', last_slash - path)) != NULL; last_slash = slash) {
+	last_slash = path + strlen(path);
+	for (slash = last_slash; slash > path; --slash) {
+		if (*slash != '/') continue;
 		strbuf_add(&sb, slash + 1, last_slash - slash - 1);
 		strbuf_addstr(&sb, " \xc2\xab ");
+		last_slash = slash;
 	}
 	strbuf_add(&sb, path, last_slash - path);
 	strbuf_addf(&sb, " - %s", ctx.page.title);
diff --git a/ui-shared.h b/www/git.causal.agency/cgit/ui-shared.h
index 6964873a..6964873a 100644
--- a/ui-shared.h
+++ b/www/git.causal.agency/cgit/ui-shared.h
diff --git a/ui-snapshot.c b/www/git.causal.agency/cgit/ui-snapshot.c
index 18361a65..28013935 100644
--- a/ui-snapshot.c
+++ b/www/git.causal.agency/cgit/ui-snapshot.c
@@ -37,6 +37,9 @@ static int write_archive_type(const char *format, const char *hex, const char *p
 	/* strvec guarantees a trailing NULL entry. */
 	memcpy(nargv, argv.v, sizeof(char *) * (argv.nr + 1));
 
+	if (fflush(stdout))
+		return errno;
+
 	result = write_archive(argv.nr, nargv, NULL, the_repository, NULL, 0);
 	strvec_clear(&argv);
 	free(nargv);
diff --git a/ui-snapshot.h b/www/git.causal.agency/cgit/ui-snapshot.h
index a8deec36..a8deec36 100644
--- a/ui-snapshot.h
+++ b/www/git.causal.agency/cgit/ui-snapshot.h
diff --git a/ui-ssdiff.c b/www/git.causal.agency/cgit/ui-ssdiff.c
index af8bc9e0..af8bc9e0 100644
--- a/ui-ssdiff.c
+++ b/www/git.causal.agency/cgit/ui-ssdiff.c
diff --git a/ui-ssdiff.h b/www/git.causal.agency/cgit/ui-ssdiff.h
index 11f27144..11f27144 100644
--- a/ui-ssdiff.h
+++ b/www/git.causal.agency/cgit/ui-ssdiff.h
diff --git a/ui-stats.c b/www/git.causal.agency/cgit/ui-stats.c
index 09b3625e..09b3625e 100644
--- a/ui-stats.c
+++ b/www/git.causal.agency/cgit/ui-stats.c
diff --git a/ui-stats.h b/www/git.causal.agency/cgit/ui-stats.h
index 0e61b03d..0e61b03d 100644
--- a/ui-stats.h
+++ b/www/git.causal.agency/cgit/ui-stats.h
diff --git a/ui-summary.c b/www/git.causal.agency/cgit/ui-summary.c
index 947812a8..947812a8 100644
--- a/ui-summary.c
+++ b/www/git.causal.agency/cgit/ui-summary.c
diff --git a/ui-summary.h b/www/git.causal.agency/cgit/ui-summary.h
index cba696af..cba696af 100644
--- a/ui-summary.h
+++ b/www/git.causal.agency/cgit/ui-summary.h
diff --git a/ui-tag.c b/www/git.causal.agency/cgit/ui-tag.c
index 424bbccd..05952429 100644
--- a/ui-tag.c
+++ b/www/git.causal.agency/cgit/ui-tag.c
@@ -25,9 +25,9 @@ static void print_tag_content(char *buf)
 	html_txt(buf);
 	html("</div>");
 	if (p) {
-		html("<div class='commit-msg'>");
+		html("<pre class='commit-msg'>");
 		html_txt(++p);
-		html("</div>");
+		html("</pre>");
 	}
 }
 
diff --git a/ui-tag.h b/www/git.causal.agency/cgit/ui-tag.h
index d295cdcd..d295cdcd 100644
--- a/ui-tag.h
+++ b/www/git.causal.agency/cgit/ui-tag.h
diff --git a/ui-tree.c b/www/git.causal.agency/cgit/ui-tree.c
index b61f6f54..21e0b884 100644
--- a/ui-tree.c
+++ b/www/git.causal.agency/cgit/ui-tree.c
@@ -89,6 +89,7 @@ static void print_object(const struct object_id *oid, const char *path, const ch
 	enum object_type type;
 	char *buf;
 	unsigned long size;
+	int is_binary;
 
 	type = oid_object_info(the_repository, oid, &size);
 	if (type == OBJ_BAD) {
@@ -103,6 +104,7 @@ static void print_object(const struct object_id *oid, const char *path, const ch
 			"Error reading object %s", oid_to_hex(oid));
 		return;
 	}
+	is_binary = buffer_is_binary(buf, size);
 
 	cgit_set_title_from_path(path);
 
@@ -110,7 +112,7 @@ static void print_object(const struct object_id *oid, const char *path, const ch
 	htmlf("blob: %s (", oid_to_hex(oid));
 	cgit_plain_link("plain", NULL, NULL, ctx.qry.head,
 		        rev, path);
-	if (ctx.repo->enable_blame) {
+	if (ctx.repo->enable_blame && !is_binary) {
 		html(") (");
 		cgit_blame_link("blame", NULL, NULL, ctx.qry.head,
 			        rev, path);
@@ -123,7 +125,7 @@ static void print_object(const struct object_id *oid, const char *path, const ch
 		return;
 	}
 
-	if (buffer_is_binary(buf, size))
+	if (is_binary)
 		print_binary_buffer(buf, size);
 	else
 		print_text_buffer(basename, buf, size);
@@ -202,9 +204,11 @@ static int ls_item(const struct object_id *oid, struct strbuf *base,
 	struct walk_tree_context *walk_tree_ctx = cbdata;
 	char *name;
 	struct strbuf fullpath = STRBUF_INIT;
+	struct strbuf linkpath = STRBUF_INIT;
 	struct strbuf class = STRBUF_INIT;
 	enum object_type type;
 	unsigned long size = 0;
+	char *buf;
 
 	name = xstrdup(pathname);
 	strbuf_addf(&fullpath, "%s%s%s", ctx.qry.path ? ctx.qry.path : "",
@@ -216,8 +220,7 @@ static int ls_item(const struct object_id *oid, struct strbuf *base,
 			htmlf("<tr><td colspan='3'>Bad object: %s %s</td></tr>",
 			      name,
 			      oid_to_hex(oid));
-			free(name);
-			return 0;
+			goto cleanup;
 		}
 	}
 
@@ -237,22 +240,45 @@ static int ls_item(const struct object_id *oid, struct strbuf *base,
 		cgit_tree_link(name, NULL, class.buf, ctx.qry.head,
 			       walk_tree_ctx->curr_rev, fullpath.buf);
 	}
+	if (S_ISLNK(mode)) {
+		html(" -> ");
+		buf = read_object_file(oid, &type, &size);
+		if (!buf) {
+			htmlf("Error reading object: %s", oid_to_hex(oid));
+			goto cleanup;
+		}
+		strbuf_addbuf(&linkpath, &fullpath);
+		strbuf_addf(&linkpath, "/../%s", buf);
+		strbuf_normalize_path(&linkpath);
+		cgit_tree_link(buf, NULL, class.buf, ctx.qry.head,
+			walk_tree_ctx->curr_rev, linkpath.buf);
+		free(buf);
+		strbuf_release(&linkpath);
+	}
 	htmlf("</td><td class='ls-size'>%li</td>", size);
 
 	html("<td>");
 	cgit_log_link("log", NULL, "button", ctx.qry.head,
 		      walk_tree_ctx->curr_rev, fullpath.buf, 0, NULL, NULL,
 		      ctx.qry.showmsg, 0);
-	if (ctx.repo->max_stats)
+	if (ctx.repo->max_stats) {
+		html(" ");
 		cgit_stats_link("stats", NULL, "button", ctx.qry.head,
 				fullpath.buf);
-	if (!S_ISGITLINK(mode))
+	}
+	if (!S_ISGITLINK(mode)) {
+		html(" ");
 		cgit_plain_link("plain", NULL, "button", ctx.qry.head,
 				walk_tree_ctx->curr_rev, fullpath.buf);
-	if (!S_ISDIR(mode) && ctx.repo->enable_blame)
+	}
+	if (!S_ISDIR(mode) && ctx.repo->enable_blame) {
+		html(" ");
 		cgit_blame_link("blame", NULL, "button", ctx.qry.head,
 				walk_tree_ctx->curr_rev, fullpath.buf);
+	}
 	html("</td></tr>\n");
+
+cleanup:
 	free(name);
 	strbuf_release(&fullpath);
 	strbuf_release(&class);
diff --git a/ui-tree.h b/www/git.causal.agency/cgit/ui-tree.h
index bbd34e35..bbd34e35 100644
--- a/ui-tree.h
+++ b/www/git.causal.agency/cgit/ui-tree.h
diff --git a/www/git.causal.agency/cgitrc b/www/git.causal.agency/cgitrc
new file mode 100644
index 00000000..8ccd7c72
--- /dev/null
+++ b/www/git.causal.agency/cgitrc
@@ -0,0 +1,29 @@
+root-title=causal agency
+root-desc=“I think some people from the Gentoo project are behind this.”
+logo=
+
+clone-url=https://$HTTP_HOST/$CGIT_REPO_URL
+snapshots=tar.gz zip
+
+enable-blame=1
+enable-commit-graph=1
+enable-subject-links=1
+enable-follow-links=1
+enable-index-owner=0
+repository-sort=age
+branch-sort=age
+
+css=/custom.css
+email-filter=/usr/local/libexec/cgit-email
+about-filter=/usr/local/libexec/about-filter
+source-filter=/usr/local/libexec/source-filter
+owner-filter=/usr/local/libexec/owner-filter
+
+readme=:README.7
+readme=:README
+
+remove-suffix=1
+enable-git-config=1
+scan-path=/home/june/pub
+
+cache-size=1024
diff --git a/www/git.causal.agency/custom.css b/www/git.causal.agency/custom.css
new file mode 100644
index 00000000..3bc61c90
--- /dev/null
+++ b/www/git.causal.agency/custom.css
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+@import url("cgit.css");
+
+* { line-height: 1.25em; }
+
+div#cgit {
+	max-width: 117ch;
+	margin: auto;
+	font-family: monospace;
+	-moz-tab-size: 4;
+	tab-size: 4;
+}
+
+div#cgit table#header td.sub {
+	border-top: none;
+}
+div#cgit table#header td.sub.right {
+	padding-right: 1em;
+}
+div#cgit table.tabs {
+	border-bottom: none;
+}
+div#cgit div.content {
+	border-bottom: none;
+}
+div#cgit table.list th a {
+	color: inherit;
+}
+div#cgit table.list tr:nth-child(even) {
+	background: inherit;
+}
+div#cgit table.list tr:hover {
+	background: inherit;
+}
+div#cgit table.list tr.nohover-highlight:hover:nth-child(even) {
+	background: inherit;
+}
+
+div#cgit table.blob td.linenumbers a:target {
+	color: goldenrod;
+	text-decoration: underline;
+	outline: none;
+}
+
+div#cgit div#summary {
+	max-width: 80ch;
+}
+
+/* for hilex(1) */
+div#cgit pre .Ke { color: dimgray; }
+div#cgit pre .Ma { color: green; }
+div#cgit pre .Co { color: navy; }
+div#cgit pre .St { color: teal; }
+div#cgit pre .Fo { color: teal; font-weight: bold; }
+div#cgit pre .Su { color: olive; }
+
+/* for htagml(1) */
+div#cgit pre a.tag { color: inherit; text-decoration: underline; }
+div#cgit pre a.tag:target { color: goldenrod; outline: none; }
+
+/* for mandoc(1) */
+table.head, table.foot { width: 100%; }
+td.head-rtitle, td.foot-os { text-align: right; }
+td.head-vol { text-align: center; }
+div.Pp { margin: 1ex 0ex; }
+div.Nd, div.Bf, div.Op { display: inline; }
+span.Pa, span.Ad { font-style: italic; }
+span.Ms { font-weight: bold; }
+dl.Bl-diag > dt { font-weight: bold; }
+code.Nm, code.Fl, code.Cm, code.Ic, code.In, code.Fd, code.Fn,
+code.Cd { font-weight: bold; font-family: inherit; }
+
+h1.Sh { font-size: 1.5em; }
+table.Nm td:first-child { padding-right: 1ch; }
+code.Fl { white-space: nowrap; }
+span.RsT { font-style: italic; }
+dl.Bl-tag:not(.Bl-compact) dt { margin-top: 1em; }
+ul.Bl-bullet:not(.Bl-compact) li { margin-top: 1em; }
+div.Bd-indent { margin-left: 4ch; }
+table.Bl-column { width: 100%; }
+table.foot { margin-top: 1em; }
+
+div#cgit a.permalink { color: inherit; }
diff --git a/www/git.causal.agency/owner-filter.sh b/www/git.causal.agency/owner-filter.sh
new file mode 100644
index 00000000..18e74cf1
--- /dev/null
+++ b/www/git.causal.agency/owner-filter.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+
+cat <<EOF
+<a href="https://liberapay.com/june/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
+EOF
diff --git a/www/git.causal.agency/source-filter.sh b/www/git.causal.agency/source-filter.sh
new file mode 100644
index 00000000..514272db
--- /dev/null
+++ b/www/git.causal.agency/source-filter.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+set -eu
+
+ctags=/usr/bin/ctags
+mtags=/usr/local/libexec/mtags
+hilex=/usr/local/libexec/hilex
+htagml=/usr/local/libexec/htagml
+
+case "$1" in
+	(*.[chlmy]|Makefile|*.mk|*.[1-9]|.profile|.shrc|*.sh)
+		tmp=$(mktemp -d -t source-filter)
+		trap 'rm -fr "${tmp}"' EXIT
+		cd "${tmp}"
+		cat >"$1"
+		touch tags
+		case "$1" in
+			(*.[chlmy]) $ctags -w "$1";;
+			(*) $mtags "$1";;
+		esac
+		$hilex -f html "$1" | $htagml -i "$1"
+		;;
+	(*)
+		exec $hilex -t -n "$1" -f html
+		;;
+esac
diff --git a/www/temp.causal.agency/.gitignore b/www/temp.causal.agency/.gitignore
new file mode 100644
index 00000000..e31ee94e
--- /dev/null
+++ b/www/temp.causal.agency/.gitignore
@@ -0,0 +1 @@
+up
diff --git a/www/temp.causal.agency/Makefile b/www/temp.causal.agency/Makefile
new file mode 100644
index 00000000..3e908305
--- /dev/null
+++ b/www/temp.causal.agency/Makefile
@@ -0,0 +1,16 @@
+WEBROOT = /usr/local/www/temp.causal.agency
+
+CFLAGS += -std=c11 -Wall -Wextra -Wpedantic -I/usr/local/include
+LDFLAGS += -static -L/usr/local/lib
+LDLIBS = -lkcgihtml -lkcgi -lz -lmd
+
+up:
+
+clean:
+	rm -f up
+
+install: up
+	install -m 700 up ${WEBROOT}/up
+
+uninstall:
+	rm -f ${WEBROOT}/up
diff --git a/www/temp.causal.agency/up.c b/www/temp.causal.agency/up.c
new file mode 100644
index 00000000..885b8acd
--- /dev/null
+++ b/www/temp.causal.agency/up.c
@@ -0,0 +1,156 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/capsicum.h>
+#include <sys/types.h>
+#include <sysexits.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <kcgi.h>
+#include <kcgihtml.h>
+
+static int cwd = -1;
+
+static const struct kvalid Key = { NULL, "file" };
+
+static enum kcgi_err head(struct kreq *req, enum khttp http, enum kmime mime) {
+	return khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[http])
+		|| khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[mime]);
+}
+
+static enum kcgi_err fail(struct kreq *req, enum khttp http) {
+	return head(req, http, KMIME_TEXT_PLAIN)
+		|| khttp_body(req)
+		|| khttp_printf(req, "%s\n", khttps[http]);
+}
+
+static enum kcgi_err handle(struct kreq *req) {
+	if (req->page) return fail(req, KHTTP_404);
+
+	if (req->method == KMETHOD_GET) {
+		struct khtmlreq html;
+		struct khtmlreq *h = &html;
+		return head(req, KHTTP_200, KMIME_TEXT_HTML)
+			|| khttp_body(req)
+			|| khtml_open(h, req, 0)
+			|| khtml_elem(h, KELEM_DOCTYPE)
+			|| khtml_elem(h, KELEM_TITLE)
+			|| khtml_puts(h, "Upload")
+			|| khtml_closeelem(h, 1)
+			|| khtml_attr(
+				h, KELEM_FORM,
+				KATTR_METHOD, "post",
+				KATTR_ACTION, "",
+				KATTR_ENCTYPE, "multipart/form-data",
+				KATTR__MAX
+			)
+			|| khtml_attr(
+				h, KELEM_INPUT,
+				KATTR_TYPE, "file",
+				KATTR_NAME, Key.name,
+				KATTR__MAX
+			)
+			|| khtml_attr(
+				h, KELEM_INPUT,
+				KATTR_TYPE, "submit",
+				KATTR_VALUE, "Upload",
+				KATTR__MAX
+			)
+			|| khtml_close(h);
+
+	} else if (req->method == KMETHOD_POST) {
+		struct kpair *field = req->fieldmap[0];
+		if (!field || !field->valsz) return fail(req, KHTTP_400);
+
+		char name[256];
+		const char *ext = strrchr(field->file, '.');
+		if (!ext) ext = "";
+		snprintf(
+			name, sizeof(name), "%jx%08x%s",
+			(intmax_t)time(NULL), arc4random(), ext
+		);
+
+		int fd = openat(cwd, name, O_CREAT | O_EXCL | O_WRONLY, 0644);
+		if (fd < 0) {
+			warn("openat");
+			return fail(req, KHTTP_507);
+		}
+		ssize_t len = write(fd, field->val, field->valsz);
+		int error = close(fd);
+		if (len < 0 || error) {
+			warn("write");
+			return fail(req, KHTTP_507);
+		}
+
+		return head(req, KHTTP_303, KMIME_TEXT_PLAIN)
+			|| khttp_head(req, kresps[KRESP_LOCATION], "/%s", name)
+			|| khttp_body(req)
+			|| khttp_puts(req, name);
+
+	} else {
+		return fail(req, KHTTP_405);
+	}
+}
+
+static void sandbox(void) {
+	cwd = open(".", O_DIRECTORY);
+	if (cwd < 0) err(EX_CONFIG, ".");
+
+	int error = cap_enter();
+	if (error) err(EX_OSERR, "cap_enter");
+
+	cap_rights_t rights;
+	cap_rights_init(&rights, CAP_LOOKUP, CAP_CREATE, CAP_PWRITE);
+	error = cap_rights_limit(cwd, &rights);
+	if (error) err(EX_OSERR, "cap_rights_limit");
+}
+
+int main(void) {
+	const char *page = "up";
+	if (khttp_fcgi_test()) {
+		struct kfcgi *fcgi;
+		enum kcgi_err error = khttp_fcgi_init(&fcgi, &Key, 1, &page, 1, 0);
+		if (error) errx(EX_CONFIG, "khttp_fcgi_init: %s", kcgi_strerror(error));
+		sandbox();
+		for (
+			struct kreq req;
+			KCGI_OK == (error = khttp_fcgi_parse(fcgi, &req));
+			khttp_free(&req)
+		) {
+			error = handle(&req);
+			if (error && error != KCGI_HUP) break;
+		}
+		if (error != KCGI_EXIT) {
+			errx(EX_PROTOCOL, "khttp_fcgi_parse: %s", kcgi_strerror(error));
+		}
+		khttp_fcgi_free(fcgi);
+	} else {
+		struct kreq req;
+		enum kcgi_err error = khttp_parse(&req, &Key, 1, &page, 1, 0);
+		if (error) errx(EX_PROTOCOL, "khttp_parse: %s", kcgi_strerror(error));
+		error = handle(&req);
+		if (error) errx(EX_PROTOCOL, "%s", kcgi_strerror(error));
+		khttp_free(&req);
+	}
+}
diff --git a/www/text.causal.agency/.gitignore b/www/text.causal.agency/.gitignore
new file mode 100644
index 00000000..8fe3acc9
--- /dev/null
+++ b/www/text.causal.agency/.gitignore
@@ -0,0 +1,3 @@
+*.txt
+feed.atom
+igp
diff --git a/www/text.causal.agency/001-make.7 b/www/text.causal.agency/001-make.7
new file mode 100644
index 00000000..b4805729
--- /dev/null
+++ b/www/text.causal.agency/001-make.7
@@ -0,0 +1,159 @@
+.Dd September 17, 2018
+.Dt MAKE 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Using Make
+.Nd writing less Makefile
+.
+.Sh DESCRIPTION
+Let's talk about
+.Xr make 1 .
+I think an important thing to know about
+.Xr make 1
+is that you don't need to write a
+.Pa Makefile
+to use it.
+There are default rules
+for C, C++ and probably Fortran.
+To build
+.Pa foo
+from
+.Pa foo.c ,
+just run:
+.
+.Pp
+.Dl make foo
+.
+.Pp
+The default rule for C files uses the
+.Ev CFLAGS
+variable,
+so you can set that in the environment
+to pass flags to the C compiler:
+.
+.Pp
+.Dl CFLAGS=-Wall make foo
+.
+.Pp
+It also uses
+.Ev LDLIBS
+for linking,
+so you can add libraries with:
+.
+.Pp
+.Dl LDLIBS=-lcurses make foo
+.
+.Pp
+Obviously writing this every time
+would become tedious,
+so it might be time to write a
+.Pa Makefile .
+But it really doesn't need much:
+.
+.Bd -literal -offset indent
+CFLAGS += -Wall -Wextra
+LDLIBS = -lcurses
+
+foo:
+.Ed
+.
+.Pp
+Assigning
+.Ev CFLAGS
+with
+.Ql +=
+preserves the system default
+or anything passed in the environment.
+Declaring
+.Pa foo
+as the first rule
+makes it the default when
+.Ql make
+is run without a target.
+Note that the rule doesn't need a definition;
+the default will still be used.
+.
+.Pp
+If
+.Pa foo
+is built from serveral source files,
+unfortunately a rule definition is required:
+.
+.Bd -literal -offset indent
+OBJS = foo.o bar.o baz.o
+
+foo: $(OBJS)
+	$(CC) $(LDFLAGS) $(OBJS) $(LDLIBS) -o $@
+.Ed
+.
+.Pp
+This rule uses
+.Ev LDFLAGS
+for passing linker flags,
+which is what the default rule does.
+The
+.Ql $@
+variable here expands to
+.Ql foo ,
+so this rule can be copied easily
+for other binary targets.
+.
+.Pp
+If some sources depend on a header file,
+they can be automatically rebuilt
+when the header changes
+by declaring a dependency rule:
+.
+.Pp
+.Dl foo.o bar.o: foo.h
+.
+.Pp
+Note that several files can appear
+either side of the
+.Ql ":" .
+.
+.Pp
+Lastly,
+it's always nice to add a
+.Cm clean
+target:
+.
+.Bd -literal -offset indent
+clean:
+	rm -f $(OBJS) foo
+.Ed
+.
+.Pp
+I hope this helps getting started with
+.Xr make 1
+without writing too much
+.Pa Makefile !
+.
+.Sh EXAMPLES
+The example
+.Pa Makefile
+in its entirety:
+.
+.Bd -literal -offset indent
+CFLAGS += -Wall -Wextra
+LDLIBS = -lcurses
+OBJS = foo.o bar.o baz.o
+
+foo: $(OBJS)
+	$(CC) $(LDFLAGS) $(OBJS) $(LDLIBS) -o $@
+
+foo.o bar.o: foo.h
+
+clean:
+	rm -f $(OBJS) foo
+.Ed
+.
+.Sh AUTHORS
+.An Mt june@causal.agency
+.
+.Pp
+This document is produced from
+.Xr mdoc 7
+source available from
+.Lk https://git.causal.agency/src/tree/www/text.causal.agency
diff --git a/www/text.causal.agency/002-writing-mdoc.7 b/www/text.causal.agency/002-writing-mdoc.7
new file mode 100644
index 00000000..b377d364
--- /dev/null
+++ b/www/text.causal.agency/002-writing-mdoc.7
@@ -0,0 +1,138 @@
+.Dd September 27, 2018
+.Dt WRITING-MDOC 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Writing mdoc
+.Nd semantic markup
+.
+.Sh DESCRIPTION
+I recently learned how to write man pages
+so that I could document
+a bunch of little programs I've written.
+Modern man pages are written in
+.Xr mdoc 7 ,
+whose documentation is also available from
+.Lk http://mandoc.bsd.lv .
+.
+.Pp
+.Xr mdoc 7
+differs from many other markup languages
+by providing
+.Dq semantic markup
+rather than just
+.Dq physical markup.
+What this means is that
+the markup indicates what something is,
+not how to format it.
+For example,
+the
+.Ql \&Ar
+macro is used to indicate
+command-line arguments
+rather than one of the macros
+for bold, italic or underline.
+This frees each author of having to choose
+and enables consistent presentation
+across different man pages.
+.
+.Pp
+Another advantage of semantic markup
+is that information can be extracted from it.
+For example,
+.Xr makewhatis 8
+can easily extract the name and short description
+from each man page
+thanks to the
+.Ql \&Nm
+and
+.Ql \&Nd
+macros.
+I use the same information
+to generate an Atom feed for these documents,
+though in admittedly a much less robust way than
+.Xr mandoc 1 .
+.
+.Pp
+When it comes to actually writing
+.Xr mdoc 7 ,
+it can take some getting used to.
+The language is of
+.Xr roff 7
+lineage
+so its syntax is very particular.
+Macros cannot appear inline,
+but must start on new lines
+beginning with
+.Ql \&. .
+Sentences should likewise
+always start on a new line.
+Since I'm in the habit of writing with
+semantic line breaks,
+I actually find these requirements
+fit in well.
+.
+.Pp
+The more frustrating syntax limitation to me
+is the rule against empty lines.
+Without them,
+it can be quite difficult to edit a lengthy document.
+Thankfully,
+lines with only a
+.Ql \&.
+on them are allowed,
+but this still causes visual noise.
+To alleviate that,
+I have a
+.Xr vim 1
+syntax file for
+.Xr mdoc 7
+which conceals the lone dots:
+.
+.Bd -literal -offset indent
+if exists("b:current_syntax")
+	finish
+endif
+
+runtime! syntax/nroff.vim
+unlet! b:current_syntax
+
+setlocal sections+=ShSs
+syntax match mdocBlank /^\\.$/ conceal
+setlocal conceallevel=2
+
+let b:current_syntax = "mdoc"
+.Ed
+.
+.Pp
+It also adds the
+.Xr mdoc 7
+section header and subsection header macros to the
+.Cm sections
+option to make
+.Xr vim 1 Ap s
+.Ic {
+and
+.Ic }
+motions
+aware of them.
+.
+.Pp
+With that,
+I've found writing man pages pleasant and rewarding.
+I've started writing other documents with
+.Xr mdoc 7
+as well,
+as you can see here.
+.
+.Sh SEE ALSO
+.Lk http://rhodesmill.org/brandon/2012/one-sentence-per-line/ "Semantic Linefeeds"
+.
+.Sh AUTHORS
+.An Mt june@causal.agency
+.
+.Pp
+This document is produced from
+.Xr mdoc 7
+source available from
+.Lk https://git.causal.agency/src/tree/www/text.causal.agency
diff --git a/www/text.causal.agency/003-pleasant-c.7 b/www/text.causal.agency/003-pleasant-c.7
new file mode 100644
index 00000000..16030b7e
--- /dev/null
+++ b/www/text.causal.agency/003-pleasant-c.7
@@ -0,0 +1,120 @@
+.Dd September 30, 2018
+.Dt PLEASANT-C 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Pleasant C
+.Nd it's good, actually
+.
+.Sh DESCRIPTION
+I've been writing a lot of C lately
+and actually find it very pleasant.
+I want to talk about some of its ergonomic features.
+These are C99 features unless otherwise noted.
+.
+.Ss Initializer syntax
+Struct and union initializer syntax
+is well generalized.
+Designators can be chained,
+making initializing nested structs easy,
+and all uninitialized fields are zeroed.
+.
+.Bd -literal -offset indent
+struct {
+	struct pollfd fds[2];
+} loop = {
+	.fds[0].fd = STDIN_FILENO,
+	.fds[1].fd = STDOUT_FILENO,
+	.fds[0].events = POLLIN,
+	.fds[1].events = POLLOUT,
+};
+.Ed
+.
+.Ss Variable-length arrays
+VLAs can be multi-dimensional,
+which can avoid manual stride multiplications
+needed to index a flat
+.Xr malloc 3 Ap d
+array.
+.
+.Bd -literal -offset indent
+uint8_t glyphs[len][height][width];
+fread(glyphs, height * width, len, stdin);
+.Ed
+.
+.Ss Incomplete array types
+The last field of a struct can be an
+.Dq incomplete
+array type,
+which means it doesn't have a length.
+A variable amount of space for the struct can be
+.Xr malloc 3 Ap d ,
+or the struct can be used as
+a sort of pointer with fields.
+.
+.Bd -literal -offset indent
+struct Line {
+	enum Filter type;
+	uint8_t data[];
+} *line = &png.data[1 + lineSize()];
+.Ed
+.
+.Ss Anonymous struct and union fields (C11)
+Members of structs or unions
+which are themselves structs or unions
+can be unnamed.
+In that case,
+each of the inner fields
+is treated as a member of the outer struct or union.
+This makes working with tagged unions nicer.
+.
+.Bd -literal -offset indent
+struct Message {
+	enum { Foo, Bar } type;
+	union {
+		uint8_t foo;
+		uint32_t bar;
+	};
+} msg = { .type = Foo, .foo = 0xFF };
+.Ed
+.
+.Ss Static assert (C11)
+Assertions can be made at compile time.
+Most useful for checking sizes of structs.
+.
+.Bd -literal -offset indent
+static_assert(13 == sizeof(struct PNGHeader), "PNG IHDR size");
+.Ed
+.
+.Ss Leading-break switch
+This one is just an odd style choice
+I came across that C happens to allow.
+To prevent accidental fall-through
+in switch statements,
+you can put breaks before the case labels.
+.
+.Bd -literal -offset indent
+while (0 < (opt = getopt(argc, argv, "h:w:"))) {
+	switch (opt) {
+		break; case 'h': height = optarg;
+		break; case 'w': width = optarg;
+		break; default:  return EX_USAGE;
+	}
+}
+.Ed
+.
+.Sh AUTHORS
+.An Mt june@causal.agency
+.
+.Pp
+This document is produced from
+.Xr mdoc 7
+source available from
+.Lk https://git.causal.agency/src/tree/www/text.causal.agency
+.
+.Sh CAVEATS
+This isn't meant to be advice.
+It's just how I like to write C,
+and I don't
+.Dq ship
+software in C.
diff --git a/www/text.causal.agency/004-uloc.7 b/www/text.causal.agency/004-uloc.7
new file mode 100644
index 00000000..edd78d80
--- /dev/null
+++ b/www/text.causal.agency/004-uloc.7
@@ -0,0 +1,64 @@
+.Dd December 14, 2018
+.Dt ULOC 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm ULOC
+.Nd unique lines of code
+.
+.Sh DESCRIPTION
+There are many tools available
+which measure SLOC: source lines of code.
+These tools are strangely complex
+for what they intend to do,
+which is to estimate the relative sizes of projects.
+They perform some amount of parsing
+in order to discount comments in various languages,
+and for reasons unknown each format their ouput
+in some oddly encumbered way.
+.
+.Pp
+I propose a much simpler method
+of estimating relative sizes of projects:
+unique lines of code.
+ULOC can be calculated with standard tools as follows:
+.
+.Bd -literal -offset indent
+sort -u *.h *.c | wc -l
+.Ed
+.
+.Pp
+In my opinion,
+the number this produces
+should be a better estimate of
+the complexity of a project.
+Compared to SLOC,
+not only are blank lines discounted,
+but so are close-brace lines
+and other repetitive code
+such as common includes.
+On the other hand,
+ULOC counts comments,
+which require just as much maintenance
+as the code around them does,
+while avoiding inflating the result
+with license headers which appear in every file,
+for example.
+.
+.Pp
+It can also be amusing
+to read all of your code sorted alphabetically.
+.
+.Sh AUTHORS
+.An Mt june@causal.agency
+.
+.Pp
+This document is produced from
+.Xr mdoc 7
+source available from
+.Lk https://git.causal.agency/src/tree/www/text.causal.agency
+.
+.Sh CAVEATS
+Estimates such as these
+should not be used for decision making
+as if they were data.
diff --git a/www/text.causal.agency/005-testing-c.7 b/www/text.causal.agency/005-testing-c.7
new file mode 100644
index 00000000..d0c636ff
--- /dev/null
+++ b/www/text.causal.agency/005-testing-c.7
@@ -0,0 +1,73 @@
+.Dd December 21, 2018
+.Dt TESTING-C 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Testing C
+.Nd a simple unit testing setup
+.
+.Sh DESCRIPTION
+This is a simple approach
+to unit testing in C
+that I've used in a couple projects.
+At the bottom of a C file
+with some code I want to test,
+I add:
+.
+.Bd -literal -offset indent
+#ifdef TEST
+#include <assert.h>
+
+int main(void) {
+	assert(...);
+	assert(...);
+}
+
+#endif
+.Ed
+.
+.Pp
+This file normally produces a
+.Pa .o
+to be linked into the main binary.
+For testing,
+I produce separate binaries
+and run them with
+.Xr make 1 :
+.
+.Bd -literal -offset indent
+TESTS = foo.t bar.t
+
+\&.SUFFIXES: .t
+
+\&.c.t:
+	$(CC) $(CFLAGS) -DTEST $(LDFLAGS) $< $(LDLIBS) -o $@
+
+test: $(TESTS)
+	set -e; $(TESTS:%=./%;)
+.Ed
+.
+.Pp
+Note that the test binaries
+aren't linked with the rest of the code,
+so there is potential for simple stubbing or mocking.
+.
+.Pp
+To get the best output
+from C's simple
+.Xr assert 3 ,
+it's best to assert the result
+of a helper function
+which takes the expected output
+and the test input,
+rather than calling
+.Xr assert 3
+inside the helper function.
+This way,
+the message printed by the assert failure
+contains a useful line number
+and the expected output
+rather than just variable names.
+.
+.Sh AUTHORS
+.An Mt june@causal.agency
diff --git a/www/text.causal.agency/006-some-libs.7 b/www/text.causal.agency/006-some-libs.7
new file mode 100644
index 00000000..5af65404
--- /dev/null
+++ b/www/text.causal.agency/006-some-libs.7
@@ -0,0 +1,96 @@
+.Dd December 11, 2019
+.Dt SOME-LIBS 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Some Libraries
+.Nd good ones
+.
+.Sh DESCRIPTION
+This is a little list of C libraries
+I've had good experiences using.
+.
+.Bl -tag -width Ds
+.It Fl lcurl
+The library behind the
+.Xr curl 1
+command.
+It downloads or uploads things on the internet
+through a number of protocols,
+not just HTTP.
+It has an easy-to-use library API,
+appropriately named
+.Xr libcurl-easy 3 .
+I've used it to implement a
+.Lk https://causal.agency/bin/title.html "page title fetcher" .
+.
+.It Fl lcurses
+Okay so this one really isn't great.
+Its interfaces can seem archaic
+and its documentation is often poor.
+However, it gets the job done
+and is commonly available pretty much everywhere.
+Interesting to note that
+.Nx
+uses its own implementation of curses
+that is not GNU ncurses,
+unlike
+.Fx .
+.
+.It Fl ledit
+This is a BSD line editing library,
+similar to GNU readline.
+It supports right-aligned prompts,
+which I prefer for variable-length
+information in shells.
+.
+.It Fl lkcgi
+A CGI and FastCGI library
+for web applications in C.
+Don't worry,
+it isolates HTTP parsing and input validation
+from application logic
+in sandboxed processes.
+I think it's an excellent example
+of how to design an API for C.
+I used it to implement the
+.Lk https://ascii.town/explore.html "torus web viewer" .
+.
+.It Fl lsqlite3
+An embedded relational database engine.
+It's amazing what you can do with this,
+and it's super easy to use!
+My one gripe with it is that the library and SQL documentation
+are not available as
+.Xr man 1
+pages.
+I'm currently working on a project using SQLite,
+but it hasn't gotten very far yet.
+.
+.It Fl ltls
+This is a new library in LibreSSL
+which provides a much simpler interface for TLS sockets
+compared to
+.Fl lssl .
+It's much more like what you'd expect
+from other TLS socket wrappers,
+with calls like
+.Xr tls_connect 3 ,
+.Xr tls_read 3
+and
+.Xr tls_write 3 .
+I've used this for IRC clients, bouncers and bots.
+.
+.It Fl lz
+An implementation of the DEFLATE compression algorithm
+and gzip format.
+It's all documented in comments in
+.In zlib.h ,
+which isn't bad,
+but for my own use I copied the docs into
+.Lk https://code.causal.agency/june/zlib-man-pages "manual pages" .
+I've used this for decoding and encoding PNG images.
+.El
+.
+.Sh AUTHORS
+.An June Bug Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/007-cgit-setup.7 b/www/text.causal.agency/007-cgit-setup.7
new file mode 100644
index 00000000..44fb436a
--- /dev/null
+++ b/www/text.causal.agency/007-cgit-setup.7
@@ -0,0 +1,271 @@
+.Dd December 15, 2019
+.Dt CGIT-SETUP 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm cgit setup
+.Nd configuration notes
+.
+.Sh DESCRIPTION
+I just set up cgit on
+.Lk https://git.causal.agency
+to replace an instance of gitea.
+After 30 days of uptime,
+gitea had accumulated over 11 hours of CPU time
+and was using hundreds of megabytes of memory.
+cgit is much more lightweight
+and much more in line with my aesthetic.
+I'm documenting how I set it up here
+mostly to remind myself in the future.
+.
+.Ss slowcgi
+cgit is CGI software,
+but
+.Xr nginx 8
+only supports FastCGI.
+I used
+.Xr slowcgi 8
+as a compatibility layer
+by adding the following to
+.Pa /etc/rc.conf :
+.Bd -literal -offset indent
+slowcgi_enable="YES"
+slowcgi_flags="-p / -s /var/run/slowcgi.sock"
+.Ed
+.
+.Ss nginx
+I added the following in a new
+.Cm server
+block to
+.Pa /usr/local/etc/nginx/nginx.conf :
+.Bd -literal -offset indent
+root /usr/local/www/cgit;
+location / {
+	try_files $uri @cgit;
+}
+location @cgit {
+	fastcgi_pass unix:/var/run/slowcgi.sock;
+	fastcgi_param SCRIPT_FILENAME $document_root/cgit.cgi;
+	fastcgi_param SCRIPT_NAME /;
+	fastcgi_param PATH_INFO $uri;
+	fastcgi_param QUERY_STRING $query_string;
+	fastcgi_param REQUEST_METHOD $request_method;
+	fastcgi_param CONTENT_TYPE $content_type;
+	fastcgi_param CONTENT_LENGTH $content_length;
+	fastcgi_param HTTPS $https if_not_empty;
+	fastcgi_param SERVER_PORT $server_port;
+	fastcgi_param SERVER_NAME $server_name;
+}
+.Ed
+.
+.Pp
+The
+.Cm try_files
+directive causes
+.Xr nginx 8
+to first try to serve static files from
+.Pa /usr/local/www/cgit
+before passing anything else on to FastCGI.
+.
+.Pp
+The
+.Va SCRIPT_FILENAME
+parameter tells
+.Xr slowcgi 8
+the path of the CGI binary to run.
+Setting
+.Va SCRIPT_NAME
+to
+.Pa /
+tells cgit its root URL
+and avoids it using query strings for everything.
+.
+.Ss cgit
+cgit doesn't provide any configuration to start from,
+so you have to just read
+.Xr cgitrc 5 .
+I added the following to
+.Pa /usr/local/etc/cgitrc :
+.Bd -literal -offset indent
+cache-size=1024
+clone-url=https://$HTTP_HOST/$CGIT_REPO_URL
+snapshots=tar.gz zip
+remove-suffix=1
+enable-git-config=1
+scan-path=/home/june/pub
+.Ed
+.
+.Pp
+The
+.Cm cache-size
+option enables caching,
+which by default is stored in
+.Pa /var/cache/cgit ,
+so I made sure that directory exists
+and is writable by the
+.Sy www
+user.
+The
+.Cm clone-url
+option sets the clone URL to advertise.
+cgit will automatically serve git over HTTP.
+The
+.Cm snapshots
+option makes tarballs available for tags and commits.
+.
+.Pp
+The
+.Cm scan-path
+option causes cgit to scan the given path
+for git repositories.
+I'm putting mine in
+.Pa ~/pub .
+The
+.Cm remove-suffix
+option causes cgit to remove the
+.Pa .git
+suffix from the URLs it uses
+for the repositories it finds,
+so that
+.Pa ~/pub/pounce.git
+is served at
+.Pa /pounce .
+The
+.Cm enable-git-config
+option allows controlling some cgit options
+from the
+.Xr git-config 1
+of each repository.
+See
+.Sx git
+below.
+.
+.Pp
+I also set up a filter to render
+.Xr mdoc 7
+files
+and do syntax highlighting
+by adding the following to
+.Pa cgitrc :
+.Bd -literal -offset indent
+readme=:README.7
+readme=:README
+about-filter=/usr/local/libexec/cgit-filter
+source-filter=/usr/local/libexec/cgit-filter
+.Ed
+.
+.Pp
+The
+.Cm readme
+options tell cgit which files to look for
+to render the
+.Dq about
+page.
+The colon prefix causes it to look for them
+in the git tree.
+The
+.Pa /usr/local/libexec/cgit-filter
+script contains the following:
+.Bd -literal -offset indent
+#!/bin/sh
+case "$1" in
+	(*.[1-9])
+		/usr/bin/mandoc -T utf8 | /usr/local/libexec/ttpre
+		;;
+	(*)
+		exec /usr/local/libexec/hi -t -n "$1" -f html -o anchor
+		;;
+esac
+.Ed
+.
+.Pp
+Filter scripts are run with the filename as their first argument
+and the contents of the file on standard input.
+The
+.Xr ttpre 1
+command is my own utility to convert
+.Xr man 1
+output to HTML.
+The
+.Xr hi 1
+command is my own
+.Lk https://causal.agency/bin/hi.html "syntax highlighter" .
+.
+.Ss git
+I create my repositories in
+.Pa ~/pub
+with
+.Ql git init --bare
+and use
+.Pa git.causal.agency:pub/example.git
+locally as the remote.
+Descriptions are set by editing the
+.Pa description
+file in each repository.
+The section and homepage can be set with
+.Xr git-config 1
+through the keys
+.Cm cgit.section
+and
+.Cm cgit.homepage ,
+respectively,
+thanks to the
+.Cm enable-git-config
+option above.
+.
+.Ss Redirects
+I added the following to the
+.Cm server
+block that used to serve gitea in
+.Pa nginx.conf :
+.Bd -literal -offset indent
+location ~* /june/([^.]+)[.]git(.*) {
+	return 301 https://git.causal.agency/$1$2?$query_string;
+}
+location ~* /june/([^/]+) {
+	return 301 https://git.causal.agency/$1;
+}
+location / {
+	return 301 https://git.causal.agency;
+}
+.Ed
+.
+.Pp
+This redirects any links to my gitea repos
+to the corresponding repo in cgit.
+The first
+.Sy location
+block also redirects gitea HTTP clone URLs to cgit
+so that
+.Xr git-pull 1
+continues to work on existing clones.
+.
+.Ss Update: fast HTTPS clones
+Someone pointed out that cloning my repos
+over HTTPS was incredibly slow,
+and this is because cgit only implements the
+.Dq dumb
+HTTP git transport.
+To speed up cloning,
+I send the URLs used by the
+.Dq smart
+HTTP transport to
+.Xr git-http-backend 1
+instead:
+.Bd -literal -offset indent
+location ~ /.+/(info/refs|git-upload-pack) {
+	fastcgi_pass unix:/var/run/slowcgi.sock;
+	fastcgi_param SCRIPT_NAME /usr/local/libexec/git-core/git-http-backend;
+	fastcgi_param GIT_HTTP_EXPORT_ALL 1;
+	fastcgi_param GIT_PROJECT_ROOT /home/june/pub;
+	include fastcgi_params;
+}
+.Ed
+.
+.Pp
+I factored out the FastCGI parameters
+I'm using with cgit
+to be included here as well.
+.
+.Sh AUTHORS
+.An June Bug Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/008-how-irc.7 b/www/text.causal.agency/008-how-irc.7
new file mode 100644
index 00000000..aba1bbf9
--- /dev/null
+++ b/www/text.causal.agency/008-how-irc.7
@@ -0,0 +1,193 @@
+.Dd March  8, 2020
+.Dt HOW-IRC 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm How I Relay Chat
+.Nd in code
+.
+.Sh DESCRIPTION
+I've been writing a lot of IRC software lately
+.Pq Sx SEE ALSO ,
+and developed some nice code patterns
+that I've been reusing.
+Here they are.
+.
+.Ss Parsing
+I use fixed size buffers almost everywhere,
+so it's necessary to know IRC's size limits.
+A traditional IRC message is a maximum of 512 bytes,
+but the IRCv3 message-tags spec adds
+(unreasonably, in my opinion)
+8191 bytes for tags.
+IRC messages also have a maximum of 15 command parameters.
+.Bd -literal -offset indent
+enum { MessageCap = 8191 + 512 };
+enum { ParamCap = 15 };
+.Ed
+.
+.Pp
+If I'm using tags,
+I'll use X macros
+to declare the set I care about.
+X macros are a way of maintaining parallel arrays,
+or in this case an enum and an array.
+.Bd -literal -offset indent
+#define ENUM_TAG \e
+	X("msgid", TagMsgid) \e
+	X("time", TagTime)
+
+enum Tag {
+#define X(name, id) id,
+	ENUM_TAG
+#undef X
+	TagCap,
+};
+
+static const char *TagNames[TagCap] = {
+#define X(name, id) [id] = name,
+	ENUM_TAG
+#undef X
+};
+.Ed
+.
+.Pp
+The TagNames array is used by the parsing function
+to assign tag values into the message structure,
+which looks like this:
+.Bd -literal -offset indent
+struct Message {
+	char *tags[TagCap];
+	char *nick;
+	char *user;
+	char *host;
+	char *cmd;
+	char *params[ParamCap];
+};
+.Ed
+.
+.Pp
+I'm a fan of using
+.Xr strsep 3
+for simple parsing.
+Although it modifies its input
+(replacing delimiters with NUL terminators),
+since the raw message is in a static buffer,
+it is ideal for so-called zero-copy parsing.
+I'm not going to include the whole parsing function here,
+but I will at least include the part that many get wrong,
+which is dealing with the colon-prefixed trailing parameter:
+.Bd -literal -offset indent
+msg.cmd = strsep(&line, " ");
+for (int i = 0; line && i < ParamCap; ++i) {
+	if (line[0] == ':') {
+		msg.params[i] = &line[1];
+		break;
+	}
+	msg.params[i] = strsep(&line, " ");
+}
+.Ed
+.
+.Ss Handling
+To handle IRC commands and replies
+I add handler functions to a big array.
+I usually have some form of helper as well
+to check the number of expected parameters.
+.Bd -literal -offset indent
+typedef void HandlerFn(struct Message *msg);
+
+static const struct Handler {
+	const char *cmd;
+	HandlerFn *fn;
+} Handlers[] = {
+	{ "001", handleReplyWelcome },
+	{ "PING", handlePing },
+	{ "PRIVMSG", handlePrivmsg },
+};
+.Ed
+.
+.Pp
+Since I keep these arrays sorted anyway,
+I started using the standard
+.Xr bsearch 3
+function,
+but a basic for loop probably works just as well.
+I do wish I could compile-time assert
+that the array really is sorted, though.
+.Bd -literal -offset indent
+static int compar(const void *cmd, const void *_handler) {
+	const struct Handler *handler = _handler;
+	return strcmp(cmd, handler->cmd);
+}
+
+void handle(struct Message msg) {
+	if (!msg.cmd) return;
+	const struct Handler *handler = bsearch(
+		msg.cmd,
+		Handlers, ARRAY_LEN(Handlers),
+		sizeof(*handler), compar
+	);
+	if (handler) handler->fn(&msg);
+}
+.Ed
+.
+.Ss Capabilities
+For IRCv3 capabilties
+I use X macros again,
+this time with another handy macro
+for declaring bit flag enums.
+.Bd -literal -offset indent
+#define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit
+
+#define ENUM_CAP \e
+	X("message-tags", CapMessageTags) \e
+	X("sasl", CapSASL) \e
+	X("server-time", CapServerTime)
+
+enum Cap {
+#define X(name, id) BIT(id),
+	ENUM_CAP
+#undef X
+};
+
+static const char *CapNames[] = {
+#define X(name, id) [id##Bit] = name,
+	ENUM_CAP
+#undef X
+};
+.Ed
+.
+.Pp
+The
+.Fn BIT
+macro declares, for example,
+.Dv CapSASL
+as the bit flag and
+.Dv CapSASLBit
+as the corresponding index.
+The
+.Vt "enum Cap"
+is used as a set,
+for example checking if SASL is enabled with
+.Ql caps & CapSASL .
+.
+.Pp
+These patterns are serving my IRC software well,
+and my IRC projects are serving me well.
+It is immensely satisfying
+to be (near) constantly using software
+that I wrote myself and am happy with,
+regardless of how niche it may be.
+.
+.Sh SEE ALSO
+.Bl -item -compact
+.It
+.Lk https://git.causal.agency/pounce/about "IRC bouncer"
+.It
+.Lk https://git.causal.agency/litterbox/about "IRC logger"
+.It
+.Lk https://git.causal.agency/catgirl/about "IRC client"
+.El
+.
+.Sh AUTHORS
+.An June Bug Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/009-casual-update.7 b/www/text.causal.agency/009-casual-update.7
new file mode 100644
index 00000000..0548436a
--- /dev/null
+++ b/www/text.causal.agency/009-casual-update.7
@@ -0,0 +1,127 @@
+.Dd May  6, 2020
+.Dt CASUAL-UPDATE 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm casual update
+.Nd software developments
+.
+.Sh DESCRIPTION
+I've been figuring out more of IMAP
+and Internet Messages in general
+while working on a new project
+so I've revisited some older ones.
+I've copied my somewhat more proper
+IMAP parsing code into them,
+so they should be more robust.
+.
+.Pp
+.Xr imbox 1
+is my tool to export messages
+in mboxrd format directly from IMAP.
+It's mostly for applying patches sent by email
+without having any kind of local mail setup.
+For that,
+it includes the
+.Xr git-fetch-email 1
+wrapper which works very similarly to
+.Xr git-send-email 1 .
+I learned by reading the source of
+.Xr git-subtree 1
+that
+.Xr git-rev-parse 1
+can be used by shell scripts
+to parse long options,
+so I added those.
+I also added the
+.Fl Fl apply
+flag to automatically pipe to
+.Xr git-am 1
+with the right flags for mboxrd.
+.
+.Pp
+.Xr notemap 1
+is a tool for mirroring text files
+to an IMAP Notes mailbox,
+which is used by FastMail's web UI
+and the macOS/iOS Notes app.
+Its original parsing code
+was particularly ad-hoc.
+Since I've now learned
+how UTF-8 headers are encoded,
+I updated it to properly encode
+the file name in the Subject line.
+.
+.Pp
+I also got distracted by
+a conversation about UNIX-domain sockets
+where I was comparing the macOS and FreeBSD
+.Xr unix 4
+pages and the Linux
+.Xr unix 7
+page.
+This lead me to make
+.Xr exman 1 ,
+a tool to locally install and read
+manual pages for Linux, POSIX,
+.Fx ,
+.Nx
+and
+.Ox .
+I've already gotten quite a bit of use out of it.
+.
+.Pp
+In yet another IRC distraction,
+I was talking about some further plans for my IRC software,
+and realized it might be time to write
+my future projects list down.
+I opened a
+.Pa .plan
+file,
+immediately wondered how anyone can write plain text,
+then switched to a
+.Pa plan.7
+file.
+There's nothing I won't use
+.Xr mdoc 7
+for.
+After a little setup,
+I can now be fingered,
+and make jokes about this silly little protocol
+from the days of old.
+.Xr finger 1 Ap s
+default output fills me with joy:
+.Bd -unfilled -offset indent
+No Mail.
+No Plan.
+.Ed
+.
+.Pp
+And speaking of IRC and plans,
+I've been meaning to tag
+.Xr catgirl 1
+version 1.0 for a while now.
+I've been using it as my main client
+and my commits to it have really slowed down.
+When I do tag it,
+I'm planning on writing another post
+about my whole
+.Dq suite
+of IRC software
+and how the parts work together.
+Watch this space.
+.
+.Sh SEE ALSO
+.Bl -item -compact
+.It
+.Lk https://git.causal.agency/imbox "imbox"
+.It
+.Lk https://git.causal.agency/notemap "notemap"
+.It
+.Lk https://git.causal.agency/exman "exman"
+.It
+.Lk https://git.causal.agency/catgirl "catgirl"
+.El
+.
+.Sh AUTHORS
+.An June Bug Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/010-irc-suite.7 b/www/text.causal.agency/010-irc-suite.7
new file mode 100644
index 00000000..515a30ab
--- /dev/null
+++ b/www/text.causal.agency/010-irc-suite.7
@@ -0,0 +1,409 @@
+.Dd June 19, 2020
+.Dt IRC-SUITE 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm IRC suite
+.Nd my own IRC software
+.
+.Sh DESCRIPTION
+Over the past months
+.Po
+eight of them, according to
+.Xr git-log 1
+.Pc
+I developed a new
+.Dq suite
+of IRC software
+that I now use full-time,
+consisting of a bouncer,
+a new logging and search solution,
+and a terminal client.
+These new programs share some characteristics:
+they are all TLS-only
+and use the libtls API from LibreSSL,
+they can all be entirely configured from the command line
+or with equivalent configuration files,
+they are all designed as
+a one process to one IRC connection mapping,
+and they all take advantage of IRCv3 features.
+.
+.Pp
+For context,
+I was previously running
+the znc IRC bouncer
+and using the Textual IRC client
+with its plain text logs.
+I also continue to use
+the Palaver IRC client for iOS.
+.
+.Ss Background
+A bouncer is a piece of server software
+that stays connected to IRC at all times
+and acts as a relay
+between your client and the IRC server.
+When the client is disconnected,
+the bouncer buffers incoming messages
+to send to the client when it reconnects.
+.
+.Pp
+Aside from this,
+bouncers have another advantage:
+client multiplexing.
+Several clients,
+for instance on different computers
+or a phone,
+should be able to connect to the same bouncer,
+and send and receive messages under the same nick.
+Unfortunately,
+znc does not handle this use-case well at all.
+Out of the box it offers two options:
+either any client connection totally clears the buffer,
+causing other clients to miss chat history;
+or the buffer is never cleared,
+causing every client connection
+to be repeatedly spammed with redundant history.
+There is also a znc wiki page
+that suggests one way to solve this issue
+is to connect znc to itself multiple times over.
+Yikes.
+.
+.Ss pounce
+My dissatisfaction with
+connecting multiple clients to znc
+directly motivated me to start working
+on a new multi-client-focused IRC bouncer.
+The result is
+.Xr pounce 1 ,
+based on a rather straightforward
+single-producer (the IRC server)
+multiple-consumer (each IRC client)
+ring buffer.
+Each client has its own
+independent position in the buffer
+so can properly be brought up to date
+whenever it connects.
+.
+.Pp
+Additionally,
+by assuming support for the IRCv3
+.Sy server-time
+extension,
+all IRC events can be accurately
+relayed to clients at any time,
+and the internals of
+.Xr pounce 1
+can be kept very simple.
+In fact,
+it completely avoids parsing most IRC messages,
+simply pushing them into the buffer
+with an associated timestamp.
+.
+.Pp
+The usernames sent by clients during registration
+are used as opaque identifiers for buffer consumers.
+This was chosen since most clients
+can be configured to send an arbitrary username,
+and those that don't often default
+to the name of the client itself,
+making it an appropriate identifier.
+.
+.Pp
+Later,
+I added a way for clients
+to be informed of their own buffer positions
+using a vendor-specific IRCv3 capability.
+This means a client
+can save the position
+of the last message it actually received,
+and request to set its position
+when it reconnects,
+ensuring no messages are lost
+to network issues
+or software crashes.
+.
+.Ss calico
+Due to the simple design of mapping
+one process to one IRC (server) connection,
+it is necessary to run several instances of
+.Xr pounce 1 .
+Initially I simply used different ports for each,
+but as I connected to more networks
+and even ran some instances for friends,
+it became less feasible.
+.
+.Pp
+The solution I came up with
+was to dispatch incoming connections
+using Server Name Indication, or SNI.
+This way,
+multiple domains pointing to the same host
+could be used with only one port
+to connect to different instances of
+.Xr pounce 1 .
+For example,
+I use a
+.Li *.irc.causal.agency
+wildcard DNS entry
+and a subdomain for each IRC network,
+all on port 6697.
+.
+.Pp
+The
+.Xr calico 1
+daemon included with pounce
+accomplishes this dispatch
+using the
+.Dv MSG_PEEK
+flag of
+.Xr recvmsg(2)
+on incoming connections.
+Since SNI is immediately sent by TLS clients
+as part of the ClientHello message in clear-text,
+it can be processed
+without doing any actual TLS.
+The connection itself is then
+sent to the corresponding
+.Xr pounce 1
+instance
+over UNIX-domain socket,
+which handles TLS as normal.
+This means that
+.Xr calico 1
+and
+.Xr pounce 1
+operate entirely independently of each other.
+.
+.Ss litterbox
+Based on the multiple-consumer ring buffer design,
+I realized it would be easy
+to implement additional functionality
+as independent purpose-built clients
+which connect to
+.Xr pounce 1
+alongside regular clients.
+This could allow dedicated OTR or DCC software
+to operate in parallel with a basic client,
+or for more passive software
+to provide notifications
+or dedicated logging.
+.
+.Pp
+For the latter,
+I wanted to do better than
+plain text log files.
+.Xr grep 1
+over files works fine,
+but search could be faster and smarter,
+and the text format is
+more lossy and less structured
+than I'd like it to be.
+Conveniently,
+SQLite provides an extension
+(actually two)
+for full-text search.
+.
+.Pp
+The litterbox project
+is my dedicated logging solution
+using SQLite FTS5.
+It consists of three tools:
+the
+.Xr litterbox 1
+daemon itself which connects to pounce
+and logs messages to SQLite,
+the
+.Xr scoop 1
+command line query tool,
+and the
+.Xr unscoop 1
+plain text import tool.
+The
+.Xr scoop 1
+tool constructs SQL queries
+and formats the results for viewing,
+with coloured nicks
+and piped to a pager
+by default.
+.
+.Pp
+The
+.Xr litterbox 1
+daemon
+can also provide a simple
+.Dq online
+.Pq over IRC
+search query interface
+to other connected clients.
+The simplest way to allow different
+.Xr pounce 1
+clients to talk to each other
+was to route private messages to self
+internally without sending them to the IRC server.
+So from any client
+I can simply message myself
+a full-text search query
+and
+.Xr litterbox 1
+responds with the results.
+.
+.Pp
+Along with routing self-messages,
+.Xr pounce 1
+also provides a vendor-specific IRCv3 capability
+for passive clients such as
+.Xr litterbox 1
+to indicate that they should not influence
+the automatic away status,
+which is normally only set
+when no clients are connected.
+.
+.Pp
+An advantage of this architecture
+of dedicated clients
+rather than bouncer modules
+is that they need not run
+on the same host.
+I run my bouncers on a VPS,
+but I'd rather not store my private logs there,
+so
+.Xr litterbox 1
+runs instead on a Raspberry Pi
+in my apartment.
+Also,
+since it is essentially
+just a regular IRC bot,
+it could be used independently
+for keeping public logs for a channel.
+.
+.Ss catgirl
+There's not really that much to say
+about the client,
+.Xr catgirl 1 .
+Of the three projects
+it contains the most code
+but is also the least interesting,
+in my opinion.
+It just does what I want a client to do,
+and gets the details right.
+.
+.Pp
+Tab complete is ordered by most recently seen or used,
+and completing several nicks
+inserts commas between them
+as well as the colon following the final nick.
+In the input line,
+the prompt is updated
+to reflect whether the input
+will be interpreted as a command or as a message.
+Messages are automatically scanned for URLs,
+which can be opened or copied with commands
+specifying the nick or a substring of the URL.
+.
+.Pp
+Scrolling in a window creates a split view,
+keeping the latest messages visible.
+Nick colours are based instead on usernames,
+keeping them more stable across renames,
+and mentions in messages are coloured
+to make the conversation easier to follow.
+The visibility of ignored messages
+can be toggled at any time.
+Channels can be muted
+so their activity is hidden
+from the status line
+unless you are pinged.
+.
+.Pp
+.Xr catgirl 1
+is configured entirely on the command line
+or in equivalent simple configuration files.
+There's no dynamic manipulation of state
+using complex
+.Ql /
+commands like in some other clients.
+.
+.Pp
+The major caveat is that
+.Xr catgirl 1
+connects to only one network at a time.
+This keeps the configuration, the interface
+and the code much simpler.
+.Xr tmux 1 ,
+.Xr screen 1
+or a tabbed terminal emulator
+are good options to run several instances.
+.
+.Pp
+If you're interested in giving
+.Xr catgirl 1
+a quick (and necessarily limited) try,
+you can
+.Li ssh chat@ascii.town .
+.
+.Ss Future
+I think I'm done with IRC software for now.
+As mentioned above,
+there are a few more pieces
+that could fit in to this setup,
+but I don't really want or need them right now.
+One thing I definitely want to try
+at some point
+is adding a litterbox component
+to index the contents of URLs
+to make finding previously shared links easier.
+.
+.Pp
+If you try any of this software
+and have feedback,
+let me know in
+.Li #ascii.town
+on tilde.chat
+or by email.
+And of course,
+patches are always welcome.
+.
+.Ss Update: scooper
+Somehow I had the motivation
+to create a web interface for litterbox:
+.Xr scooper 1 .
+It can be used either as CGI
+or as a FastCGI worker,
+and I used the excellent
+.Xr kcgi 3
+library for it.
+.
+.Pp
+The main advantage of this interface
+is that you can click on a search result
+to be brought to its context in the log viewer.
+I also added an option to
+.Xr litterbox 1
+to provide a corresponding scooper link
+in response to its query interface.
+.
+.Pp
+A small demo of scooper is hosted at
+.Aq Lk "https://causal.agency/scooper/" .
+It publicly logs the
+.Li #litterbox
+channel on tilde.chat.
+.
+.Sh SEE ALSO
+.Bl -item -compact
+.It
+.Lk "https://git.causal.agency/pounce" pounce
+.It
+.Lk "https://git.causal.agency/litterbox" litterbox
+.It
+.Lk "https://git.causal.agency/catgirl" catgirl
+.It
+.Lk "https://www.sqlite.org/fts5.html" "SQLite FTS5 Extension"
+.It
+.Lk "https://git.causal.agency/scooper" scooper
+.It
+.Lk "https://kristaps.bsd.lv/kcgi/" kcgi
+.El
+.
+.Sh AUTHORS
+.An June Bug Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/011-libretls.7 b/www/text.causal.agency/011-libretls.7
new file mode 100644
index 00000000..c29c325e
--- /dev/null
+++ b/www/text.causal.agency/011-libretls.7
@@ -0,0 +1,220 @@
+.Dd August  9, 2020
+.Dt LIBRETLS 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm LibreTLS
+.Nd libtls for OpenSSL
+.
+.Sh DESCRIPTION
+This is a sort of announcement post about LibreTLS,
+my port of libtls from LibreSSL to OpenSSL.
+If you've wanted to try any of my software
+but have been unable to because of LibreSSL,
+LibreTLS is an option that will likely work for you.
+I'm including instructions
+for building it and my IRC software
+on Debian as an example,
+since manually installing libraries
+is less straightforward than it could be.
+.
+.Pp
+libtls is
+.Do
+a new TLS library,
+designed to make it easier to write foolproof applications
+.Dc .
+It was developed as part of LibreSSL,
+.Ox Ap s
+fork of OpenSSL,
+and is implemented against their version of libssl.
+It provides a nice high-level API
+for TLS sockets,
+with functions like
+.Xr tls_connect 3 ,
+.Xr tls_read 3
+and
+.Xr tls_write 3 .
+This is a vast improvement over libssl's
+confusing mess of an API!
+Its relative obscurity is a real shame
+for C programmers.
+.
+.Pp
+An obvious cause of its obscurity
+is that it is tied to LibreSSL.
+Although LibreSSL is available
+for platforms other than
+.Ox ,
+it conflicts with OpenSSL
+so is difficult to install alongside it
+and is often not packaged at all.
+Additionally,
+even if a user manually installs LibreSSL,
+libtls is likely not to work on some distros
+due to its hardcoded CA bundle file path.
+.
+.Pp
+Since libtls is implemented against libssl,
+which originates in OpenSSL,
+it should be possible to use libtls with it.
+This is what I set out to do in LibreTLS.
+I started by importing the sources
+from a LibreSSL-portable release,
+then worked on porting the portions
+that were incompatible with OpenSSL.
+.
+.Pp
+The simpler changes just involved
+replacing internal struct field accesses
+with public APIs.
+libtls accesses libssl internals
+using a hack to get the header files
+to declare private struct fields,
+and for basically no reason.
+The bigger changes involved
+reimplementing some functions
+which only exist in LibreSSL,
+but these were still quite small.
+I also imported the necessary compatibility functions
+from LibreSSL's libcrypto
+and adapated the autotools build files
+to produce only a libtls
+which depends on OpenSSL.
+.
+.Pp
+Along the way
+I decided to make one small behavioural change
+in order for LibreTLS to be more likely
+to work for everyone.
+I removed the hardcoded CA file path
+and changed the default configuration
+to use OpenSSL's default CA paths,
+which include a CA directory.
+This seems to be the preferred CA source
+on systems such as Debian,
+where the default CA file path doesn't exist.
+.
+.Pp
+I think the reason LibreSSL
+wants to avoid using a CA directory
+is so that it can fully load the CA file
+once before being sandboxed.
+However,
+using OpenSSL's default configuration,
+the CA file will still be loaded immediately
+if it exists.
+If it doesn't exist,
+sandboxed applications
+will fail when trying to
+load certificates from the directory,
+but unsandboxed applications
+will work just fine.
+Since LibreSSL's libtls
+would fail either way,
+I think the new behaviour
+is an improvement.
+.
+.Pp
+Another advantage of separating libtls from LibreSSL
+is that it is unencumbered by OpenSSL's
+awkward double-license,
+both of which are incompatible with the GPL.
+libtls is all new ISC-licensed code,
+and future versions of OpenSSL (3.0)
+will be released under the Apache 2.0 license,
+which is compatible with GPLv3.
+In the future,
+GPL software will be able to link with
+libtls and OpenSSL without additional permissions.
+.
+.Pp
+It's also worth noting that LibreSSL
+likely will not be able to import any code
+from future versions of OpenSSL,
+since Apache 2.0 is on
+.Ox Ap s
+license shitlist.
+LLVM is also slowly changing their license
+to Apache 2.0,
+so it'll be interesting to see what
+.Ox
+does.
+.
+.Ss Installing Manually
+To install LibreTLS on Debian,
+for example,
+fetch a release tarball from
+.Lk https://causal.agency/libretls/
+and install the build dependencies:
+.Bd -literal -offset indent
+sudo apt-get install build-essential libssl-dev pkgconf
+.Ed
+.
+.Pp
+.Xr pkgconf 1
+isn't a dependency of LibreTLS itself,
+but it's how my software
+configures its build
+for a dependency on libtls.
+The usual build steps
+will install the library:
+.Bd -literal -offset indent
+\&./configure
+make all
+sudo make install
+.Ed
+.
+.Pp
+The library will be installed in
+.Pa /usr/local/lib
+by default,
+and you need to make sure
+the dynamic linker
+will be able to find it there.
+On Debian,
+.Pa /usr/local/lib
+already appears in
+.Pa /etc/ld.so.conf.d/libc.conf ,
+but on other systems
+you'll probably need to add it to either
+.Pa /etc/ld.so.conf
+or a new file such as
+.Pa /etc/ld.so.conf.d/local.conf .
+Once the library is installed
+and the path is configured,
+the linker cache needs to be refreshed:
+.Bd -literal -offset indent
+sudo ldconfig
+.Ed
+.
+.Pp
+You'll probably also need to set
+.Ev PKG_CONFIG_PATH
+for the configure scripts
+of my software:
+.Bd -literal -offset indent
+PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./configure
+.Ed
+.
+.Pp
+On
+.Fx ,
+LibreTLS and some of my IRC software
+can be installed from my own
+.Lk https://git.causal.agency/ports/ "ports tree"
+.
+.Sh SEE ALSO
+.Bl -item -compact
+.It
+.Lk https://git.causal.agency/libretls/about LibreTLS
+.It
+.Lk https://man.openbsd.org/tls_init.3 "libtls API documentation"
+.El
+.
+.Pp
+Another alternative libtls implementation,
+.Lk https://sr.ht/~mcf/libtls-bearssl/ "libtls-bearssl"
+.
+.Sh AUTHORS
+.An June Bug Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/012-inability.7 b/www/text.causal.agency/012-inability.7
new file mode 100644
index 00000000..d352143b
--- /dev/null
+++ b/www/text.causal.agency/012-inability.7
@@ -0,0 +1,39 @@
+.Dd November 26, 2020
+.Dt INABILITY 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Inability
+.Nd losing the ability to create
+.
+.Sh DESCRIPTION
+For often weeks, sometimes months at a time,
+I lose the ability to write new code.
+I can still make fixes
+and little cleanups
+in my existing projects,
+but if I try to work on something new,
+nothing happens.
+I can't get anything done.
+.
+.Pp
+I think it's now been
+over 3 months
+since I've created anything.
+I don't know what to do about it.
+In the past I've eventually
+regained the ability to code,
+but it's unclear to me how or why.
+I also don't know what
+I should be doing instead.
+Writing code is the only hobby
+I've ever really developed,
+so without it I basically
+don't do anything.
+.
+.Pp
+Does this happen to anyone else?
+How do you cope?
+.
+.Sh AUTHORS
+.Mt june@causal.agency
diff --git a/www/text.causal.agency/013-hot-tips.7 b/www/text.causal.agency/013-hot-tips.7
new file mode 100644
index 00000000..63b6e353
--- /dev/null
+++ b/www/text.causal.agency/013-hot-tips.7
@@ -0,0 +1,156 @@
+.Dd December  2, 2020
+.Dt HOT-TIPS 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm hot tips
+.Nd from my files
+.
+.Sh DESCRIPTION
+This is a short list of tips
+from my configuration files and code
+that might be useful.
+.
+.Ss Shell
+.Bl -tag -width Ds
+.It CDPATH=:~
+This is useful if you sometimes type,
+for example,
+.Ql cd src/bin
+wanting to go to
+.Pa ~/src/bin
+but you aren't in
+.Pa ~ .
+If the path doesn't exist
+in the current directory,
+.Ic cd
+will try it in
+.Pa ~
+as well.
+.
+.It alias ls='LC_COLLATE=C ls'
+This makes it so that
+.Xr ls 1
+lists files in ASCIIbetical order,
+which puts capitalized names like
+.Pa README
+and
+.Pa Makefile
+first.
+.
+.It git config --global commit.verbose true
+Not shell but close enough.
+This makes it so the entire diff is shown
+below the usual summary
+in the editor for a
+.Xr git-commit(1)
+message.
+Useful for doing a quick review
+of what you're committing.
+.El
+.
+.Ss (neo)vim
+.Bl -tag -width Ds
+.It set inccommand=nosplit
+This is the only
+.Xr nvim 1
+feature I really care about
+aside from the improved defaults.
+This provides a live preview of what a
+.Ic :s
+substitution command will do.
+It makes it much easier to
+write complex substitutions.
+.
+.It nmap <leader>s vip:sort<CR>
+This mapping sorts the lines of a paragraph,
+or block of text separated by blank lines.
+I use this a lot to sort
+#include directives.
+.
+.It nmap <leader>S $vi{:sort<CR>
+Similar to the last mapping,
+this one sorts lines inside braces.
+I use this to sort
+switch statement cases
+or array initializers.
+.
+.It nmap <leader>a m':0/^#include <<CR>:nohlsearch<CR>O#include <
+I use this mapping to add new
+#include directives,
+usually followed by
+.Ic <leader>s
+and
+.Ic ''
+to sort them
+and return to where I was.
+.
+.It nmap <leader>d :0delete<CR>:0read !date +'.Dd \e%B \e%e, \e%Y'<CR>
+I use this to replace the first line of
+.Xr mdoc 7
+files with the current date.
+.El
+.
+.Ss C
+.Bl -tag -width Ds
+.It #define Q(...) #__VA_ARGS__
+This is what I've started using
+to quote things like SQL statements
+or HTML fragments in C.
+Anything that happens to be valid C tokens,
+which is most code,
+can be quoted this way.
+Macros are not expanded
+inside the quoted part.
+You can embed (matched) quotes
+without having to escape them.
+Whitespace gets collapsed,
+so you can write nicely formatted multi-line SQL
+that doesn't mess up your debug logging,
+for example.
+.Bd -literal -offset indent
+const char *sql = Q(
+	INSERT OR IGNORE INTO names (nick, user, host)
+	VALUES (:nick, :user, :host);
+);
+.Ed
+.
+.It #define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit
+I use this macro to declare bitflag enums.
+It takes advantage of
+auto-incrementing enum items
+so you don't need to set the values manually.
+You also get constants
+for both the bit index
+and the flag value
+for each item.
+.Bd -literal -offset indent
+enum Attr {
+	BIT(Bold),
+	BIT(Reverse),
+	BIT(Italic),
+	BIT(Underline),
+};
+.Ed
+.Pp
+For example,
+defines
+.Sy ItalicBit = 2
+and
+.Sy Italic = 1 << 2 .
+Ignore the extraneous constants.
+.
+.It typedef int FnType(const char *str, size_t len);
+You can just typedef function types!
+It annoys me more than it probably should
+that everyone writes ugly
+function pointer typedefs.
+Just stick
+.Sy typedef
+on the front of a function declaration
+and use
+.Vt FnType * .
+.El
+.
+.Sh AUTHORS
+.Mt june@causal.agency
diff --git a/www/text.causal.agency/014-using-vi.7 b/www/text.causal.agency/014-using-vi.7
new file mode 100644
index 00000000..e6a6a7a0
--- /dev/null
+++ b/www/text.causal.agency/014-using-vi.7
@@ -0,0 +1,135 @@
+.Dd January 11, 2021
+.Dt USING-VI 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Using vi
+.Nd simpler tools
+.
+.Sh DESCRIPTION
+Happy new year
+and hello from
+.Xr vi 1 !
+I'm in the mood to post something
+but not in the mood for
+.Dq social
+media.
+This one will probably be short.
+.
+.Pp
+Yesterday I was trying to work on sandboxing
+.Xr catgirl 1
+(that's the IRC client I work on)
+with
+.Xr pledge 2
+and
+.Xr unveil 2
+on
+.Ox ,
+as suggested by the maintainer of its port.
+I've done similar things before,
+but only on server software
+rather than user software.
+.
+.Pp
+Anyway I was in
+.Xr ssh 1
+to my
+.Ox
+VM
+.Po
+sadly I don't currently have any hardware to run
+.Ox
+on
+.Pc
+using my usual editor,
+which is
+.Xr nvim 1 .
+I'm honestly not very thrilled
+with what neovim is doing lately,
+but the cleaned up defaults
+make my configuration files happier.
+.
+.Pp
+The real problem with
+.Xr nvim 1 ,
+though,
+is that it's laggy as hell on
+.Ox .
+There is significant delay
+on every single keystroke,
+as if I'm typing remotely to a server
+on the other side of the world,
+but this is on a local VM!
+.
+.Pp
+So I did the only reasonable thing:
+I typed
+.Sy :qa
+followed by
+.Sy vi .
+The difference was astonishing.
+Typing and editing suddenly felt
+.Em physical
+again.
+(I put that in italics even though I know it won't render.)
+Not only was it a vast improvement over
+.Xr nvim 1
+in
+.Xr ssh 1
+in a VM,
+it was a marked improvement over
+.Xr nvim 1
+running locally and natively.
+.
+.Pp
+Now obviously
+.Xr vi 1
+doesn't have all the bells and whistles
+of newer editors,
+but of course the core editing model
+that makes
+.Xr vim 1
+and
+.Xr nvim 1
+so good is there,
+and in purer form,
+I think.
+The
+.Xr vi 1
+manual page
+is feasible to just sit down and read,
+and learn everything there is to know about the editor.
+I set up a basic configuration
+and got coding.
+.Bd -literal -offset indent
+export EXINIT='set ai ic sm sw=4 ts=4'
+.Ed
+.
+.Pp
+After I finished my
+.Xr pledge 2
+and
+.Xr unveil 2
+patch,
+I was so pleased with
+.Xr vi 1
+that I kept on using it
+yesterday and today
+for other work,
+and obviously to write this post.
+Despite the lack of editor amenities,
+its responsiveness and simplicity
+are enough to make using it
+.Em comfortable
+and perhaps
+.Em cosy .
+I'm not sure I'll ever use
+.Xr vi 1
+full-time,
+but for now I am much less likely
+to launch
+.Xr nvim 1 .
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/015-reusing-tags.7 b/www/text.causal.agency/015-reusing-tags.7
new file mode 100644
index 00000000..19546496
--- /dev/null
+++ b/www/text.causal.agency/015-reusing-tags.7
@@ -0,0 +1,155 @@
+.Dd January 17, 2021
+.Dt REUSING-TAGS 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm reusing tags
+.Nd beyond ctags
+.
+.Sh DESCRIPTION
+I've tried to start writing this post a couple times now
+and I keep getting bogged down in explanations,
+so I'm just going to tell you
+about some cool things I did
+and hope they make sense.
+.
+.Pp
+When I wrote my first syntax highlighter,
+I decided that function definitions
+should have anchor links,
+because line number anchor links
+are entirely useless
+if you expect the file to change at all.
+Since the syntax highlighter
+was somewhat deliberately just a big pile of regex,
+I hacked in more regex to try
+to identify function and type definitions.
+It wasn't elegant and it didn't always work well.
+It did work though,
+and I found the links very useful.
+.
+.Pp
+Recently I was thinking about
+the lexer generator
+.Xr lex 1
+and decided to
+rewrite the syntax highlighter
+using it.
+Really syntax highlighting
+is no different than lexical analysis.
+I ran into a problem though,
+trying to preserve my anchor link function,
+because really that should involve
+some amount of parsing.
+Trying to port my regex hacks to
+.Xr lex 1
+made the lexers way more complicated
+and less reliable,
+so I gave up on it for a while.
+.
+.Pp
+And then,
+probably in the shower,
+I realized I was approaching it
+completely from the wrong direction.
+There's already a tool that does what I want,
+and I already use it:
+.Xr ctags 1 .
+All I need to do is use its output
+to insert anchor links
+into my syntax highlighter output.
+In an afternoon I wrote
+.Xr htagml 1 ,
+which loads tag definitions for its input file,
+then scans through the input for where they match.
+It can either HTML-escape
+the input as it goes,
+or use already formatted HTML
+being piped into it from a syntax highlighter.
+.
+.Pp
+The result is three simple tools
+working together to accomplish
+what a more complex tool
+couldn't reliably achieve.
+I'm very pleased with it,
+and I've updated my site and cgit
+to use the new
+.Xr lex 1 Ns -based
+highlighter,
+.Xr ctags 1
+and
+.Xr htagml 1 .
+I'm currently missing a lexer for
+.Xr sh 1 ,
+but I plan to write it eventually.
+I also want to write a tool
+to generate tags for
+.Xr make 1 ,
+.Xr mdoc 7
+and perhaps
+.Xr sh 1 .
+The cool thing about generating more kinds of tags
+is that they'll not only improve
+the HTML output,
+they'll also be usable in my editor.
+.
+.Pp
+Speaking of generating different kinds of tags,
+I also wrote some scripts not too long ago
+for reading IETF RFCs offline.
+The plain text files are available to
+.Xr rsync 1 ,
+but they're hard to navigate on their own.
+By scanning the files for headings
+and generating tags,
+it allows jumping to sections using
+.Ic :ta
+or
+.Ic ^]
+in
+.Xr vi 1 .
+For
+.Xr nvim 1
+I also added an
+.Ic :RFC
+command to open an RFC by number
+and set up
+.Ic ^]
+to work optimally for them.
+.
+.Pp
+I'm still using
+.Xr vi 1
+for most of my editing,
+by the way.
+And of course
+.Xr ctags 1
+was made to work with it!
+Simple old tools
+are really doing it for me lately.
+.
+.Sh SEE ALSO
+.Bl -item -compact
+.It
+.Lk https://causal.agency/bin/htagml.html htagml
+.It
+.Lk https://causal.agency/bin/hilex.html hilex
+.It
+.Lk https://git.causal.agency/src/tree/doc/rfc rfctags
+.El
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
+.
+.Sh ADDENDUM
+.Xr catgirl 1 ,
+.Xr pounce 1 ,
+.Xr litterbox 1
+and
+.Xr scooper 1
+all have new releases,
+if you're using any of them.
+Also, this space is now
+available over gopher,
+if that's your sort of thing.
diff --git a/www/text.causal.agency/016-using-openbsd.7 b/www/text.causal.agency/016-using-openbsd.7
new file mode 100644
index 00000000..b843e3c3
--- /dev/null
+++ b/www/text.causal.agency/016-using-openbsd.7
@@ -0,0 +1,505 @@
+.Dd February 14, 2021
+.Dt USING-OPENBSD 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Using OpenBSD
+.Nd for real
+.
+.Sh DESCRIPTION
+Hello from
+.Ox !
+After wishing one too many times
+that I had a real BSD
+on a physical machine,
+I finally got around to
+just installing one on my
+mid-2014 MacBook Pro.
+I hadn't done it sooner
+because I didn't realize
+how easy it would be.
+It helped that I already had a
+.Dq Boot Camp
+partition with a disused Windows 8 install
+that I could replace.
+.
+.Pp
+I roughly followed an old jcs gist
+along with the
+.Ox
+Disk Setup guide.
+I'm once again happy
+that I bought a printer\(em
+they're very useful for instructions
+to install an operating system
+on your only usable computer.
+I set up encrypted softraid
+and the operating system
+installed smoothly.
+.
+.Pp
+Next I had to install rEFInd,
+since the default Mac boot manager
+is really not keen on booting much.
+Installing it requires using the
+macOS recovery partition these days.
+But there was a problem
+with my new boot menu:
+I was promised a picture of Puffy,
+and instead I just got some abstract coloured circles!
+Turns out a bunch of OS icons
+got removed from rEFInd at some point,
+and I had to rescue Puffy
+from the git history.
+.
+.Pp
+So I could happily boot
+.Ox
+by selecting Puffy,
+but I had no networking.
+I thought the wifi chip might be supported by
+.Xr bwfm 4 ,
+but I got unlucky and it's a BCM4360,
+which everything hates.
+Based on the jcs gist,
+I checked the list of hardware
+supported by the
+.Xr urtwn 4
+driver for a wifi dongle to order.
+Just having a clear list
+in the driver manual is wonderful.
+I went with the Edimax EW-7811Un v2,
+which I could get for around $20.
+It's nice and tiny,
+though it has a piercing blue LED
+(destroy all blue LEDs)
+which I had to cover with electrical tape.
+.
+.Pp
+I had to do one other thing
+before I could get it all working, though.
+When I had checked the
+.Xr urtwn 4
+hardware list,
+I had been looking at
+.Ox Ns -current ,
+but I had installed
+.Ox 6.8 ,
+and support for the v2 hardware
+I had bought was added after that release.
+So I downloaded a snapshot
+.Pa bsd.rd
+along with the
+.Xr urtwn 4
+firmware file
+to a USB drive
+and upgraded the system.
+.
+.Pp
+Connecting to wifi with
+.Xr ifconfig 8
+is a breeze, by the way,
+and then you just write the same thing to a
+.Xr hostname.if 5
+file to make it automatic.
+I wanted to use
+.Ox
+for exactly this reason:
+simple, consistent, cohesive, well-documented tools.
+.
+.Pp
+Finally, I got to configuring.
+The console is configured with
+.Xr wsconsctl 8 ,
+and similarly you can put the commands in
+.Xr wsconsctl.conf 5
+to have them run at boot.
+I added
+.Li display.brightness=50%
+to tone down the brightness,
+which is initially 100%,
+and
+.Li keyboard.backlight=0%
+to turn off those annoying lights.
+.Xr wsconsctl.conf 5
+is also where you can set
+trackpad settings if you're not using
+.Xr synaptics 4 .
+I ended up using:
+.Bd -literal -offset indent
+mouse1.tp.tapping=1
+mouse1.tp.scaling=0.2
+mouse1.reverse_scrolling=1
+.Ed
+.Pp
+This enables tapping with several fingers
+to simulate different mouse buttons,
+makes the cursor move at a reasonable speed
+and scrolling move in the right direction.
+I also set up my usual modified QWERTY layout.
+.
+.Pp
+For
+.Xr X 7
+I had enabled
+.Xr xenodm 1 ,
+which seems quite nice.
+It automatically prompts you to add your
+.Xr ssh 1
+keys to
+.Xr ssh-agent 1
+when you log in.
+One of the reasons I had not wanted
+to set up another graphical system
+is that I thought
+I would have to make too many choices,
+and that I would have to choose least bad options
+rather than actually good options,
+but
+.Ox
+already includes reasonable choices.
+I wanted to use
+.Xr cwm 1 ,
+so I started a basic
+.Pa .xsession
+file:
+.Bd -literal -offset indent
+\&. ~/.profile
+export LC_CTYPE=en_US.UTF-8
+xset r rate 175 m 5/4 0
+xmodmap ~/.config/X/modmap
+xrdb -load ~/.config/X/resources
+exec cwm -c ~/.config/cwm/cwmrc
+.Ed
+.
+.Pp
+The
+.Xr xset 1
+command sets keyboard repeat rate
+and mouse acceleration.
+I spent some time going through
+.Xr cwm 1 Ap s
+functions and writing up bindings
+that would get me something close enough
+to what I'm used to in macOS.
+Most importantly,
+putting everything on the 4 modifier (command key).
+.
+.Pp
+I also added key bindings on F1 and F2
+to adjust the brightness with
+.Xr xbacklight 1 ,
+and on F10, F11 and F12
+to adjust volume with
+.Xr sndioctl 1 .
+I'm not sure why the F keys
+just send regular F1, F2, etc.\&
+regardless of the Fn key.
+I don't use F keys for anything else though,
+so I'm not too concerned.
+Once again,
+.Xr sndioctl 1
+is such an easy straightforward tool:
+.Bd -literal -offset indent
+bind-key F10 "sndioctl output.mute=!"
+bind-key F11 "sndioctl output.level=-0.05"
+bind-key F12 "sndioctl output.level=+0.05"
+.Ed
+.
+.Pp
+For aesthetic configuration,
+I added a new output to my
+.Xr scheme 1
+colour scheme tool for
+.Xr X 7 Ns -style
+RGB and
+.Xr xterm 1
+resources.
+Normally I use the
+.Em Go Mono
+font,
+but since
+.Ox
+already includes
+.Em Luxi Mono ,
+which
+.Em Go Mono
+is based on,
+I used that.
+The most important configuration
+to make anything readable on a high-DPI display is:
+.Bd -literal -offset indent
+Xft.dpi: 144
+Xft.antialias: true
+Xft.hinting: false
+.Ed
+.
+.Pp
+I'm annoyed that I haven't found
+where these resources are actually documented.
+I would hope they'd be in
+.Xr Xft 3
+or something,
+but they're not.
+Anyway,
+turning off hinting
+seems absolutely necessary
+to prevent text from looking like garbage.
+.
+.Pp
+It seems that to get a reasonably sized cursor
+I need to install
+.Sy xcursor-dmz .
+I'd prefer if there wasn't this one
+extra package that I needed
+for a reasonable setup.
+Tangentially,
+I've never understood why
+the black versions of dmz cursors
+are called
+.Dq aa
+when it seems like that
+would stand for antialiasing
+or something.
+.Bd -literal -offset indent
+Xcursor.size: 64
+Xcursor.theme: dmz-aa
+.Ed
+.
+.Pp
+For a desktop background,
+I found a cute bitmap (little picture)
+of snowflakes already in the system
+and used colours from my usual scheme:
+.Bd -literal -offset indent
+xsetroot -bitmap /usr/X11R6/include/X11/bitmaps/xsnow \e
+	-bg rgb:14/13/0E -fg rgb:7A/49/55
+.Ed
+.
+.Pp
+Since I'd rather not install anything
+I don't have to,
+I went with the default
+.Xr xterm 1 .
+It seems more than adequate, honestly.
+I read through its RESOURCES
+section to configure it how I like.
+The important settings are
+.Sy XTerm*utf8
+and
+.Sy XTerm*metaSendsEscape .
+Since I'm used to copying and pasting on macOS,
+I added equivalent
+.Dq translations :
+.Bd -literal -offset indent
+XTerm*VT100*translations: #override \en\e
+	Super <Key>C: copy-selection(CLIPBOARD) \en\e
+	Super <Key>V: insert-selection(CLIPBOARD)
+.Ed
+.
+.Pp
+The next thing I needed
+was a clock and battery indicator.
+I actually had my battery die on me
+while I was doing all this,
+which reminded me.
+.Xr xclock 1
+would be perfect,
+but then I'd need something else
+for battery.
+There are a couple basic battery indicators
+for X in ports,
+but they're terribly ugly.
+I wanted something as simple as
+.Xr xclock 1 ,
+but that I could add some other text to.
+Then I realized I could just use
+.Xr xterm 1
+for that.
+To my
+.Pa xsession
+I added:
+.Bd -literal -offset indent
+xterm -name clock -geometry 14x1-0+0 -sl 0 -e clock &
+.Ed
+.Pp
+This places a little terminal
+in the top-right corner of the screen
+with no scrollback lines,
+running a script called
+.Pa clock .
+To have
+.Xr cwm 1
+treat it like a
+.Dq panel
+and show it on every desktop,
+I added this to my
+.Pa cwmrc :
+.Bd -literal -offset indent
+ignore clock
+autogroup 0 clock,XTerm
+.Ed
+.Pp
+The
+.Pa clock
+script simply uses
+.Xr date 1
+and
+.Xr apm 8
+to print the time and battery charge
+every minute:
+.Bd -literal -offset indent
+tput civis
+sleep=$(( 60 - $(date +'%S' | sed 's/^0//') ))
+while :; do
+	if [ $(apm -a) -eq 1 ]; then
+		printf '%3s%%' "$(apm -l)"
+	else
+		test $(apm -b) -eq 2 && tput setaf 1 bold
+		printf '%3.3sm' "$(apm -m)"
+		tput sgr0
+	fi
+	printf ' %s\r' "$(date +'%a %H:%M')"
+	sleep $sleep
+	sleep=60
+done
+.Ed
+.Pp
+The initial setting of
+.Va sleep
+is to align the updates
+with the minute ticking over.
+I made the battery output
+a bit fancier by showing
+percentage while charging,
+minutes left while discharging,
+and highlighting in red
+when the battery is
+.Dq critical .
+.
+.Pp
+Now is a good time to mention adding
+.Ql apmd_flags=-A
+to
+.Pa /etc/rc.conf.local
+to enable
+.Dq automatic performance adjustment ,
+or not running your battery flat
+as fast as possible mode.
+It seems like I can get up to 3 hours
+of battery life depending on the screen brightness,
+but this is quite an old battery by now.
+.
+.Pp
+The other thing I needed
+was something to tone down
+that awful, evil blue light from the screen.
+I asked around and someone told me about
+.Xr sct 1 ,
+originally written by tedu.
+The package also includes a little
+.Xr sctd 1
+script that you can add to your
+.Pa .xsession
+to have it automatically adjust
+the colour temperature throughout the day.
+My eyes are no longer being assaulted.
+.
+.Pp
+While I was doing all this,
+I of course needed to talk about it on IRC,
+and it was very nice to be able to
+install my own IRC client with:
+.Bd -literal -offset indent
+doas pkg_add catgirl
+.Ed
+.Pp
+I don't plan to do
+general Web Browsing on
+.Ox ,
+and there is definitely
+no good choice for browser,
+so I just installed
+.Xr imv 1 ,
+.Xr mpv 1 ,
+.Xr youtube-dl 1
+and
+.Xr w3m 1 .
+I wrote a script
+to open images by piping
+.Xr curl 1
+into
+.Xr imv 1 ,
+videos with
+.Xr mpv 1 ,
+and everything else with
+.Xr w3m 1
+in a new
+.Xr xterm 1 .
+Annoyingly,
+.Xr mpv 1
+seems incapable of exiting
+without segfaulting.
+That's quality.
+.
+.Pp
+One thing I am still missing
+is automatic brightness adjustment
+based on ambient light
+like macOS can do.
+I can read the sensor with
+.Xr sysctl 8
+.Cm hw.sensors.asmc0.illuminance0 ,
+which is measured in lux.
+I tried doing something with it in a script,
+but it seems tricky to map its value
+to brightness adjustments
+and to play nice with manual brightness changes,
+so I'll just keep doing it manually for now.
+.
+.Pp
+Update:
+prx sent mail to let me know about
+.Aq Lk https://github.com/jcs/xdimmer .
+I should've guessed jcs had written something.
+.
+.Pp
+And that's my current
+.Ox
+setup after a week of using it.
+I'm quite enjoying it,
+and still being pleasantly surprised
+by the quality-of-life from
+.Ox
+tools and documentation.
+For a small example,
+I can jump to sections
+or flag definitions in
+.Xr man 1
+using
+.Ic :t .
+Systems without basic usability like that
+should be ashamed.
+.
+.Pp
+I would post a screenshot,
+but this is
+.Li text.causal.agency
+;)
+.
+.Sh SEE ALSO
+.Lk https://gist.github.com/jcs/5573685
+.Pp
+My full configurations are in
+.Aq Lk https://git.causal.agency/src .
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
+.
+.Sh BUGS
+There's a red LED
+inside the headphone jack
+that is always on
+and I have no idea how to turn off.
+If anyone knows
+please send me an email.
diff --git a/www/text.causal.agency/017-unpasswords.7 b/www/text.causal.agency/017-unpasswords.7
new file mode 100644
index 00000000..f9643f2f
--- /dev/null
+++ b/www/text.causal.agency/017-unpasswords.7
@@ -0,0 +1,153 @@
+.Dd February 20, 2021
+.Dt UNPASSWORDS 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Unpasswords
+.Nd password anti-management
+.
+.Sh DESCRIPTION
+Right away I want to say
+that I'm not trying to tell anyone
+how to manage their online authentication.
+This is just how I do it,
+and I haven't seen anyone else write about it.
+.
+.Pp
+I don't use a password manager.
+It's not a type of software
+I want to deal with.
+For the small handful of sites
+that I use regularly
+and that actually matter,
+I use strong passwords
+(stored in my noggin)
+and TOTP.
+For everything else,
+I simply do not know the password,
+and neither does any software.
+.
+.Pp
+I think I started doing this one time
+when I had legitimately forgotten
+the password to some old account.
+I clicked on
+.Dq forgot my password
+and opened the email,
+but I didn't want to
+come up with a new password
+I would just forget again.
+Instead I set a random one
+.Po
+I usually use
+.Ql openssl rand -base64 33
+for this
+.Pc
+and immediately used that to log in
+while it was still in my clipboard.
+Next time I wanted to log in,
+I could use
+.Dq forgot my password
+again.
+.
+.Pp
+Thinking about it,
+I realized that any web authentication
+with an email password reset flow
+is only ever as strong as
+the authentication for your email account.
+So what is the point of having
+all these passwords set on different sites?
+They all answer to your email account,
+and storing them in a password manager
+seems to add another potential point of failure.
+May as well have no other passwords at all,
+or as close as possible.
+.Po
+Shout out to sites like Liberapay
+and asciienema
+which let me not set a password at all.
+.Pc
+.
+.Pp
+So I started doing that for any site
+that I don't regularly log in to.
+Going through the password reset flow
+can be a bit slow,
+but it doesn't need to be done often.
+And I can do it from anywhere
+I have access to my email,
+which I feel is more easily reliable
+than syncing password management databases.
+It's quite stress-free.
+.
+.Pp
+After doing this manually for years,
+this week I finally got around to
+writing some automation for it.
+A while ago I had written
+.Xr imbox 1 ,
+a tool to directly export mail
+in mboxrd format from IMAP,
+along with
+.Xr git-fetch-email 1 ,
+a wrapper which offloads configuration to
+.Xr git-config 1 .
+It can match emails by
+Subject, From, To and Cc.
+This week I added a flag
+to use IMAP IDLE
+to wait for a matching message
+if there isn't one already,
+and a flag to move matching messages
+(for example to Trash)
+after exporting them.
+.
+.Pp
+With those two new flags,
+I started writing some shell scripts
+to automate the password reset flow
+using
+.Xr curl 1
+to submit forms and
+.Xr git-fetch-email 1
+with
+.Xr sed 1
+to pull the reset tokens
+from my inbox.
+At the end of the script,
+the random password it set
+is copied to the clipboard
+and the login page for the site is opened.
+So now logging in is as simple
+as running a command,
+waiting for the login page to open,
+and pasting.
+.
+.Pp
+The script isn't sophisticated,
+but I don't think it needs to be.
+I've written functions
+for a couple different sites already,
+and they all work in mostly the same way.
+Writing a new one is just a matter
+of identifying the form URLs and fields
+along with where the token is in the email.
+I'm not going to turn this automation
+into any kind of generally usable project,
+because I don't want to have to
+maintain functions for tonnes of different services.
+If you're interested in this idea,
+I encourage you to use my script as a template
+and implement the functions for services you use.
+.
+.Sh SEE ALSO
+.Bl -item -compact
+.It
+.Lk https://git.causal.agency/imbox
+.It
+.Lk https://causal.agency/bin/sup.html
+.El
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/018-operating-systems.7 b/www/text.causal.agency/018-operating-systems.7
new file mode 100644
index 00000000..691102e2
--- /dev/null
+++ b/www/text.causal.agency/018-operating-systems.7
@@ -0,0 +1,86 @@
+.Dd February 22, 2021
+.Dt OPERATING-SYSTEMS 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Operating systems
+.Nd criteria
+.
+.Sh DESCRIPTION
+Sometimes in conversation
+I use the term
+.Dq real operating system
+which people,
+perhaps rightfully,
+take as inflammatory.
+But I have actually thought about
+what I mean when I say
+.Dq real operating system
+and come up with
+this list of criteria.
+.
+.Pp
+An operating system should be...
+.Bl -bullet
+.It
+Consistent and cohesive:
+all parts of the system should have similar
+usage, configuration, documentation and so on.
+Parts of the system should naturally work together,
+because they were designed to do so.
+.
+.It
+Documented:
+the system should include its own documentation.
+A user should not have to
+search some external wiki
+to learn how the system works.
+It should be obvious
+where to find documentation
+on a particular topic.
+.
+.It
+Programmable:
+the system should provide
+a way to program the computer.
+A computer which cannot be programmed
+is not a computer at all.
+Usually this takes the form
+of a C compiler
+and the tools that go with it.
+In earlier times,
+it might have been
+a BASIC interpreter.
+.
+.It
+Examinable and modifiable:
+the full source tree
+for the system should be included,
+or easily obtainable
+through official means.
+A user should have no trouble
+finding the corresponding source
+for a part of the system.
+Together with the previous point,
+the source tree should be
+compiled by the included toolchain,
+allowing local modification.
+.El
+.
+.Pp
+Some things that may be parts
+of real operating systems,
+but are not themselves operating systems:
+a kernel,
+a package manager,
+a collection of packages.
+.
+.Pp
+I will leave it as an
+.Dq exercise for the reader
+to guess which operating systems
+meet these criteria
+and which don't.
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/019-mailing-list.7 b/www/text.causal.agency/019-mailing-list.7
new file mode 100644
index 00000000..3c70b160
--- /dev/null
+++ b/www/text.causal.agency/019-mailing-list.7
@@ -0,0 +1,287 @@
+.Dd March  4, 2021
+.Dt MAILING-LIST 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Mailing List
+.Nd a small-scale approach
+.
+.Sh DESCRIPTION
+When I initially published
+some software I expected
+other people to use,
+I just asked that patches
+be mailed directly to me,
+but I figured that
+if more people were interested,
+it would be better
+to have a mailing list.
+Unfortunately
+email software,
+mailing list options in particular,
+are quite daunting.
+I wanted a light-weight option
+that would require me to host
+as little software as possible.
+.
+.Pp
+My regular email is hosted by Fastmail,
+and I poked around its settings
+to see what I could do.
+It turns out Fastmail lets you
+configure address aliases to
+.Dq also send to all contacts in
+a contacts group.
+That's a mailing list!
+I created a group called
+.Dq List
+and an alias called
+.Mt list@causal.agency
+configured to deliver to that group.
+So it's really just an alias
+for my regular address
+that happens to also
+deliver to another group of people.
+.
+.Pp
+It's easier to just configure
+and manage one mailing list,
+so what I do is ask patches and feedback
+to be sent to
+.Mt list+catgirl@causal.agency ,
+for example.
+Fastmail treats any
+.Ar +suffix
+the same as the base address,
+but the full address can be used
+by subscribers to filter mail by topic
+if they wish.
+.
+.Pp
+To subscribe someone to the list,
+I add their contact to the group.
+For a long time I was planning
+to write some software
+to manage these subscriptions.
+It should be possible
+to process subscription requests from IMAP
+and manipulate the contact group with CardDAV.
+When I went to start implementing this,
+however,
+I found CardDAV (and WebDAV in general)
+completely inscrutable.
+It's the kind of protocol
+that is split across like 20
+different RFCs
+and you can't understand anything
+by just reading
+the one you actually care about.
+So I've given up on that
+and will keep manually subscribing people
+on request.
+.
+.Pp
+The only thing missing, then,
+is a way for people to read
+mail sent to the list
+while they aren't subscribed.
+All the existing
+mailing list archive software
+I know of
+expects to have the mail locally,
+but I'd rather keep all my mail in IMAP.
+First,
+in order to make sure
+I keep a complete archive
+of the mailing list in IMAP,
+I added a small amount
+of Sieve code
+to my Fastmail filters configuration:
+.Bd -literal -offset indent
+if address :matches ["To", "Cc"] "list*@causal.agency" {
+	fileinto :copy :flags "\e\eSeen" "INBOX.List";
+}
+.Ed
+.
+.Pp
+Sieve is a small standard language
+specifically for filtering mail.
+This bit of code matches
+anything sent to the list
+and adds a copy of it
+(the original is going into my inbox)
+to the
+.Dq List
+folder
+and marks the copy as read.
+.
+.Pp
+With a pristine IMAP mailbox
+to export from,
+I wrote a new archive generator.
+It's called
+.Xr bubger 1
+kirg (have it in a way).
+My goal was to render directly from IMAP
+and produce only static files as output,
+making it not only easy to serve,
+but also to run in one place
+and copy the files elsewhere.
+That's important to me
+because it has access to my email,
+so I'd rather run it
+on my local network and
+.Xr rsync 1
+its output into The Cloud.
+The static files are in
+HTML, Atom and mboxrd formats.
+.
+.Pp
+The architecture of
+.Xr bubger 1
+is that for each piece of mail,
+identified by its UID in the mailbox,
+HTML and Atom fragments
+are exported along with the mboxrd.
+Those fragments are then stitched together
+using the IMAP SORT and THREAD extensions
+to make full pages and feeds
+for each thread.
+The fragments act as a cache
+for subsequent runs.
+.
+.Pp
+I admit I did some
+pretty questionable things
+to achieve this.
+Namely,
+I wrote a small string templating engine in C.
+I use it to produce the HTML
+and XML for Atom,
+as well as to generate URLs
+and paths.
+I'm really happy with how it works, actually.
+This is also where
+I really started using
+one of my favourite C hacks:
+.Bd -literal -offset indent
+#define Q(...) #__VA_ARGS__
+.Ed
+.
+.Pp
+I quote all my HTML/XML templates
+with this and it's lovely.
+.
+.Pp
+I've been working on
+.Xr bubger 1
+on and off for almost a year now,
+and it's been interesting.
+I learned a lot about how email
+works from having to deal with
+all the ways a message can be.
+Thankfully a lot of that dealing
+is done by the IMAP server.
+.
+.Pp
+As for running it,
+I initially just ran it with
+.Xr cron 8 ,
+and that's still a good way to go.
+To hook it up to
+.Xr rsync 1 ,
+pipe it like so:
+.Bd -literal -offset indent
+bubger -C list [...] | rsync -a --files-from=- list remote:list
+.Ed
+.
+.Pp
+Later,
+I got a little annoyed
+with having to wait
+for the next run
+if I wanted to link
+to some mail I just received.
+I added an option
+to use IMAP IDLE
+to wait for new mail continuously
+and I started running it
+under my process supervisor,
+.Xr catsitd 8 .
+.
+.Pp
+The setup is a little more complex
+to feed the list of updated files to
+.Xr rsync 1 .
+I added the
+.Xr catsit-watch 1
+utility to run a command
+when a file changes,
+and in my
+.Xr catsit.conf 5
+I have the following:
+.Bd -literal -offset indent
+bubger	~/.local/libexec/bubger
+rsync	catsit-watch -i -f ~/list/UIDNEXT ~/.local/libexec/rsync
+.Ed
+.
+.Pp
+The
+.Pa ~/.local/libexec/bubger
+script runs
+.Xr bubger 1 ,
+writing the list of updated paths to
+.Pa ~/list/FILES ,
+truncating it before each update:
+.Bd -literal -offset indent
+exec bubger -it -C ~/list [...] >~/list/FILES
+.Ed
+.
+.Pp
+And the
+.Pa ~/.local/libexec/rsync
+script gets run each time a
+.Xr bubger 1
+update completes
+.Po
+.Pa UIDNEXT
+is always the last file written
+.Pc
+and copies the listed files
+to the remote host:
+.Bd -literal -offset indent
+exec rsync -a --files-from=$HOME/list/FILES ~/list remote:list
+.Ed
+.
+.Pp
+I haven't tagged any
+.Xr bubger 1
+releases yet
+because it hasn't gotten
+a huge amount of testing,
+and I'm not sure anyone but me
+would even want to use it.
+But I'm happy
+with how it's working right now,
+so I might tag 1.0 soon
+just for fun.
+.
+.Sh SEE ALSO
+.Bl -item -compact
+.It
+.Lk https://causal.agency/list/
+.It
+.Lk https://git.causal.agency/bubger/about
+.It
+.Lk https://git.causal.agency/catsit/about
+.El
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
+.
+.Sh BUGS
+Almost every time
+I try to type
+.Dq mailing list
+I instead type
+.Dq mailist list .
diff --git a/www/text.causal.agency/020-c-style.7 b/www/text.causal.agency/020-c-style.7
new file mode 100644
index 00000000..9816dbc3
--- /dev/null
+++ b/www/text.causal.agency/020-c-style.7
@@ -0,0 +1,172 @@
+.Dd March 16, 2021
+.Dt C-STYLE 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm C Style
+.Nd a rough description
+.
+.Sh DESCRIPTION
+This is a rough description
+of the style in which I write C,
+since it's uncommon
+but some people seem to like it.
+I don't have any hard rules,
+it just needs to look right.
+.
+.Ss Superficialities
+I use tabs
+and they're set to 4 characters wide
+in my editor.
+I keep my lines shorter than 80 columns,
+which I enforce by
+not resizing my terminal's width.
+I use block indentation only,
+meaning I write long function calls
+like this:
+.Bd -literal -offset indent
+fprintf(
+    imap.w, "%s UID THREAD %s UTF-8 %s\er\en",
+    Atoms[thread], algo, search
+);
+.Ed
+.Pp
+Anything that can be sorted
+should be sorted,
+with trailing commas where possible.
+This and block indentation
+make for simpler diffs.
+.Pp
+I either write single-line ifs
+or always use braces.
+I put parentheses
+around ternary expressions.
+I use camelCase
+for functions and variables,
+and PascalCase for types and constants.
+When an acronym appears
+in an identifier,
+it's in either all lower case
+or all upper case.
+The despicable SCREAMING_SNAKE_CASE
+is reserved for macros.
+I don't set globals or statics to zero
+since that is already the default.
+I don't compare against zero or NULL
+unnecessarily.
+.
+.Ss \&No side-effects in control flow
+I never write a function call
+with side-effects
+inside the condition of an if statement.
+I find this makes following the
+.Dq happy path
+through functions
+much easier.
+I write things like this:
+.Bd -literal -offset indent
+pidFile = open(pidPath, O_WRONLY | O_CREAT | O_CLOEXEC, 0600);
+if (pidFile < 0) err(EX_CANTCREAT, "%s", pidPath);
+
+error = flock(pidFile, LOCK_EX | LOCK_NB);
+if (error && errno != EWOULDBLOCK) err(EX_IOERR, "%s", pidPath);
+if (error) errx(EX_CANTCREAT, "%s: file is locked", pidPath);
+.Ed
+.Pp
+I do write side-effects
+inside for and while statement heads,
+since that's generally expected.
+For some reason
+I like to write the constant first
+if I'm comparing the result of an assignment
+with a side-effect.
+.Bd -literal -offset indent
+for (ssize_t len; 0 <= (len = getline(&buf, &cap, file)); ++line)
+.Ed
+.
+.Ss Paragraphs
+I leave blank lines
+between logical chunks of
+.Dq things happening .
+This is usually between side-effects
+with their related error handling,
+or between groups of closely related side-effects.
+I try to keep variable declarations
+glued to the top of the bit of code
+they're used in.
+.
+.Ss Leading break
+I've mentioned this previously.
+I write my switch statement breaks
+before each case label.
+Doing this aligns nicely,
+and being in the habit
+means I always avoid
+accidental fallthrough.
+.Bd -literal -offset indent
+switch (opt) {
+    break; case 'a': append = 1;
+    break; case 'd': delay = strtol(optarg, NULL, 10);
+    break; case 'f': watch(kq, optarg);
+    break; case 'i': init = 1;
+    break; default: return EX_USAGE;
+}
+.Ed
+.
+.Ss Function type definitions
+Function types are always typedef'd,
+and it's the function type itself
+that is defined,
+not a function pointer type!
+I put the typedef above any functions
+that are supposed to be of that type
+so it's clear what the pattern is.
+.Bd -literal -offset indent
+typedef void Action(struct Service *service);
+Action *fn = NULL;
+.Ed
+.
+.Ss Constants
+I prefer enums over #defines
+for integer constants,
+and static const strings over #defines
+unless I want to do concatenation.
+.Bd -literal -offset indent
+enum { Cap = 1024 };
+.Ed
+.Pp
+I avoid the preprocessor
+wherever possible,
+with the notable exception of X macros,
+which I've talked about previously.
+Doing things in the actual language
+makes for easier debugging.
+.
+.Ss Organization
+I usually use only one header file
+in each project.
+The dependency is easy to declare
+and the complete rebuild
+when the header changes
+isn't a problem for small projects.
+Unless it's a single-file program,
+I name the file which contains main
+something generic,
+since the name of the project
+isn't relevant to its function.
+I name functions like
+.Ar nounVerb ,
+and all the functions for a 
+.Ar noun
+are defined in
+.Pa noun.c .
+Not really to do with C,
+but I always put a FILES section
+in my README pages
+to briefly describe
+the layout of the code
+for anyone looking to
+read or make changes to it.
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/021-time-machine.7 b/www/text.causal.agency/021-time-machine.7
new file mode 100644
index 00000000..93d35c1e
--- /dev/null
+++ b/www/text.causal.agency/021-time-machine.7
@@ -0,0 +1,144 @@
+.Dd April 25, 2021
+.Dt TIME-MACHINE 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Time Machine
+.Nd an awful one
+.
+.Sh DESCRIPTION
+If, like me,
+you have a Raspberry Pi 3 at home
+that you've just upgraded to
+.Fx 13.0
+which has a hard drive
+from an old laptop
+attached to it by USB adapter
+with ZFS on it
+and you want to
+use that as a Time Machine
+backup destination
+over SMB using
+.Xr samba 8 ,
+despite
+.Xr samba 8
+being awful software
+and using ZFS on a system
+with only 1 GB of RAM
+being a terrible idea,
+this is how to do it.
+.
+.Pp
+In
+.Pa /usr/local/etc/smb4.conf :
+.Bd -literal -offset indent
+[global]
+vfs objects = zfsacl catia fruit streams_xattr
+fruit:metadata = stream
+fruit:model = Macmini
+
+[TimeMachine]
+read only = no
+path = /media/zhdd/backup/TimeMachine
+fruit:time machine = yes
+fruit:time machine max size = 250G
+.Ed
+.
+.Pp
+The important thing here is
+.Sy zfsacl
+in the vfs objects list.
+Most pages will tell you about the others,
+but without
+.Sy zfsacl
+Time Machine will just fail to
+create the backup
+and not provide any useful error.
+I'm not actually sure if the
+.Sy fruit:metadata
+setting is required,
+but a bunch of pages recommend it.
+The
+.Sy fruit:model
+just makes it look nice in Finder.
+The rest creates an SMB share called
+.Dq TimeMachine
+that macOS will be willing to use.
+You can limit the size of the share that
+.Xr samba 8
+reports so that Time Machine
+doesn't fill up the whole drive.
+.
+.Pp
+The other important thing to do
+is to create some swap space.
+When I first tried backing up
+to this share,
+it stopped after a while
+because
+.Xr smbd 8
+got killed
+when there was nowhere to swap pages to.
+A wiki page told me to
+create swap on ZFS like this:
+.Bd -literal -offset indent
+zfs create -V 2G \e
+	-o org.freebsd:swap=on \e
+	-o checksum=off \e
+	-o compression=off \e
+	-o dedup=off \e
+	-o sync=disabled \e
+	-o primarycache=none \e
+	zhdd/swap
+swapon /dev/zvol/zhdd/swap
+.Ed
+.
+.Pp
+To be fair to
+.Xr samba 8 ,
+most of the memory
+is being used by the ZFS ARC
+.Po
+which you can see in
+.Xr top 1
+.Pc ,
+but
+.Xr smbd 8
+still seems to be using
+far more memory than is reasonable.
+It's interesting seeing processes
+with 0 RES in
+.Xr htop 1
+because they're all being swapped out
+while the ARC takes half the available RAM.
+And having to wait for my shell
+to be paged back in when I quit
+.Xr htop 1 .
+.
+.Pp
+Anyway,
+as expected this whole thing
+is terribly slow.
+On my initial backup,
+I'm currently at 26.49 GB
+of 104.22 GB
+with an estimate of 8 hours remaining.
+Normally transfer time estimates
+are wildly inaccurate,
+but I think in this case it's right.
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
+.
+.Sh BUGS
+.Fx
+doesn't seem to want to mount
+the ZFS volumes on the hard-drive-over-USB
+automatically at boot.
+I have to
+.Xr zpool-import 8
+the drive manually each time.
+I don't know if there's a workaround for this,
+but I don't have anything essential
+to the system on the drive,
+and it doesn't need to reboot often.
diff --git a/www/text.causal.agency/022-swans-are-dead.7 b/www/text.causal.agency/022-swans-are-dead.7
new file mode 100644
index 00000000..8664e886
--- /dev/null
+++ b/www/text.causal.agency/022-swans-are-dead.7
@@ -0,0 +1,164 @@
+.Dd May  5, 2021
+.Dt SWANS-ARE-DEAD 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Swans Are Dead
+.Nd album by Swans
+.
+.Sh DESCRIPTION
+Swans Are Dead
+is the best Swans album.
+Among my favourites are
+Soundtracks for the Blind,
+To Be Kind
+and Love of Life,
+but Swans Are Dead
+is the one I come back to
+most consistently.
+I'm always in the mood
+to listen to these tunes.
+.
+.Pp
+It's interesting to me
+that I enjoy it so much,
+I think because I had the expectation
+that live albums
+are not of the same quality
+as studio albums,
+but that's just completely untrue
+in the case of Swans.
+The performances are excellent
+and the recording is
+for the most part perfect.
+The album feels live,
+without any distracting deficiencies
+of live recording
+that would take you out
+of just enjoying the music.
+.
+.Bl -ohang
+.It Dq Feel Happiness
+This track feels kind of special
+since it's the only song on the album
+that was never released
+as part of another project.
+I absolutely love this format of song.
+It's like 10 minutes of build
+before any lyrics happen,
+which you only get after
+the wave of the first part
+of the song collapses.
+It bookends the first disc nicely with
+.Dq Blood Promise,
+I think,
+which is sort of the reverse.
+.
+.It Dq Blood On Yr Hands
+This is such a great start
+to the Jarboe-focused
+section of the black disc.
+A cappella apart from the hum
+of the equipment on stage,
+I love this vocal performance.
+I sing this song,
+terribly,
+in the shower.
+The lack of instrumental
+seems to make it stick in my mind even more.
+.
+.It Dq I Crawled
+This is another great vocal performance
+by Jarboe.
+It's so much more dynamic and intense
+than the version of this song
+released much earlier on Young God
+with Gira's vocals.
+I remember seeing a bad comment
+somewhere online
+from someone who couldn't stand
+any Swans song Jarboe sang on.
+They must have never heard
+this version of
+.Dq I Crawled.
+Incredible.
+.
+.It Dq Blood Promise
+My favourite track on
+Swans Are Dead,
+by far.
+I had actually never heard of
+.Dq The Whiffenpoof Song
+until I looked up
+the recording they use
+to introduce this song
+and indicate it's the last of the show.
+Anyway,
+this track highlights
+what makes Swans live albums
+so interesting.
+This performance of the song
+has evolved so much
+from the studio recording on
+The Great Annihilator.
+They share the same lyrics,
+but the earlier version is only 4:15,
+to the live version's fifteen and a half minutes!
+And it sucks me in the whole time.
+As the song winds down
+you can hear an audience member yell,
+.Dq Don't stop!
+and I agree.
+.
+.It Dq The Sound
+One of my all-time favourite songs.
+It's the one that got me to listen to
+Soundtracks for the Blind,
+and might've gotten me into Swans altogether.
+I don't quite remember
+what order I started listening to things in.
+This version of it is great.
+I don't think I could choose
+between this and the studio recording.
+There are just
+two ways to enjoy it.
+I love how frantic the guitars get
+at the height of this track.
+.
+.It Dq I See Them All Lined Up
+This version of the song
+is way more harsh
+than the version on Soundtracks.
+It loses some contrast
+between the verses
+and the explosions of sound
+punctuating them,
+it just hits hard
+the whole time.
+.
+.It Dq Yum Yab
+An absolute banger.
+The drums sound so good on this
+and they really get me moving.
+The whole thing is delightfully nasty and fun.
+.El
+.
+.Pp
+Everything else on the album
+is good too,
+of course,
+I just don't have as much to say.
+There's almost two and a half hours of music
+on this thing!
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
+.Pp
+I want to try writing
+about different kinds of things here,
+and this is my first attempt
+at doing so.
+There's more music
+I want to write about,
+and maybe some other
+non-computer topics.
diff --git a/www/text.causal.agency/Makefile b/www/text.causal.agency/Makefile
new file mode 100644
index 00000000..192bb272
--- /dev/null
+++ b/www/text.causal.agency/Makefile
@@ -0,0 +1,47 @@
+WEBROOT ?= /usr/local/www/text.causal.agency
+LIBEXEC ?= /usr/local/libexec
+
+CFLAGS += -Wall -Wextra
+
+TXTS += 001-make.txt
+TXTS += 002-writing-mdoc.txt
+TXTS += 003-pleasant-c.txt
+TXTS += 004-uloc.txt
+TXTS += 005-testing-c.txt
+TXTS += 006-some-libs.txt
+TXTS += 007-cgit-setup.txt
+TXTS += 008-how-irc.txt
+TXTS += 009-casual-update.txt
+TXTS += 010-irc-suite.txt
+TXTS += 011-libretls.txt
+TXTS += 012-inability.txt
+TXTS += 013-hot-tips.txt
+TXTS += 014-using-vi.txt
+TXTS += 015-reusing-tags.txt
+TXTS += 016-using-openbsd.txt
+TXTS += 017-unpasswords.txt
+TXTS += 018-operating-systems.txt
+TXTS += 019-mailing-list.txt
+TXTS += 020-c-style.txt
+TXTS += 021-time-machine.txt
+TXTS += 022-swans-are-dead.txt
+
+all: ${TXTS}
+
+.SUFFIXES: .7 .txt
+
+.7.txt:
+	mandoc -T utf8 $< | col -bx > $@
+
+feed.atom: feed.sh ${TXTS}
+	sh feed.sh > feed.atom
+
+clean:
+	rm -f ${TXTS} feed.atom igp
+
+install: ${TXTS} feed.atom
+	install -p -m 644 ${TXTS} feed.atom ${WEBROOT}
+
+install-igp: igp
+	install igp ${LIBEXEC}
+	install -p -m 644 igp.c ${WEBROOT}
diff --git a/www/text.causal.agency/feed.sh b/www/text.causal.agency/feed.sh
new file mode 100644
index 00000000..f45bd326
--- /dev/null
+++ b/www/text.causal.agency/feed.sh
@@ -0,0 +1,55 @@
+#!/bin/sh
+set -eu
+
+readonly Root='https://text.causal.agency'
+
+updated=$(date -u '+%FT%TZ')
+cat <<-EOF
+	<?xml version="1.0" encoding="utf-8"?>
+	<feed xmlns="http://www.w3.org/2005/Atom">
+	<title>Causal Agency</title>
+	<author><name>June</name><email>june@causal.agency</email></author>
+	<link href="${Root}"/>
+	<link rel="self" href="${Root}/feed.atom"/>
+	<id>${Root}/</id>
+	<updated>${updated}</updated>
+EOF
+
+encode() {
+	sed '
+		s/&/\&amp;/g
+		s/</\&lt;/g
+		s/"/\&quot;/g
+	' "$@"
+}
+
+for txt in *.txt; do
+	entry="${txt%.txt}.7"
+	date=$(grep '^[.]Dd' "$entry" | cut -c 5-)
+	title=$(grep '^[.]Nm' "$entry" | cut -c 5- | encode)
+	summary=$(grep '^[.]Nd' "$entry" | cut -c 5- | encode)
+	published=$(date -ju -f '%B %d, %Y %T' "${date} 00:00:00" '+%FT%TZ')
+	mtime=$(stat -f '%m' "$entry")
+	updated=$(date -ju -f '%s' "$mtime" '+%FT%TZ')
+	cat <<-EOF
+		<entry>
+		<title>${title}</title>
+		<summary>${summary}</summary>
+		<link href="${Root}/${txt}"/>
+		<id>${Root}/${txt}</id>
+		<published>${published}</published>
+		<updated>${updated}</updated>
+		<content type="xhtml">
+		<div xmlns="http://www.w3.org/1999/xhtml">
+	EOF
+	printf '<pre>'
+	encode "$txt"
+	cat <<-EOF
+		</pre>
+		</div>
+		</content>
+		</entry>
+	EOF
+done
+
+echo '</feed>'
diff --git a/www/text.causal.agency/igp.1 b/www/text.causal.agency/igp.1
new file mode 100644
index 00000000..ccfaeaa6
--- /dev/null
+++ b/www/text.causal.agency/igp.1
@@ -0,0 +1,49 @@
+.Dd January 14, 2021
+.Dt IGP 1
+.Os
+.
+.Sh NAME
+.Nm igp
+.Nd insane gopher posse
+.
+.Sh SYNOPSIS
+.Nm
+.Op Fl h Ar host
+.Op Fl p Ar port
+.Ar directory
+.
+.Sh DESCRIPTION
+The
+.Nm
+utility is a simple
+Internet Gopher server
+which can be started by
+.Xr inetd 8 .
+It serves directory listings
+and files under
+.Ar directory .
+.
+.Pp
+The arguments are as follows:
+.Bl -tag -width Ds
+.It Fl h Ar host
+Set the host used in directory listings.
+The default is the local host name.
+.It Fl p Ar port
+Set the port used in directory listeings.
+The default is port 70.
+.El
+.
+.Sh STANDARDS
+.Rs
+.%A B. Alberti
+.%A F. Anklesaria
+.%A D. Johnson
+.%A P. Lindner
+.%A M. McCahill
+.%A D. Torrey
+.%T The Internet Gopher Protocol
+.%I IETF
+.%R RFC 1436
+.%D March 1993
+.Re
diff --git a/www/text.causal.agency/igp.c b/www/text.causal.agency/igp.c
new file mode 100644
index 00000000..d7db2b28
--- /dev/null
+++ b/www/text.causal.agency/igp.c
@@ -0,0 +1,143 @@
+/* Copyright (C) 2021  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <sysexits.h>
+#include <time.h>
+#include <unistd.h>
+
+#ifdef __FreeBSD__
+#include <sys/capsicum.h>
+#endif
+
+static int compar(const void *_a, const void *_b) {
+	const struct dirent *a = _a;
+	const struct dirent *b = _b;
+	if (a->d_type != b->d_type) {
+		return (a->d_type > b->d_type) - (a->d_type < b->d_type);
+	}
+	return strcmp(a->d_name, b->d_name);
+}
+
+int main(int argc, char *argv[]) {
+	int error;
+	const char *host = NULL;
+	const char *port = "70";
+	for (int opt; 0 < (opt = getopt(argc, argv, "h:p:"));) {
+		switch (opt) {
+			break; case 'h': host = optarg;
+			break; case 'p': port = optarg;
+			break; default:  return EX_USAGE;
+		}
+	}
+	if (optind == argc) return EX_USAGE;
+	if (!host) {
+		static char buf[256];
+		error = gethostname(buf, sizeof(buf));
+		if (error) abort();
+		host = buf;
+	}
+
+	const char *path = argv[optind];
+	int root = open(path, O_RDONLY | O_DIRECTORY);
+	if (root < 0) err(EX_NOINPUT, "/");
+
+#ifdef __FreeBSD__
+	cap_rights_t cap;
+	error = cap_enter()
+		|| cap_rights_limit(STDIN_FILENO, cap_rights_init(&cap, CAP_READ))
+		|| cap_rights_limit(STDOUT_FILENO, cap_rights_init(&cap, CAP_WRITE))
+		|| cap_rights_limit(STDERR_FILENO, &cap)
+		|| cap_rights_limit(
+			root, cap_rights_init(&cap, CAP_PREAD, CAP_FSTATAT, CAP_FSTATFS)
+		);
+	if (error) abort();
+#else
+#warning "This is completely insecure without capsicum(4)!"
+#endif
+
+	char buf[1024];
+	if (!fgets(buf, sizeof(buf), stdin)) return EX_PROTOCOL;
+	char *ptr = buf;
+	char *sel = strsep(&ptr, "\t\r\n");
+	if (sel[0] == '/') sel++;
+
+	int fd = (sel[0] ? openat(root, sel, O_RDONLY) : root);
+	if (fd < 0) err(EX_NOINPUT, "%s", sel);
+
+	struct stat stat;
+	error = fstat(fd, &stat);
+	if (error) err(EX_IOERR, "%s", sel);
+	if (!(stat.st_mode & (S_IFREG | S_IFDIR))) {
+		errx(EX_NOINPUT, "%s: Not a file or directory", sel);
+	}
+
+	if (stat.st_mode & S_IFREG) {
+#ifdef __FreeBSD__
+		error = sendfile(fd, STDOUT_FILENO, 0, 0, NULL, NULL, 0);
+		if (!error) return EX_OK;
+#endif
+		char buf[4096];
+		for (ssize_t len; 0 < (len = read(fd, buf, sizeof(buf)));) {
+			fwrite(buf, len, 1, stdout);
+		}
+		return EX_OK;
+	}
+
+	DIR *dir = fdopendir(fd);
+	if (!dir) err(EX_IOERR, "%s", sel);
+
+	size_t len = 0;
+	size_t width = 0;
+	static struct dirent ents[4096];
+	for (struct dirent *ent; len < 4096 && (ent = readdir(dir));) {
+		if (ent->d_name[0] == '.') continue;
+		if (ent->d_type != DT_REG && ent->d_type != DT_DIR) continue;
+		if (ent->d_namlen > width) width = ent->d_namlen;
+		ents[len++] = *ent;
+	}
+
+	qsort(ents, len, sizeof(ents[0]), compar);
+	for (size_t i = 0; i < len; ++i) {
+		char mtime[26] = "";
+		if (ents[i].d_type == DT_REG) {
+			error = fstatat(fd, ents[i].d_name, &stat, 0);
+			if (error) err(EX_IOERR, "%s/%s", sel, ents[i].d_name);
+			ctime_r(&stat.st_mtime, mtime);
+			mtime[24] = '\0';
+		}
+		printf(
+			"%c%-*s  %s\t%s%s%s\t%s\t%s\r\n",
+			(ents[i].d_type == DT_DIR ? '1' : '0'),
+			(int)width, ents[i].d_name, mtime,
+			sel, (sel[0] ? "/" : ""), ents[i].d_name, host, port
+		);
+	}
+
+	printf("i-- \t\t%s\t%s\r\n", host, port);
+	printf("0Served by IGP (AGPLv3)\tigp.c\ttext.causal.agency\t70\r\n");
+	printf(".\r\n");
+}