summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--TOUR.722
-rw-r--r--bin/beef.c2
-rw-r--r--bin/bit.y2
-rw-r--r--bin/c11.l2
-rw-r--r--bin/dehtml.l2
-rw-r--r--bin/downgrade.c2
-rw-r--r--bin/dtch.c2
-rw-r--r--bin/ever.c2
-rw-r--r--bin/freecell.c2
-rw-r--r--bin/git-comment.pl2
-rw-r--r--bin/glitch.c687
-rw-r--r--bin/hilex.c2
-rw-r--r--bin/hilex.h2
-rw-r--r--bin/make.l2
-rw-r--r--bin/man1/up.118
-rw-r--r--bin/mdoc.l2
-rw-r--r--bin/modem.c2
-rw-r--r--bin/mtags.c2
-rw-r--r--bin/nudge.c2
-rw-r--r--bin/order.y2
-rw-r--r--bin/pbd.c2
-rw-r--r--bin/png.h2
-rw-r--r--bin/pngo.c2
-rw-r--r--bin/psf2png.c2
-rw-r--r--bin/ptee.c2
-rw-r--r--bin/quick.c2
-rw-r--r--bin/relay.c2
-rw-r--r--bin/scheme.c2
-rw-r--r--bin/sh.l14
-rw-r--r--bin/shotty.l2
-rw-r--r--bin/title.c2
-rw-r--r--bin/up.sh30
-rw-r--r--bin/xx.c2
-rw-r--r--home/.config/X/resources2
-rw-r--r--home/.config/git/config3
-rw-r--r--txt/books.txt3
-rw-r--r--www/temp.causal.agency/up.c2
-rw-r--r--www/text.causal.agency/037-care.722
38 files changed, 472 insertions, 387 deletions
diff --git a/TOUR.7 b/TOUR.7
deleted file mode 100644
index 8466f52..0000000
--- a/TOUR.7
+++ /dev/null
@@ -1,22 +0,0 @@
-.Dd March 16, 2022
-.Dt TOUR 7
-.Os "Causal Agency"
-.
-.Sh NAME
-.Nm tour
-.Nd things that are here
-.
-.Sh FILES
-.Bl -tag -width Ds
-.It Pa home/
-.Dq dotfiles
-.It Pa bin/
-utilities of varying utility
-.It Pa doc/rfc/
-scripts for reading IETF RFCs
-.It Pa doc/zlib/
-zlib documentation ported to
-.Xr mdoc 7
-.It Pa www/
-sources for causal.agency sites
-.El
diff --git a/bin/beef.c b/bin/beef.c
index b2579b7..556f308 100644
--- a/bin/beef.c
+++ b/bin/beef.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2019  June 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
diff --git a/bin/bit.y b/bin/bit.y
index ab31049..1119bce 100644
--- a/bin/bit.y
+++ b/bin/bit.y
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2019  June 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
diff --git a/bin/c11.l b/bin/c11.l
index a4b8c25..b1f0b96 100644
--- a/bin/c11.l
+++ b/bin/c11.l
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2020  June 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
diff --git a/bin/dehtml.l b/bin/dehtml.l
index 3f2de59..799f092 100644
--- a/bin/dehtml.l
+++ b/bin/dehtml.l
@@ -1,4 +1,4 @@
-/* Copyright (C) 2021  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2021  June 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
diff --git a/bin/downgrade.c b/bin/downgrade.c
index d4c5b59..3101971 100644
--- a/bin/downgrade.c
+++ b/bin/downgrade.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2021  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2021  June 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
diff --git a/bin/dtch.c b/bin/dtch.c
index 2aea53a..026493d 100644
--- a/bin/dtch.c
+++ b/bin/dtch.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2017-2019  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2017-2019  June 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
diff --git a/bin/ever.c b/bin/ever.c
index f983912..f8ff943 100644
--- a/bin/ever.c
+++ b/bin/ever.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2017  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2017  June 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
diff --git a/bin/freecell.c b/bin/freecell.c
index 11bed1c..fbc0fe2 100644
--- a/bin/freecell.c
+++ b/bin/freecell.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019, 2021  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2019, 2021  June 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
diff --git a/bin/git-comment.pl b/bin/git-comment.pl
index 5100941..5352702 100644
--- a/bin/git-comment.pl
+++ b/bin/git-comment.pl
@@ -1,5 +1,5 @@
 #!/usr/bin/env perl
-# Copyright (C) 2021  C. McEnroe <june@causal.agency>
+# Copyright (C) 2021  June 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
diff --git a/bin/glitch.c b/bin/glitch.c
index 9747f35..d0c926f 100644
--- a/bin/glitch.c
+++ b/bin/glitch.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2018, 2021  June 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
@@ -14,8 +14,9 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include <arpa/inet.h>
 #include <err.h>
+#include <inttypes.h>
+#include <limits.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include <stdio.h>
@@ -25,286 +26,328 @@
 #include <unistd.h>
 #include <zlib.h>
 
-#define PACKED __attribute__((packed))
-
-#define CRC_INIT (crc32(0, Z_NULL, 0))
+#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
 
 static const char *path;
 static FILE *file;
 static uint32_t crc;
 
-static void readExpect(void *ptr, size_t size, const char *expect) {
-	fread(ptr, size, 1, file);
-	if (ferror(file)) err(EX_IOERR, "%s", path);
-	if (feof(file)) errx(EX_DATAERR, "%s: missing %s", path, expect);
-	crc = crc32(crc, ptr, size);
+static void pngRead(void *ptr, size_t len, const char *desc) {
+	size_t n = fread(ptr, len, 1, file);
+	if (!n && ferror(file)) err(EX_IOERR, "%s", path);
+	if (!n) errx(EX_DATAERR, "%s: missing %s", path, desc);
+	crc = crc32(crc, ptr, len);
 }
 
-static void writeExpect(const void *ptr, size_t size) {
-	fwrite(ptr, size, 1, file);
-	if (ferror(file)) err(EX_IOERR, "%s", path);
-	crc = crc32(crc, ptr, size);
+static void pngWrite(const void *ptr, size_t len) {
+	size_t n = fwrite(ptr, len, 1, file);
+	if (!n) err(EX_IOERR, "%s", path);
+	crc = crc32(crc, ptr, len);
 }
 
-static const uint8_t Signature[8] = "\x89PNG\r\n\x1A\n";
+static const uint8_t Sig[8] = "\x89PNG\r\n\x1A\n";
 
-static void readSignature(void) {
-	uint8_t signature[8];
-	readExpect(signature, 8, "signature");
-	if (0 != memcmp(signature, Signature, 8)) {
+static void sigRead(void) {
+	uint8_t sig[sizeof(Sig)];
+	pngRead(sig, sizeof(sig), "signature");
+	if (memcmp(sig, Sig, sizeof(sig))) {
 		errx(EX_DATAERR, "%s: invalid signature", path);
 	}
 }
 
-static void writeSignature(void) {
-	writeExpect(Signature, sizeof(Signature));
+static void sigWrite(void) {
+	pngWrite(Sig, sizeof(Sig));
 }
 
-struct PACKED Chunk {
-	uint32_t size;
-	char type[4];
-};
+static uint32_t u32Read(const char *desc) {
+	uint8_t b[4];
+	pngRead(b, sizeof(b), desc);
+	return (uint32_t)b[0] << 24 | (uint32_t)b[1] << 16
+		| (uint32_t)b[2] << 8 | (uint32_t)b[3];
+}
 
-static const char *typeStr(struct Chunk chunk) {
-	static char buf[5];
-	memcpy(buf, chunk.type, 4);
-	return buf;
+static void u32Write(uint32_t x) {
+	uint8_t b[4] = { x >> 24 & 0xFF, x >> 16 & 0xFF, x >> 8 & 0xFF, x & 0xFF };
+	pngWrite(b, sizeof(b));
 }
 
-static struct Chunk readChunk(void) {
+struct Chunk {
+	uint32_t len;
+	char type[5];
+};
+
+static struct Chunk chunkRead(void) {
 	struct Chunk chunk;
-	readExpect(&chunk, sizeof(chunk), "chunk");
-	chunk.size = ntohl(chunk.size);
-	crc = crc32(CRC_INIT, (Byte *)chunk.type, sizeof(chunk.type));
+	chunk.len = u32Read("chunk length");
+	crc = crc32(0, Z_NULL, 0);
+	pngRead(chunk.type, 4, "chunk type");
+	chunk.type[4] = 0;
 	return chunk;
 }
 
-static void writeChunk(struct Chunk chunk) {
-	chunk.size = htonl(chunk.size);
-	writeExpect(&chunk, sizeof(chunk));
-	crc = crc32(CRC_INIT, (Byte *)chunk.type, sizeof(chunk.type));
+static void chunkWrite(struct Chunk chunk) {
+	u32Write(chunk.len);
+	crc = crc32(0, Z_NULL, 0);
+	pngWrite(chunk.type, 4);
 }
 
-static void readCrc(void) {
-	uint32_t expected = crc;
-	uint32_t found;
-	readExpect(&found, sizeof(found), "CRC32");
-	found = ntohl(found);
-	if (found != expected) {
-		errx(
-			EX_DATAERR, "%s: expected CRC32 %08X, found %08X",
-			path, expected, found
-		);
-	}
+static void crcRead(void) {
+	uint32_t expect = crc;
+	uint32_t actual = u32Read("CRC32");
+	if (actual == expect) return;
+	errx(
+		EX_DATAERR, "%s: expected CRC32 %08X, found %08X",
+		path, expect, actual
+	);
 }
 
-static void writeCrc(void) {
-	uint32_t net = htonl(crc);
-	writeExpect(&net, sizeof(net));
+static void crcWrite(void) {
+	u32Write(crc);
 }
 
-static void skipChunk(struct Chunk chunk) {
-	uint8_t discard[chunk.size];
-	readExpect(discard, sizeof(discard), "chunk data");
-	readCrc();
+static void chunkSkip(struct Chunk chunk) {
+	if (!(chunk.type[0] & 0x20)) {
+		errx(EX_CONFIG, "%s: unsupported critical chunk %s", path, chunk.type);
+	}
+	uint8_t buf[4096];
+	while (chunk.len > sizeof(buf)) {
+		pngRead(buf, sizeof(buf), "chunk data");
+		chunk.len -= sizeof(buf);
+	}
+	if (chunk.len) pngRead(buf, chunk.len, "chunk data");
+	crcRead();
 }
 
-static struct PACKED {
+enum Color {
+	Grayscale = 0,
+	Truecolor = 2,
+	Indexed = 3,
+	GrayscaleAlpha = 4,
+	TruecolorAlpha = 6,
+};
+enum Compression {
+	Deflate,
+};
+enum FilterMethod {
+	Adaptive,
+};
+enum Interlace {
+	Progressive,
+	Adam7,
+};
+
+enum { HeaderLen = 13 };
+static struct {
 	uint32_t width;
 	uint32_t height;
 	uint8_t depth;
-	enum PACKED {
-		Grayscale      = 0,
-		Truecolor      = 2,
-		Indexed        = 3,
-		GrayscaleAlpha = 4,
-		TruecolorAlpha = 6,
-	} color;
+	uint8_t color;
 	uint8_t compression;
 	uint8_t filter;
 	uint8_t interlace;
 } header;
-_Static_assert(13 == sizeof(header), "header size");
 
-static size_t pixelBits(void) {
+static size_t pixelLen;
+static size_t lineLen;
+static size_t dataLen;
+
+static void recalc(void) {
+	size_t pixelBits = header.depth;
 	switch (header.color) {
-		case Grayscale:      return 1 * header.depth;
-		case Truecolor:      return 3 * header.depth;
-		case Indexed:        return 1 * header.depth;
-		case GrayscaleAlpha: return 2 * header.depth;
-		case TruecolorAlpha: return 4 * header.depth;
-		default: abort();
+		break; case GrayscaleAlpha: pixelBits *= 2;
+		break; case Truecolor: pixelBits *= 3;
+		break; case TruecolorAlpha: pixelBits *= 4;
 	}
+	pixelLen = (pixelBits + 7) / 8;
+	lineLen = (header.width * pixelBits + 7) / 8;
+	dataLen = (1 + lineLen) * header.height;
 }
 
-static size_t pixelSize(void) {
-	return (pixelBits() + 7) / 8;
+static void headerRead(struct Chunk chunk) {
+	if (chunk.len != HeaderLen) {
+		errx(
+			EX_DATAERR, "%s: expected %s length %" PRIu32 ", found %" PRIu32,
+			path, chunk.type, (uint32_t)HeaderLen, chunk.len
+		);
+	}
+	header.width = u32Read("header width");
+	header.height = u32Read("header height");
+	pngRead(&header.depth, 1, "header depth");
+	pngRead(&header.color, 1, "header color");
+	pngRead(&header.compression, 1, "header compression");
+	pngRead(&header.filter, 1, "header filter");
+	pngRead(&header.interlace, 1, "header interlace");
+	crcRead();
+	recalc();
+}
+
+static void headerWrite(void) {
+	struct Chunk ihdr = { HeaderLen, "IHDR" };
+	chunkWrite(ihdr);
+	u32Write(header.width);
+	u32Write(header.height);
+	pngWrite(&header.depth, 1);
+	pngWrite(&header.color, 1);
+	pngWrite(&header.compression, 1);
+	pngWrite(&header.filter, 1);
+	pngWrite(&header.interlace, 1);
+	crcWrite();
 }
 
-static size_t lineSize(void) {
-	return (header.width * pixelBits() + 7) / 8;
-}
+static struct {
+	uint32_t len;
+	uint8_t rgb[256][3];
+} pal;
 
-static size_t dataSize(void) {
-	return (1 + lineSize()) * header.height;
+static struct {
+	uint32_t len;
+	uint8_t a[256];
+} trans;
+
+static void palClear(void) {
+	pal.len = 0;
+	trans.len = 0;
 }
 
-static void readHeader(void) {
-	struct Chunk ihdr = readChunk();
-	if (0 != memcmp(ihdr.type, "IHDR", 4)) {
-		errx(EX_DATAERR, "%s: expected IHDR, found %s", path, typeStr(ihdr));
+static void palRead(struct Chunk chunk) {
+	if (chunk.len % 3) {
+		errx(
+			EX_DATAERR, "%s: %s length %" PRIu32 " not divisible by 3",
+			path, chunk.type, chunk.len
+		);
 	}
-	if (ihdr.size != sizeof(header)) {
+	pal.len = chunk.len / 3;
+	if (pal.len > 256) {
 		errx(
-			EX_DATAERR, "%s: expected IHDR size %zu, found %u",
-			path, sizeof(header), ihdr.size
+			EX_DATAERR, "%s: %s length %" PRIu32 " > 256",
+			path, chunk.type, pal.len
 		);
 	}
-	readExpect(&header, sizeof(header), "header");
-	readCrc();
-	header.width = ntohl(header.width);
-	header.height = ntohl(header.height);
-	if (!header.width) errx(EX_DATAERR, "%s: invalid width 0", path);
-	if (!header.height) errx(EX_DATAERR, "%s: invalid height 0", path);
+	pngRead(pal.rgb, chunk.len, "palette data");
+	crcRead();
 }
 
-static void writeHeader(void) {
-	struct Chunk ihdr = { .size = sizeof(header), .type = "IHDR" };
-	writeChunk(ihdr);
-	header.width = htonl(header.width);
-	header.height = htonl(header.height);
-	writeExpect(&header, sizeof(header));
-	writeCrc();
-	header.width = ntohl(header.width);
-	header.height = ntohl(header.height);
+static void palWrite(void) {
+	struct Chunk plte = { 3 * pal.len, "PLTE" };
+	chunkWrite(plte);
+	pngWrite(pal.rgb, plte.len);
+	crcWrite();
 }
 
-static struct {
-	uint32_t len;
-	uint8_t entries[256][3];
-} palette;
-
-static void readPalette(void) {
-	struct Chunk chunk;
-	for (;;) {
-		chunk = readChunk();
-		if (0 == memcmp(chunk.type, "PLTE", 4)) break;
-		skipChunk(chunk);
+static void transRead(struct Chunk chunk) {
+	trans.len = chunk.len;
+	if (trans.len > 256) {
+		errx(
+			EX_DATAERR, "%s: %s length %" PRIu32 " > 256",
+			path, chunk.type, trans.len
+		);
 	}
-	palette.len = chunk.size / 3;
-	readExpect(palette.entries, chunk.size, "palette data");
-	readCrc();
+	pngRead(trans.a, chunk.len, "transparency data");
+	crcRead();
 }
 
-static void writePalette(void) {
-	struct Chunk plte = { .size = 3 * palette.len, .type = "PLTE" };
-	writeChunk(plte);
-	writeExpect(palette.entries, plte.size);
-	writeCrc();
+static void transWrite(void) {
+	struct Chunk trns = { trans.len, "tRNS" };
+	chunkWrite(trns);
+	pngWrite(trans.a, trns.len);
+	crcWrite();
 }
 
 static uint8_t *data;
 
-static void readData(void) {
-	data = malloc(dataSize());
-	if (!data) err(EX_OSERR, "malloc(%zu)", dataSize());
+static void dataAlloc(void) {
+	data = malloc(dataLen);
+	if (!data) err(EX_OSERR, "malloc");
+}
 
-	struct z_stream_s stream = { .next_out = data, .avail_out = dataSize() };
+static void dataRead(struct Chunk chunk) {
+	z_stream stream = { .next_out = data, .avail_out = dataLen };
 	int error = inflateInit(&stream);
-	if (error != Z_OK) errx(EX_SOFTWARE, "%s: inflateInit: %s", path, stream.msg);
+	if (error != Z_OK) errx(EX_SOFTWARE, "inflateInit: %s", stream.msg);
 
 	for (;;) {
-		struct Chunk chunk = readChunk();
-		if (0 == memcmp(chunk.type, "IDAT", 4)) {
-			uint8_t *idat = malloc(chunk.size);
-			if (!idat) err(EX_OSERR, "malloc");
-
-			readExpect(idat, chunk.size, "image data");
-			readCrc();
+		if (strcmp(chunk.type, "IDAT")) {
+			errx(EX_DATAERR, "%s: missing IDAT chunk", path);
+		}
 
-			stream.next_in = idat;
-			stream.avail_in = chunk.size;
-			int error = inflate(&stream, Z_SYNC_FLUSH);
-			free(idat);
+		uint8_t *idat = malloc(chunk.len);
+		if (!idat) err(EX_OSERR, "malloc");
 
-			if (error == Z_STREAM_END) break;
-			if (error != Z_OK) errx(EX_DATAERR, "%s: inflate: %s", path, stream.msg);
+		pngRead(idat, chunk.len, "image data");
+		crcRead();
+		
+		stream.next_in = idat;
+		stream.avail_in = chunk.len;
+		error = inflate(&stream, Z_SYNC_FLUSH);
+		free(idat);
 
-		} else if (0 == memcmp(chunk.type, "IEND", 4)) {
-			errx(EX_DATAERR, "%s: missing IDAT chunk", path);
-		} else {
-			skipChunk(chunk);
+		if (error == Z_STREAM_END) break;
+		if (error != Z_OK) {
+			errx(EX_DATAERR, "%s: inflate: %s", path, stream.msg);
 		}
-	}
 
+		chunk = chunkRead();
+	}
 	inflateEnd(&stream);
-	if ((size_t)stream.total_out != dataSize()) {
+	if ((size_t)stream.total_out != dataLen) {
 		errx(
-			EX_DATAERR, "%s: expected data size %zu, found %zu",
-			path, dataSize(), (size_t)stream.total_out
+			EX_DATAERR, "%s: expected data length %zu, found %zu",
+			path, dataLen, (size_t)stream.total_out
 		);
 	}
 }
 
-static void writeData(void) {
-	uLong size = compressBound(dataSize());
-	uint8_t *deflate = malloc(size);
-	if (!deflate) err(EX_OSERR, "malloc");
+static void dataWrite(void) {
+	z_stream stream = {
+		.next_in = data,
+		.avail_in = dataLen,
+	};
+	int error = deflateInit2(
+		&stream, Z_BEST_COMPRESSION, Z_DEFLATED, 15, 8, Z_FILTERED
+	);
+	if (error != Z_OK) errx(EX_SOFTWARE, "deflateInit2: %s", stream.msg);
 
-	int error = compress2(deflate, &size, data, dataSize(), Z_BEST_SPEED);
-	if (error != Z_OK) errx(EX_SOFTWARE, "%s: compress2: %d", path, error);
+	uLong bound = deflateBound(&stream, dataLen);
+	uint8_t *buf = malloc(bound);
+	if (!buf) err(EX_OSERR, "malloc");
 
-	struct Chunk idat = { .size = size, .type = "IDAT" };
-	writeChunk(idat);
-	writeExpect(deflate, size);
-	writeCrc();
+	stream.next_out = buf;
+	stream.avail_out = bound;
+	deflate(&stream, Z_FINISH);
+	deflateEnd(&stream);
 
-	free(deflate);
-}
+	struct Chunk idat = { stream.total_out, "IDAT" };
+	chunkWrite(idat);
+	pngWrite(buf, stream.total_out);
+	crcWrite();
+	free(buf);
 
-static void writeEnd(void) {
-	struct Chunk iend = { .size = 0, .type = "IEND" };
-	writeChunk(iend);
-	writeCrc();
+	struct Chunk iend = { 0, "IEND" };
+	chunkWrite(iend);
+	crcWrite();
 }
 
-enum PACKED Filter {
+enum Filter {
 	None,
 	Sub,
 	Up,
 	Average,
 	Paeth,
-	FilterCount,
+	FilterCap,
 };
 
-static struct {
-	bool brokenPaeth;
-	bool filt;
-	bool recon;
-	uint8_t declareFilter;
-	uint8_t applyFilter;
-	enum Filter declareFilters[255];
-	enum Filter applyFilters[255];
-	bool invert;
-	bool mirror;
-	bool zeroX;
-	bool zeroY;
-} options;
-
 struct Bytes {
-	uint8_t x;
-	uint8_t a;
-	uint8_t b;
-	uint8_t c;
+	uint8_t x, a, b, c;
 };
 
+static bool brokenPaeth;
 static uint8_t paethPredictor(struct Bytes f) {
 	int32_t p = (int32_t)f.a + (int32_t)f.b - (int32_t)f.c;
-	int32_t pa = abs(p - (int32_t)f.a);
-	int32_t pb = abs(p - (int32_t)f.b);
-	int32_t pc = abs(p - (int32_t)f.c);
+	int32_t pa = labs(p - (int32_t)f.a);
+	int32_t pb = labs(p - (int32_t)f.b);
+	int32_t pc = labs(p - (int32_t)f.c);
 	if (pa <= pb && pa <= pc) return f.a;
-	if (options.brokenPaeth) {
+	if (brokenPaeth) {
 		if (pb < pc) return f.b;
 	} else {
 		if (pb <= pc) return f.b;
@@ -319,7 +362,7 @@ static uint8_t recon(enum Filter type, struct Bytes f) {
 		case Up:      return f.x + f.b;
 		case Average: return f.x + ((uint32_t)f.a + (uint32_t)f.b) / 2;
 		case Paeth:   return f.x + paethPredictor(f);
-		default:      abort();
+		default: abort();
 	}
 }
 
@@ -330,59 +373,59 @@ static uint8_t filt(enum Filter type, struct Bytes f) {
 		case Up:      return f.x - f.b;
 		case Average: return f.x - ((uint32_t)f.a + (uint32_t)f.b) / 2;
 		case Paeth:   return f.x - paethPredictor(f);
-		default:      abort();
+		default: abort();
 	}
 }
 
-static struct Line {
-	enum Filter type;
-	uint8_t data[];
-} **lines;
-
-static void scanlines(void) {
-	lines = calloc(header.height, sizeof(*lines));
-	if (!lines) err(EX_OSERR, "calloc(%u, %zu)", header.height, sizeof(*lines));
-
-	size_t stride = 1 + lineSize();
-	for (uint32_t y = 0; y < header.height; ++y) {
-		lines[y] = (struct Line *)&data[y * stride];
-		if (lines[y]->type >= FilterCount) {
-			errx(EX_DATAERR, "%s: invalid filter type %hhu", path, lines[y]->type);
-		}
-	}
+static uint8_t *lineType(uint32_t y) {
+	return &data[y * (1 + lineLen)];
+}
+static uint8_t *lineData(uint32_t y) {
+	return 1 + lineType(y);
 }
 
 static struct Bytes origBytes(uint32_t y, size_t i) {
-	bool a = (i >= pixelSize()), b = (y > 0), c = (a && b);
+	bool a = (i >= pixelLen), b = (y > 0), c = (a && b);
 	return (struct Bytes) {
-		.x = lines[y]->data[i],
-		.a = a ? lines[y]->data[i - pixelSize()] : 0,
-		.b = b ? lines[y - 1]->data[i] : 0,
-		.c = c ? lines[y - 1]->data[i - pixelSize()] : 0,
+		.x = lineData(y)[i],
+		.a = (a ? lineData(y)[i-pixelLen] : 0),
+		.b = (b ? lineData(y-1)[i] : 0),
+		.c = (c ? lineData(y-1)[i-pixelLen] : 0),
 	};
 }
 
-static void reconData(void) {
+static bool reconFilter;
+static void dataRecon(void) {
 	for (uint32_t y = 0; y < header.height; ++y) {
-		for (size_t i = 0; i < lineSize(); ++i) {
-			if (options.filt) {
-				lines[y]->data[i] = filt(lines[y]->type, origBytes(y, i));
+		for (size_t i = 0; i < lineLen; ++i) {
+			if (reconFilter) {
+				lineData(y)[i] = filt(*lineType(y), origBytes(y, i));
 			} else {
-				lines[y]->data[i] = recon(lines[y]->type, origBytes(y, i));
+				lineData(y)[i] = recon(*lineType(y), origBytes(y, i));
 			}
 		}
-		lines[y]->type = None;
+		*lineType(y) = None;
 	}
 }
 
-static void filterData(void) {
-	for (uint32_t y = header.height - 1; y < header.height; --y) {
-		uint8_t filter[FilterCount][lineSize()];
-		uint32_t heuristic[FilterCount] = {0};
+static bool filterRecon;
+static size_t applyFilter;
+static enum Filter applyFilters[256];
+static size_t declFilter;
+static enum Filter declFilters[256];
+
+static void dataFilter(void) {
+	uint8_t *filter[FilterCap];
+	for (enum Filter i = None; i < FilterCap; ++i) {
+		filter[i] = malloc(lineLen);
+		if (!filter[i]) err(EX_OSERR, "malloc");
+	}
+	for (uint32_t y = header.height-1; y < header.height; --y) {
+		uint32_t heuristic[FilterCap] = {0};
 		enum Filter minType = None;
-		for (enum Filter type = None; type < FilterCount; ++type) {
-			for (size_t i = 0; i < lineSize(); ++i) {
-				if (options.recon) {
+		for (enum Filter type = None; type < FilterCap; ++type) {
+			for (size_t i = 0; i < lineLen; ++i) {
+				if (filterRecon) {
 					filter[type][i] = recon(type, origBytes(y, i));
 				} else {
 					filter[type][i] = filt(type, origBytes(y, i));
@@ -391,49 +434,26 @@ static void filterData(void) {
 			}
 			if (heuristic[type] < heuristic[minType]) minType = type;
 		}
-
-		if (options.declareFilter) {
-			lines[y]->type = options.declareFilters[y % options.declareFilter];
+		if (declFilter) {
+			*lineType(y) = declFilters[y % declFilter];
 		} else {
-			lines[y]->type = minType;
+			*lineType(y) = minType;
 		}
-
-		if (options.applyFilter) {
-			enum Filter type = options.applyFilters[y % options.applyFilter];
-			memcpy(lines[y]->data, filter[type], lineSize());
+		if (applyFilter) {
+			memcpy(lineData(y), filter[applyFilters[y % applyFilter]], lineLen);
 		} else {
-			memcpy(lines[y]->data, filter[minType], lineSize());
+			memcpy(lineData(y), filter[minType], lineLen);
 		}
 	}
-}
-
-static void invert(void) {
-	for (uint32_t y = 0; y < header.height; ++y) {
-		for (size_t i = 0; i < lineSize(); ++i) {
-			lines[y]->data[i] ^= 0xFF;
-		}
-	}
-}
-
-static void mirror(void) {
-	for (uint32_t y = 0; y < header.height; ++y) {
-		for (size_t i = 0, j = lineSize() - 1; i < j; ++i, --j) {
-			uint8_t t = lines[y]->data[i];
-			lines[y]->data[i] = lines[y]->data[j];
-			lines[y]->data[j] = t;
-		}
-	}
-}
-
-static void zeroX(void) {
-	for (uint32_t y = 0; y < header.height; ++y) {
-		memset(lines[y]->data, 0, pixelSize());
+	for (enum Filter i = None; i < FilterCap; ++i) {
+		free(filter[i]);
 	}
 }
 
-static void zeroY(void) {
-	memset(lines[0]->data, 0, lineSize());
-}
+static bool invertData;
+static bool mirrorData;
+static bool zeroX;
+static bool zeroY;
 
 static void glitch(const char *inPath, const char *outPath) {
 	if (inPath) {
@@ -441,98 +461,145 @@ static void glitch(const char *inPath, const char *outPath) {
 		file = fopen(path, "r");
 		if (!file) err(EX_NOINPUT, "%s", path);
 	} else {
-		path = "(stdin)";
+		path = "stdin";
 		file = stdin;
 	}
 
-	readSignature();
-	readHeader();
-	if (header.color == Indexed) readPalette();
-	readData();
+	sigRead();
+	struct Chunk ihdr = chunkRead();
+	if (strcmp(ihdr.type, "IHDR")) {
+		errx(EX_DATAERR, "%s: expected IHDR, found %s", path, ihdr.type);
+	}
+	headerRead(ihdr);
+	if (header.interlace != Progressive) {
+		errx(EX_CONFIG, "%s: unsupported interlacing", path);
+	}
+
+	palClear();
+	dataAlloc();
+	for (;;) {
+		struct Chunk chunk = chunkRead();
+		if (!strcmp(chunk.type, "PLTE")) {
+			palRead(chunk);
+		} else if (!strcmp(chunk.type, "tRNS")) {
+			transRead(chunk);
+		} else if (!strcmp(chunk.type, "IDAT")) {
+			dataRead(chunk);
+		} else if (!strcmp(chunk.type, "IEND")) {
+			break;
+		} else {
+			chunkSkip(chunk);
+		}
+	}
 	fclose(file);
 
-	scanlines();
-	reconData();
-	filterData();
-	if (options.invert) invert();
-	if (options.mirror) mirror();
-	if (options.zeroX) zeroX();
-	if (options.zeroY) zeroY();
-	free(lines);
+	dataRecon();
+	dataFilter();
 
+	if (invertData) {
+		for (uint32_t y = 0; y < header.height; ++y) {
+			for (size_t i = 0; i < lineLen; ++i) {
+				lineData(y)[i] ^= 0xFF;
+			}
+		}
+	}
+	if (mirrorData) {
+		for (uint32_t y = 0; y < header.height; ++y) {
+			for (size_t i = 0, j = lineLen-1; i < j; ++i, --j) {
+				uint8_t x = lineData(y)[i];
+				lineData(y)[i] = lineData(y)[j];
+				lineData(y)[j] = x;
+			}
+		}
+	}
+	if (zeroX) {
+		for (uint32_t y = 0; y < header.height; ++y) {
+			memset(lineData(y), 0, pixelLen);
+		}
+	}
+	if (zeroY) {
+		memset(lineData(0), 0, lineLen);
+	}
+
+	char buf[PATH_MAX];
 	if (outPath) {
 		path = outPath;
-		file = fopen(path, "w");
-		if (!file) err(EX_CANTCREAT, "%s", path);
+		if (outPath == inPath) {
+			snprintf(buf, sizeof(buf), "%sg", outPath);
+			file = fopen(buf, "wx");
+			if (!file) err(EX_CANTCREAT, "%s", buf);
+		} else {
+			file = fopen(path, "w");
+			if (!file) err(EX_CANTCREAT, "%s", outPath);
+		}
 	} else {
-		path = "(stdout)";
+		path = "stdout";
 		file = stdout;
 	}
 
-	writeSignature();
-	writeHeader();
-	if (header.color == Indexed) writePalette();
-	writeData();
-	writeEnd();
+	sigWrite();
+	headerWrite();
+	if (header.color == Indexed) {
+		palWrite();
+		if (trans.len) transWrite();
+	}
+	dataWrite();
 	free(data);
-
 	int error = fclose(file);
 	if (error) err(EX_IOERR, "%s", path);
+
+	if (outPath && outPath == inPath) {
+		error = rename(buf, outPath);
+		if (error) err(EX_CANTCREAT, "%s", outPath);
+	}
 }
 
-static enum Filter parseFilter(const char *s) {
-	switch (s[0]) {
+static enum Filter parseFilter(const char *str) {
+	switch (str[0]) {
 		case 'N': case 'n': return None;
 		case 'S': case 's': return Sub;
 		case 'U': case 'u': return Up;
 		case 'A': case 'a': return Average;
 		case 'P': case 'p': return Paeth;
-		default: errx(EX_USAGE, "invalid filter type %s", s);
+		default: errx(EX_USAGE, "invalid filter type %s", str);
 	}
 }
 
-static uint8_t parseFilters(enum Filter *filters, const char *s) {
-	uint8_t len = 0;
-	do {
-		filters[len++] = parseFilter(s);
-		s = strchr(s, ',');
-	} while (s++);
+static size_t parseFilters(enum Filter *filters, char *str) {
+	size_t len = 0;
+	while (str) {
+		char *filt = strsep(&str, ",");
+		filters[len++] = parseFilter(filt);
+	}
 	return len;
 }
 
 int main(int argc, char *argv[]) {
 	bool stdio = false;
-	char *output = NULL;
+	char *outPath = NULL;
 
-	int opt;
-	while (0 < (opt = getopt(argc, argv, "a:cd:fimo:prxy"))) {
+	for (int opt; 0 < (opt = getopt(argc, argv, "a:cd:fimo:prxy"));) {
 		switch (opt) {
-			break; case 'a':
-				options.applyFilter = parseFilters(options.applyFilters, optarg);
+			break; case 'a': applyFilter = parseFilters(applyFilters, optarg);
 			break; case 'c': stdio = true;
-			break; case 'd':
-				options.declareFilter = parseFilters(options.declareFilters, optarg);
-			break; case 'f': options.filt = true;
-			break; case 'i': options.invert = true;
-			break; case 'm': options.mirror = true;
-			break; case 'o': output = optarg;
-			break; case 'p': options.brokenPaeth = true;
-			break; case 'r': options.recon = true;
-			break; case 'x': options.zeroX = true;
-			break; case 'y': options.zeroY = true;
-			break; default: return EX_USAGE;
+			break; case 'd': declFilter = parseFilters(declFilters, optarg);
+			break; case 'f': reconFilter = true;
+			break; case 'i': invertData = true;
+			break; case 'm': mirrorData = true;
+			break; case 'o': outPath = optarg;
+			break; case 'p': brokenPaeth = true;
+			break; case 'r': filterRecon = true;
+			break; case 'x': zeroX = true;
+			break; case 'y': zeroY = true;
+			break; default:  return EX_USAGE;
 		}
 	}
 
-	if (argc - optind == 1 && (output || stdio)) {
-		glitch(argv[optind], output);
-	} else if (optind < argc) {
+	if (optind < argc) {
 		for (int i = optind; i < argc; ++i) {
-			glitch(argv[i], argv[i]);
+			glitch(argv[i], (stdio ? NULL : outPath ? outPath : argv[i]));
 		}
 	} else {
-		glitch(NULL, output);
+		glitch(NULL, outPath);
 	}
-
-	return EX_OK;
 }
diff --git a/bin/hilex.c b/bin/hilex.c
index 0bcc5c7..2289ef9 100644
--- a/bin/hilex.c
+++ b/bin/hilex.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2020  June 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
diff --git a/bin/hilex.h b/bin/hilex.h
index 882b5f9..b57fc8c 100644
--- a/bin/hilex.h
+++ b/bin/hilex.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2020  June 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
diff --git a/bin/make.l b/bin/make.l
index ae1be2f..6296716 100644
--- a/bin/make.l
+++ b/bin/make.l
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2020  June 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
diff --git a/bin/man1/up.1 b/bin/man1/up.1
index 2240b99..aece79b 100644
--- a/bin/man1/up.1
+++ b/bin/man1/up.1
@@ -1,4 +1,4 @@
-.Dd June 21, 2021
+.Dd July 26, 2022
 .Dt UP 1
 .Os
 .
@@ -8,15 +8,9 @@
 .
 .Sh SYNOPSIS
 .Nm
-.Op Fl h
-.Op Ar file
-.
-.Nm
-.Fl c | t
-.Ar command
-.
-.Nm
-.Fl s
+.Op Fl c | h | s | t
+.Op Fl w Ar warn
+.Op Ar file | command
 .
 .Sh DESCRIPTION
 .Nm
@@ -65,6 +59,10 @@ Run a command with
 and
 .Xr shotty 1
 to produce an HTML file for upload.
+.It Fl w Ar warn
+Create an HTML redirect with
+.Ar warn
+in its title.
 .El
 .
 .Pp
diff --git a/bin/mdoc.l b/bin/mdoc.l
index f29b6ce..b6deacb 100644
--- a/bin/mdoc.l
+++ b/bin/mdoc.l
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2020  June 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
diff --git a/bin/modem.c b/bin/modem.c
index 2133ae0..4392e07 100644
--- a/bin/modem.c
+++ b/bin/modem.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2018  June 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
diff --git a/bin/mtags.c b/bin/mtags.c
index 11cd9c8..5c1a057 100644
--- a/bin/mtags.c
+++ b/bin/mtags.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2021  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2021  June 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
diff --git a/bin/nudge.c b/bin/nudge.c
index 108d245..8ae916e 100644
--- a/bin/nudge.c
+++ b/bin/nudge.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2020  June 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
diff --git a/bin/order.y b/bin/order.y
index c2e8797..b3cbf2d 100644
--- a/bin/order.y
+++ b/bin/order.y
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2019  June 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
diff --git a/bin/pbd.c b/bin/pbd.c
index 2fd401a..9f47b63 100644
--- a/bin/pbd.c
+++ b/bin/pbd.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2017  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2017  June 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
diff --git a/bin/png.h b/bin/png.h
index 15b6d13..0df4699 100644
--- a/bin/png.h
+++ b/bin/png.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2018  June 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
diff --git a/bin/pngo.c b/bin/pngo.c
index ba90889..eb51ccc 100644
--- a/bin/pngo.c
+++ b/bin/pngo.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2018, 2021  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2018, 2021  June 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
diff --git a/bin/psf2png.c b/bin/psf2png.c
index 1aaa863..c36238a 100644
--- a/bin/psf2png.c
+++ b/bin/psf2png.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2018  June 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
diff --git a/bin/ptee.c b/bin/ptee.c
index 8374bd8..52350a2 100644
--- a/bin/ptee.c
+++ b/bin/ptee.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2019  June 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
diff --git a/bin/quick.c b/bin/quick.c
index edf35a3..d814873 100644
--- a/bin/quick.c
+++ b/bin/quick.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2021  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2021  June 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
diff --git a/bin/relay.c b/bin/relay.c
index aa7913c..fd79946 100644
--- a/bin/relay.c
+++ b/bin/relay.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2019  June 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
diff --git a/bin/scheme.c b/bin/scheme.c
index 33fd8b6..2bae8f8 100644
--- a/bin/scheme.c
+++ b/bin/scheme.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2018, 2019  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2018, 2019  June 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
diff --git a/bin/sh.l b/bin/sh.l
index 9664b00..8f0f772 100644
--- a/bin/sh.l
+++ b/bin/sh.l
@@ -1,4 +1,4 @@
-/* Copyright (C) 2021  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2021  June 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
@@ -36,7 +36,7 @@ static int pop(void) {
 }
 %}
 
-%s Param Command Arith Backtick
+%s Param Command Arith Backtick Subshell
 %x DQuote HereDocDel HereDoc HereDocLit
 
 word [[:alnum:]_.-]+
@@ -51,7 +51,7 @@ reserved [!{}]|else|do|elif|for|done|fi|then|until|while|if|case|esac
 
 "\\". { return Escape; }
 
-<INITIAL,DQuote,HereDoc,Param,Command,Arith>{
+<INITIAL,DQuote,HereDoc,Param,Command,Arith,Subshell>{
 	"$"[*@#?$!0-9-] |
 	"$"[_[:alpha:][_[:alnum:]]* |
 	"${"[#]?{param}"}" {
@@ -73,6 +73,10 @@ reserved [!{}]|else|do|elif|for|done|fi|then|until|while|if|case|esac
 		BEGIN(push(Backtick));
 		return Subst;
 	}
+	"(" {
+		BEGIN(push(Subshell));
+		return Normal;
+	}
 }
 <Param>"}" |
 <Command>")" |
@@ -81,6 +85,10 @@ reserved [!{}]|else|do|elif|for|done|fi|then|until|while|if|case|esac
 	BEGIN(pop());
 	return Subst;
 }
+<Subshell>")" {
+	BEGIN(pop());
+	return Normal;
+}
 
 "\n" {
 	first = true;
diff --git a/bin/shotty.l b/bin/shotty.l
index ff78d24..7496e75 100644
--- a/bin/shotty.l
+++ b/bin/shotty.l
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019, 2021  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2019, 2021  June 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
diff --git a/bin/title.c b/bin/title.c
index 82f89d9..47ff720 100644
--- a/bin/title.c
+++ b/bin/title.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2019  June 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
diff --git a/bin/up.sh b/bin/up.sh
index cebc29c..6305b1e 100644
--- a/bin/up.sh
+++ b/bin/up.sh
@@ -4,21 +4,32 @@ set -eu
 readonly Host='temp.causal.agency'
 readonly Root='/var/www'
 
+temp=
+temp() {
+	temp=$(mktemp -d)
+	trap 'rm -r "$temp"' EXIT
+}
+
+warn=
 upload() {
 	src=$1
 	ext=${src##*.}
-	ts=$(date +'%s')
-	rand=$(openssl rand -hex 4)
-	url=$(printf '%s/%x%s.%s' "$Host" "$ts" "$rand" "$ext")
+	name=$(printf '%x%s' "$(date +%s)" "$(openssl rand -hex 4)")
+	url="${Host}/${name}.${ext}"
 	scp -q "$src" "${Host}:${Root}/${url}"
+	if test -n "$warn"; then
+		test -n "$temp" || temp
+		cat >"${temp}/warn.html" <<-EOF
+			<!DOCTYPE html>
+			<title>${warn}</title>
+			<meta http-equiv="refresh" content="0;url=${name}.${ext}">
+		EOF
+		url="${Host}/${name}.html"
+		scp -q "${temp}/warn.html" "${Host}:${Root}/${url}"
+	fi
 	echo "https://${url}"
 }
 
-temp() {
-	temp=$(mktemp -d)
-	trap 'rm -r "$temp"' EXIT
-}
-
 uploadText() {
 	temp
 	cat >"${temp}/input.txt"
@@ -64,12 +75,13 @@ uploadTerminal() {
 	upload "${temp}/term.html"
 }
 
-while getopts 'chst' opt; do
+while getopts 'chstw:' opt; do
 	case $opt in
 		(c) fn=uploadCommand;;
 		(h) fn=uploadHilex;;
 		(s) fn=uploadScreen;;
 		(t) fn=uploadTerminal;;
+		(w) warn=$OPTARG;;
 		(?) exit 1;;
 	esac
 done
diff --git a/bin/xx.c b/bin/xx.c
index 6d04f2f..39d7ec0 100644
--- a/bin/xx.c
+++ b/bin/xx.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2017  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2017  June 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
diff --git a/home/.config/X/resources b/home/.config/X/resources
index cfedf60..f4603cd 100644
--- a/home/.config/X/resources
+++ b/home/.config/X/resources
@@ -26,7 +26,7 @@ XTerm*VT100*translations: #override \n\
 	<Btn4Down>: scroll-back(1,line,m) \n\
 	<Btn5Down>: scroll-forw(1,line,m)
 
-XTerm*faceName: Go Mono:size=11
+XTerm*faceName: Go Mono:size=12
 XTerm*internalBorder: 6
 XTerm*colorBDMode: true
 XTerm*scrollBar: false
diff --git a/home/.config/git/config b/home/.config/git/config
index 1a6d5ce..b4c5d97 100644
--- a/home/.config/git/config
+++ b/home/.config/git/config
@@ -12,6 +12,9 @@
 [merge]
 	conflictStyle = diff3
 
+[push]
+	autoSetupRemote = true
+
 [pull]
 	rebase = true
 
diff --git a/txt/books.txt b/txt/books.txt
index 2c42476..23710ff 100644
--- a/txt/books.txt
+++ b/txt/books.txt
@@ -1,5 +1,8 @@
 [ 2022 ]
 
+ 10. ★☆☆ Sally Rooney, Conversations With Friends
+  9. ★☆☆ Sally Rooney, Normal People
+  8. ★★★ Andrea Stewart, The Bone Shard Emperor
   7. ★★☆ ed. Gwen Benaway, Maiden, Mother, Crone
   6. ★★★ Andrea Stewart, The Bone Shard Daughter
   5. ★☆☆ Madeline Miller, Circe
diff --git a/www/temp.causal.agency/up.c b/www/temp.causal.agency/up.c
index 4b83b56..561a890 100644
--- a/www/temp.causal.agency/up.c
+++ b/www/temp.causal.agency/up.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2020  June 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
diff --git a/www/text.causal.agency/037-care.7 b/www/text.causal.agency/037-care.7
index 3fffd77..83fef1d 100644
--- a/www/text.causal.agency/037-care.7
+++ b/www/text.causal.agency/037-care.7
@@ -121,10 +121,12 @@ a week later.
 It takes another 30 days
 to get the certificate of change,
 after which you can
-order a new birth certificate.
-In all it took about 3 months
+order a new birth certificate
+and RAMQ will (slowly) send you a form
+to get a new card.
+In all it took about 4 months
 from when I mailed the application
-to receiving the certificate of change.
+to having ID with my name on it.
 .
 .Ss Therapy
 I'm not seeking therapy
@@ -134,6 +136,20 @@ that's aware of it.
 I'll update this
 if I find one.
 .
+.Ss Piercings
+Ok I know this isn't trans-specific
+but at least for me getting piercings
+was gender-affirming.
+And I don't mean like
+getting your ears pierced
+is some kind of female rite of passage,
+because that's not even what I got done.
+Anyway,
+I went to Mauve.
+They're super nice,
+really know what they're doing,
+and their website has lots of info.
+.
 .Sh AUTHORS
 .An june Aq Mt june@causal.agency
 .