about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile37
-rw-r--r--README.748
-rw-r--r--buffer.c9
-rw-r--r--catgirl.1525
-rw-r--r--chat.c65
-rw-r--r--chat.h40
-rw-r--r--command.c56
-rw-r--r--complete.c (renamed from cache.c)122
-rw-r--r--config.c14
-rwxr-xr-xconfigure8
-rw-r--r--filter.c9
-rw-r--r--handle.c115
-rw-r--r--input.c35
-rw-r--r--irc.c37
-rw-r--r--log.c29
-rw-r--r--sandman.1 (renamed from scripts/sandman.1)0
-rw-r--r--sandman.m (renamed from scripts/sandman.m)13
-rw-r--r--scripts/.gitignore1
-rw-r--r--scripts/Makefile22
-rw-r--r--scripts/build-chroot.sh74
-rw-r--r--scripts/chroot-man.sh2
-rw-r--r--scripts/chroot-prompt.sh7
-rw-r--r--scripts/reconnect.sh10
-rw-r--r--scripts/sshd_config9
-rw-r--r--ui.c25
-rw-r--r--url.c35
-rw-r--r--window.c18
-rw-r--r--xdg.c3
29 files changed, 657 insertions, 712 deletions
diff --git a/.gitignore b/.gitignore
index b31d1c5..519791d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,5 @@ catgirl
 chroot.tar
 config.mk
 root/
+sandman
 tags
diff --git a/Makefile b/Makefile
index e8ef63a..e08e8e3 100644
--- a/Makefile
+++ b/Makefile
@@ -8,14 +8,18 @@ CFLAGS += ${CEXTS:%=-Wno-%}
 LDADD.libtls = -ltls
 LDADD.ncursesw = -lncursesw
 
+BINS = catgirl
+MANS = ${BINS:=.1}
+
 -include config.mk
 
 LDLIBS = ${LDADD.libtls} ${LDADD.ncursesw}
+LDLIBS.sandman = -framework Cocoa
 
 OBJS += buffer.o
-OBJS += cache.o
 OBJS += chat.o
 OBJS += command.o
+OBJS += complete.o
 OBJS += config.o
 OBJS += edit.o
 OBJS += filter.o
@@ -28,11 +32,13 @@ OBJS += url.o
 OBJS += window.o
 OBJS += xdg.o
 
+OBJS.sandman = sandman.o
+
 TESTS += edit.t
 
 dev: tags all check
 
-all: catgirl
+all: ${BINS}
 
 catgirl: ${OBJS}
 	${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@
@@ -41,6 +47,9 @@ ${OBJS}: chat.h
 
 edit.o edit.t input.o: edit.h
 
+sandman: ${OBJS.sandman}
+	${CC} ${LDFLAGS} ${OBJS.$@} ${LDLIBS.$@} -o $@
+
 check: ${TESTS}
 
 .SUFFIXES: .t
@@ -53,25 +62,13 @@ tags: *.[ch]
 	ctags -w *.[ch]
 
 clean:
-	rm -f catgirl ${OBJS} ${TESTS} tags
+	rm -f ${BINS} ${OBJS} ${OBJS.sandman} ${TESTS} tags
 
-install: catgirl catgirl.1
+install: ${BINS} ${MANS}
 	install -d ${DESTDIR}${BINDIR} ${DESTDIR}${MANDIR}/man1
-	install catgirl ${DESTDIR}${BINDIR}
-	install -m 644 catgirl.1 ${DESTDIR}${MANDIR}/man1
+	install ${BINS} ${DESTDIR}${BINDIR}
+	install -m 644 ${MANS} ${DESTDIR}${MANDIR}/man1
 
 uninstall:
-	rm -f ${DESTDIR}${BINDIR}/catgirl ${DESTDIR}${MANDIR}/man1/catgirl.1
-
-CHROOT_USER = chat
-CHROOT_GROUP = ${CHROOT_USER}
-
-chroot.tar: catgirl catgirl.1 scripts/chroot-prompt.sh scripts/chroot-man.sh
-chroot.tar: scripts/build-chroot.sh
-	sh scripts/build-chroot.sh ${CHROOT_USER} ${CHROOT_GROUP}
-
-install-chroot: chroot.tar
-	tar -px -f chroot.tar -C /home/${CHROOT_USER}
-
-clean-chroot:
-	rm -fr chroot.tar root
+	rm -f ${BINS:%=${DESTDIR}${BINDIR}/%}
+	rm -f ${MANS:%=${DESTDIR}${MANDIR}/man1/%}
diff --git a/README.7 b/README.7
index 2886b88..c4f82e8 100644
--- a/README.7
+++ b/README.7
@@ -1,5 +1,5 @@
 .\" To view this file: $ man ./README.7
-.Dd July 30, 2022
+.Dd May  8, 2025
 .Dt README 7
 .Os "Causal Agency"
 .
@@ -88,7 +88,7 @@ Reconnection:
 when the connection to the server is lost,
 .Nm
 exits.
-It can be run in a loop
+She can be run in a loop
 or connected to a bouncer,
 such as
 .Lk https://git.causal.agency/pounce "pounce" .
@@ -109,6 +109,10 @@ TLS is now ubiquitous
 and certificates are easy to obtain.
 .El
 .
+.Sh TESTIMONIALS
+.Dq catgirl has like the best scrolling i've ever used in a terminal application
+.D1 \(em my friend kylie
+.
 .Sh INSTALLING
 .Nm
 requires ncurses and
@@ -173,14 +177,14 @@ wrapper is provided for macOS
 to stop and start
 .Nm
 on system sleep and wake.
-Install it as follows:
+To enable him,
+configure with:
 .Bd -literal -offset indent
-$ make -C scripts sandman
-# make -C scripts install
+$ ./configure --enable-sandman
 .Ed
 .
 .Sh FILES
-.Bl -tag -width "command.c" -compact
+.Bl -tag -width "complete.c" -compact
 .It Pa chat.h
 global state and declarations
 .It Pa chat.c
@@ -201,8 +205,8 @@ command handling
 line wrapping
 .It Pa edit.c
 line editing
-.It Pa cache.c
-ordered cache
+.It Pa complete.c
+tab complete
 .It Pa url.c
 URL detection
 .It Pa filter.c
@@ -213,6 +217,8 @@ chat logging
 configuration parsing
 .It Pa xdg.c
 XDG base directories
+.It Pa sandman.m
+sleep/wake wrapper for macOS
 .El
 .
 .Pp
@@ -222,24 +228,13 @@ example
 .Xr tmux 1
 configuration for multiple networks
 and automatic reconnects
-.It Pa scripts/sandman.m
-sleep/wake wrapper for macOS
+.It Pa scripts/reconnect.sh
+example script to restart
+.Xr catgirl 1
+when she gets disconnected
 .It Pa scripts/notify-send.scpt
 .Xr notify-send 1
 in AppleScript
-.It Pa scripts/build-chroot.sh
-chroot builder for
-.Ox
-and
-.Fx
-.It Pa scripts/chroot-prompt.sh
-name prompt wrapper for chroot
-.It Pa scripts/chroot-man.sh
-.Xr man 1
-implementation for chroot
-.It Pa scripts/sshd_config
-.Xr sshd 8
-configuration for public chroot
 .El
 .
 .Sh CONTRIBUTING
@@ -252,12 +247,9 @@ For sending patches by email, see
 Mailing list archives are available at
 .Aq Lk https://causal.agency/list/catgirl.html .
 .
-.Pp
-Monetary contributions can be
-.Lk https://liberapay.com/june/donate "donated via Liberapay" .
-.
 .Sh SEE ALSO
-.Xr catgirl 1
+.Xr catgirl 1 ,
+.Xr sandman 1
 .
 .Pp
 IRC bouncer:
diff --git a/buffer.c b/buffer.c
index f82e553..6a7b412 100644
--- a/buffer.c
+++ b/buffer.c
@@ -30,7 +30,6 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sysexits.h>
 #include <time.h>
 #include <wchar.h>
 #include <wctype.h>
@@ -50,7 +49,7 @@ struct Buffer {
 
 struct Buffer *bufferAlloc(void) {
 	struct Buffer *buffer = calloc(1, sizeof(*buffer));
-	if (!buffer) err(EX_OSERR, "calloc");
+	if (!buffer) err(1, "calloc");
 	return buffer;
 }
 
@@ -107,7 +106,7 @@ static int flow(struct Lines *hard, int cols, const struct Line *soft) {
 	line->heat = soft->heat;
 	line->time = soft->time;
 	line->str = strdup(soft->str);
-	if (!line->str) err(EX_OSERR, "strdup");
+	if (!line->str) err(1, "strdup");
 
 	int width = 0;
 	int align = 0;
@@ -185,7 +184,7 @@ static int flow(struct Lines *hard, int cols, const struct Line *soft) {
 
 		size_t cap = StyleCap + align + strlen(&wrap[n]) + 1;
 		line->str = malloc(cap);
-		if (!line->str) err(EX_OSERR, "malloc");
+		if (!line->str) err(1, "malloc");
 
 		char *end = &line->str[cap];
 		str = seprintf(line->str, end, "%*s", (width = align), "");
@@ -209,7 +208,7 @@ int bufferPush(
 	soft->heat = heat;
 	soft->time = time;
 	soft->str = strdup(str);
-	if (!soft->str) err(EX_OSERR, "strdup");
+	if (!soft->str) err(1, "strdup");
 	if (heat < thresh) return 0;
 	return flow(&buffer->hard, cols, soft);
 }
diff --git a/catgirl.1 b/catgirl.1
index 32eb365..f2a2fbb 100644
--- a/catgirl.1
+++ b/catgirl.1
@@ -1,4 +1,4 @@
-.Dd May 29, 2022
+.Dd May 24, 2024
 .Dt CATGIRL 1
 .Os
 .
@@ -8,7 +8,7 @@
 .
 .Sh SYNOPSIS
 .Nm
-.Op Fl KRelqv
+.Op Fl Relqv
 .Op Fl C Ar copy
 .Op Fl H Ar hash
 .Op Fl I Ar highlight
@@ -46,9 +46,9 @@
 The
 .Nm
 IRC client
-provides a curses interface
-for TLS-only
-Internet Relay Chat.
+provides a curses interface for
+Internet Relay Chat
+over TLS.
 The only required option is
 .Fl h ,
 the host name to connect to.
@@ -62,7 +62,8 @@ in
 to view the list of
 .Sx COMMANDS
 and
-.Sx KEY BINDINGS .
+.Sx KEY BINDINGS
+in this manual.
 .
 .Pp
 Options can be loaded from files
@@ -78,50 +79,65 @@ unless the path starts with
 .Ql \&./
 or
 .Ql \&../ .
-Files and flags listed later
-on the command line
-take precedence over
-those listed earlier.
+For example,
+a configuration file at
+.Pa ~/.config/catgirl/tilde
+can be loaded by running
+.Ql catgirl tilde .
 .
 .Pp
 Each option is placed on a line,
 and lines beginning with
 .Ql #
 are ignored.
+An optional
+.Ql =
+may appear
+between an option and its value.
 The options are listed below
 following their corresponding flags.
+Flags and options in files are processed
+in the order they appear on the command line,
+so later values override earlier values.
 .
 .Bl -tag -width Ds
-.It Fl C Ar util | Cm copy No = Ar util
-Set the utility used by
-.Ic /copy .
+.It Fl C Ar util | Cm copy Ar util
+Set the utility used by the
+.Ic /copy 
+command.
 Subsequent
 .Cm copy
-options append arguments to
-.Ar util .
-The URL to copy is provided to
-.Ar util
-on standard input.
+options add arguments
+to the utility.
+The URL to copy is provided
+to the utility on standard input.
 The default is the first available of
 .Xr pbcopy 1 ,
 .Xr wl-copy 1 ,
-.Xr xclip 1 ,
+.Xr xclip 1
+or
 .Xr xsel 1 .
 .
-.It Fl H Ar seed,bound | Cm hash No = Ar seed,bound
-Set the initial seed
-of the nick and channel
-color hash function
-and the maximum IRC color value
-produced by the function.
-The default is 0,75.
-To use only colors from
-the 16-color terminal set,
+.It Fl H Ar seed,bound | Cm hash Ar seed,bound
+Set the seed for choosing
+nick and channel colours
+and the maximum IRC colour value
+that will be chosen.
+Changing the seed
+will randomize the chosen colours,
+in case you don't like the ones
+chosen for yourself or your crush.
+.Pp
+The default is 0,75,
+which uses colours
+in the 256-colour terminal set.
+To use only colours
+from the 16-colour terminal set,
 use 0,15.
-To disable nick and channel colors,
+To disable nick and channel colours,
 use 0,0.
 .
-.It Fl I Ar pattern | Cm highlight No = Ar pattern
+.It Fl I Ar pattern | Cm highlight Ar pattern
 Add a case-insensitive message highlight pattern,
 which may contain
 .Ql * ,
@@ -132,60 +148,51 @@ wildcards as in
 .Xr glob 7 .
 The format of the pattern is as follows:
 .Bd -ragged -offset indent
+.\" FIXME: there's really no reason !user@host should be required
 .Ar nick Ns Op Ar !user@host Op Ar command Op Ar channel Op Ar message
 .Ed
 .Pp
 The commands which can be matched are:
-.Sy INVITE ,
-.Sy JOIN ,
-.Sy NICK ,
-.Sy NOTICE ,
-.Sy PART ,
-.Sy PRIVMSG ,
-.Sy QUIT ,
-.Sy SETNAME .
-.
-.It Fl K | Cm kiosk
-Disable the
-.Ic /copy ,
-.Ic /debug ,
-.Ic /exec ,
-.Ic /join ,
-.Ic /list ,
-.Ic /msg ,
-.Ic /open ,
-.Ic /part ,
-.Ic /query ,
-.Ic /quote
-commands.
-Replace the username
-with a hash of its original value.
+.Sy invite ,
+.Sy join ,
+.Sy nick ,
+.Sy notice ,
+.Sy part ,
+.Sy privmsg ,
+.Sy quit ,
+.Sy setname .
+.Pp
+For example,
+to highlight whenever your crush
+joins your favourite channel:
+.Pp
+.Dl highlight crush!*@* join #channel
 .
-.It Fl N Ar util | Cm notify No = Ar util
+.It Fl N Ar util | Cm notify Ar util
 Send notifications using a utility.
 Subsequent
 .Cm notify
-options append arguments to
-.Ar util .
+options add arguments
+to the utility.
 The window name and message
-are provided to
-.Ar util
+are provided to the utility
 as two additional arguments,
 appropriate for
 .Xr notify-send 1 .
 .
-.It Fl O Ar util | Cm open No = Ar util
-Set the utility used by
-.Ic /open .
+.It Fl O Ar util | Cm open Ar util
+Set the utility used by the
+.Ic /open
+command.
 Subsequent
 .Cm open
-options append arguments to
-.Ar util .
-The URL to open is provided to
-.Ar util
-as an argument.
+options add arguments
+to the utility.
+The URL to open is provided
+to the utility as an additional argument.
 The default is the first available of
-.Xr open 1 ,
+.Xr open 1
+or
 .Xr xdg-open 1 .
 .
 .It Fl R | Cm restrict
@@ -201,58 +208,75 @@ option,
 and viewing this manual with
 .Ic /help .
 .
-.It Fl S Ar host | Cm bind No = Ar host
+.It Fl S Ar host | Cm bind Ar host
 Bind to source address
 .Ar host
 when connecting to the server.
-To connect from any address
-over IPv4 only,
+To connect from any IPv4 address,
 use 0.0.0.0.
-To connect from any address
-over IPv6 only,
+To connect from any IPv6 address,
 use ::.
 .
-.It Fl T Ns Oo Ar format Oc | Cm timestamp Op = Ar format
-Show timestamps by default,
-in the specified
-.Xr strftime 3
-.Ar format .
-The format string may contain
-raw IRC formatting codes.
-The default format is
-.Qq \&%X .
+.It Fl T Ns Oo Ar format Oc | Cm timestamp Op Ar format
+Show timestamps by default.
+The optional
+.Ar format
+string is interpreted by
+.Xr strftime 3 .
+The string may contain
+raw IRC formatting codes,
+if you can figure out
+how to enter them.
 .
-.It Fl a Ar user : Ns Ar pass | Cm sasl-plain No = Ar user : Ns Ar pass
-Authenticate as
-.Ar user
-with
-.Ar pass
-using SASL PLAIN.
+.It Fl a Ar user : Ns Ar pass | Cm sasl-plain Ar user : Ns Ar pass
+Authenticate with NickServ
+during connection using SASL PLAIN.
+.Nm
+will disconnect
+if authentication fails.
 Leave
 .Ar pass
-blank to prompt for the password.
+blank to prompt for the password when
+.Nm
+starts.
 .
-.It Fl c Ar path | Cm cert No = Ar path
-Load the TLS client certificate from
-.Ar path .
-The
-.Ar path
-is searched for in the same manner
-as configuration files.
-If the private key is in a separate file,
-it is loaded with
-.Cm priv .
-With
-.Cm sasl-external ,
-authenticate using SASL EXTERNAL.
-Certificates can be generated with
-.Fl g .
+.It Fl c Ar path | Cm cert Ar path
+Connect using a TLS client certificate
+loaded from
+.Ar path ,
+which is searched for
+in the same manner as configuration files.
+If the private key
+is in a separate file,
+additionally specify it with the
+.Cm priv
+option.
+.Pp
+To use this certificate
+to authenticate to NickServ
+using CertFP,
+use the
+.Cm sasl-external
+option.
+See
+.Sx Configuring CertFP .
+.Pp
+Client certificates
+can be generated with the
+.Fl g
+flag.
 .
 .It Fl e | Cm sasl-external
-Authenticate using SASL EXTERNAL,
-also known as CertFP.
-The TLS client certificate is loaded with
-.Cm cert .
+Authenticate to NickServ
+during connection using CertFP
+via SASL EXTERNAL.
+.Nm
+will disconnect
+if authentication fails.
+The client certificate
+must be specified with the
+.Cm cert
+option.
 See
 .Sx Configuring CertFP .
 .
@@ -262,11 +286,11 @@ Generate a TLS client certificate using
 and write it to
 .Ar path .
 .
-.It Fl h Ar host | Cm host No = Ar host
-Connect to
+.It Fl h Ar host | Cm host Ar host
+Connect to the IRC server
 .Ar host .
 .
-.It Fl i Ar pattern | Cm ignore No = Ar pattern
+.It Fl i Ar pattern | Cm ignore Ar pattern
 Add a case-insensitive message ignore pattern,
 which may contain
 .Ql * ,
@@ -281,56 +305,69 @@ The format of the pattern is as follows:
 .Ed
 .Pp
 The commands which can be matched are:
-.Sy INVITE ,
-.Sy JOIN ,
-.Sy NICK ,
-.Sy NOTICE ,
-.Sy PART ,
-.Sy PRIVMSG ,
-.Sy QUIT ,
-.Sy SETNAME .
+.Sy invite ,
+.Sy join ,
+.Sy nick ,
+.Sy notice ,
+.Sy part ,
+.Sy privmsg ,
+.Sy quit ,
+.Sy setname .
+.Pp
+Visibility of ignored messages
+can be toggled using
+.Ic M--
+and
+.Ic M-+ .
 .
-.It Fl j Ar channels Oo Ar keys Oc | Cm join No = Ar channels Oo Ar keys Oc
+.It Fl j Ar channels Oo Ar keys Oc | Cm join Ar channels Oo Ar keys Oc
 Join the comma-separated list of
 .Ar channels
 with the optional comma-separated list of channel
 .Ar keys .
+No spaces may appear in either list.
 .
-.It Fl k Ar path | Cm priv No = Ar priv
-Load the TLS client private key from
-.Ar path .
-The
-.Ar path
-is searched for in the same manner
-as configuration files.
+.It Fl k Ar path | Cm priv Ar path
+Load the TLS client private key 
+for a certificate loaded with the
+.Cm cert
+option from
+.Ar path ,
+which is search for
+in the same manner as configuration files.
 .
 .It Fl l | Cm log
-Log chat events to files in paths
-.Pa $XDG_DATA_HOME/catgirl/log/network/channel/YYYY-MM-DD.log .
+Log messages to files in
+.Pa $XDG_DATA_HOME/catgirl/log
+.Po
+usually
+.Pa ~/.local/share/catgirl/log
+.Pc .
+Directories are created
+for each network and channel,
+and files are created for each date
+in the format
+.Pa YYYY-MM-DD.log .
 .
-.It Fl m Ar mode | Cm mode No = Ar mode
-Set the user
-.Ar mode .
+.It Fl m Ar modes | Cm mode Ar modes
+Set user modes as soon as possible
+after connecting.
 .
-.It Fl n Ar nick Oo Ar ... Oc | Cm nick No = Ar nick Oo Ar ... Oc
-Set nickname to
-.Ar nick .
-The default nickname is
-the value of the environment variable
+.It Fl n Ar nick Oo Ar ... Oc | Cm nick Ar nick Oo Ar ... Oc
+Set the nickname with optional fallbacks,
+should one nick be unavailable.
+Each nick is treated as a highlight word.
+The default nickname is the value of
 .Ev USER .
-Additional space-separated nicks
-will be tried in order
-if the first is not available,
-and all nicks
-are treated as highlight words.
 .
 .It Fl o
-Print the server certificate chain
-to standard output in PEM format
-and exit.
+Connect to the server
+only to obtain its certificate chain
+and write it to standard output
+in PEM format.
 .
-.It Fl p Ar port | Cm port No = Ar port
-Connect to
+.It Fl p Ar port | Cm port Ar port
+Connect to the IRC server on
 .Ar port .
 The default port is 6697.
 .
@@ -339,17 +376,27 @@ Raise the default message visibility threshold
 for new windows,
 hiding general events
 (joins, quits, etc.).
+The threshold can be lowered with
+.Ic M-- .
 .
-.It Fl r Ar real | Cm real No = Ar real
-Set realname to
-.Ar real .
-The default realname is the same as the nickname.
+.It Fl r Ar real | Cm real Ar real
+Set the
+.Dq realname
+which appears in
+.Ic /whois .
+The default is the same as the nickname.
+This is a good place to add your pronouns.
 .
-.It Fl s Ar name | Cm save No = Ar name
-Save and load the contents of windows from
+.It Fl s Ar name | Cm save Ar name
+Persist windows and their scrollback
+in a file called
 .Ar name
 in
-.Pa $XDG_DATA_DIRS/catgirl ,
+.Pa $XDG_DATA_DIRS/catgirl
+.Po
+usually
+.Pa ~/.local/share/catgirl
+.Pc ,
 or an absolute or relative path if
 .Ar name
 starts with
@@ -358,39 +405,50 @@ starts with
 or
 .Ql \&../ .
 .
-.It Fl t Ar path | Cm trust No = Ar path
-Trust the self-signed certificate
-loaded from
-.Ar path
-and disable server name verification.
-The
-.Ar path
-is searched for in the same manner
-as configuration files.
+.It Fl t Ar path | Cm trust Ar path
+Trust the self-signed certificate in
+.Ar path ,
+which is searched for
+in the same manner as configuration files.
+Server name verification is also disabled.
 See
 .Sx Connecting to Servers with Self-signed Certificates .
 .
-.It Fl u Ar user | Cm user No = Ar user
-Set username to
-.Ar user .
-The default username is the same as the nickname.
+.It Fl u Ar user | Cm user Ar user
+Set the username.
+This is almost entirely irrelevant,
+except that it's more likely to remain stable,
+and
+.Nm
+uses it to choose nick colours.
+The default is the same as the nickname.
 .
 .It Fl v | Cm debug
-Log raw IRC messages to the
+Log raw IRC protocol to the
 .Sy <debug>
-window
+window,
 as well as standard error
 if it is not a terminal.
 .
-.It Fl w Ar pass | Cm pass No = Ar pass
-Log in with the server password
-.Ar pass .
+.It Fl w Ar pass | Cm pass Ar pass
+Connect using a server password.
 Leave
 .Ar pass
-blank to prompt for the password.
+blank
+.Po
+using an
+.Ql =
+.Pc
+to prompt for the password when
+.Nm
+starts.
 .El
 .
 .Ss Configuring CertFP
+CertFP allows you to
+authenticate with NickServ during connection
+using a TLS client certificate
+rather than your account password.
 .Bl -enum
 .It
 Generate a new TLS client certificate:
@@ -398,32 +456,34 @@ Generate a new TLS client certificate:
 $ catgirl -g ~/.config/catgirl/example.pem
 .Ed
 .It
-Connect to the server using the certificate:
+Connect to the server using the certificate
+by adding the following configuration:
 .Bd -literal -offset indent
-cert = example.pem
-# or: $ catgirl -c example.pem
+cert example.pem
 .Ed
 .It
-Identify with services or use
-.Cm sasl-plain ,
+Identify with NickServ,
 then add the certificate fingerprint
 to your account:
 .Bd -literal -offset indent
 /ns CERT ADD
 .Ed
 .It
-Enable SASL EXTERNAL
+Enable SASL EXTERNAL in your configuration
 to require successful authentication
 when connecting
 (not possible on all networks):
 .Bd -literal -offset indent
-cert = example.pem
+cert example.pem
 sasl-external
-# or: $ catgirl -e -c example.pem
 .Ed
 .El
 .
 .Ss Connecting to Servers with Self-signed Certificates
+If connecting to a server fails
+with a certificate verification error
+due to a self-signed certificate,
+it needs to be trusted manually.
 .Bl -enum
 .It
 Connect to the server
@@ -436,8 +496,7 @@ Configure
 .Nm
 to trust the certificate:
 .Bd -literal -offset indent
-trust = example.pem
-# or: $ catgirl -t example.pem
+trust example.pem
 .Ed
 .El
 .
@@ -445,13 +504,13 @@ trust = example.pem
 The
 .Nm
 interface is split
-into three areas.
+into three main areas.
 .
 .Ss Status Line
 The top line of the terminal
 shows window statuses.
 Only the currently active window
-and windows with activity are listed.
+and windows with activity are shown.
 The status line for a window
 might look like this:
 .Bd -literal -offset indent
@@ -462,7 +521,8 @@ The number on the left
 is the window number.
 Following it may be one of
 .Ql - ,
-.Ql + ,
+.Ql +
+or
 .Ql ++ ,
 as well as
 .Ql = .
@@ -478,11 +538,11 @@ indicates the number of unread messages.
 The number following
 .Ql ~
 indicates how many lines
-are below the scroll position.
+are below the current scroll position.
 An
 .Ql @
 indicates that there is unsent input
-in the window's
+waiting in the window's
 .Sx Input Line .
 .Pp
 .Nm
@@ -512,7 +572,11 @@ with the nick between
 hyphens.
 .Pp
 Blank lines are inserted into the chat
-as unread markers.
+as unread markers
+whenever there are messages
+in a window that is not active
+or the terminal is not focused
+(in some terminal emulators).
 .Pp
 While scrolling,
 the most recent 5 lines of chat
@@ -540,68 +604,106 @@ starting at the point where it will be split
 into a second message.
 .
 .Sh COMMANDS
-Any unique prefix can be used to abbreviate a command.
+Commands can be abbreviated
+if no other command
+shares the same prefix.
 For example,
 .Ic /join
 can be typed
-.Ic /j .
+.Ic /j ,
+and
+.Ic /window
+can be typed
+.Ic /wi .
 .
 .Ss Chat Commands
 .Bl -tag -width Ds
 .It Ic /away Op Ar message
 Set or clear your away status.
+This is sent in reply to private messages
+and shown in
+.Ic /whois .
 .It Ic /cs Ar command
-Send a command to ChanServ.
+Send a command to ChanServ,
+the service for managing registered channels.
 .It Ic /invite Ar nick
-Invite a user to the channel.
+Invite someone to the channel.
 .It Ic /join Op Ar channel Op Ar key
 Join the named channel,
-the current channel,
+the current channel (if you've left),
 or the channel you've been invited to.
-.It Ic /list Op Ar channel
-List channels.
+.It Ic /list Op Ar search
+List channels, their user counts and their topics.
+The
+.Ar search
+can usually contain glob-style wildcards.
 .It Ic /me Op Ar action
 Send an action message.
+These are used to write messages in third person.
 .It Ic /msg Ar nick message
-Send a private message.
+Send a private message to someone.
 .It Ic /names
-List users in the channel.
+List the users in the channel.
 .It Ic /nick Ar nick
-Change nicknames.
+Change your nickname.
 .It Ic /notice Ar message
 Send a notice.
+It's best not to do this.
 .It Ic /ns Ar command
-Send a command to NickServ.
+Send a command to NickServ,
+the service for managing your account.
 .It Ic /ops
 List channel operators.
+They can kick or ban someone from the channel.
 .It Ic /part Op Ar message
 Leave the channel.
+Use
+.Ic /close
+if you want to close the window afterwards.
 .It Ic /query Ar nick
-Start a private conversation.
+Start a private conversation with someone.
 .It Ic /quit Op Ar message
-Quit IRC.
+Disconnect from IRC and close
+.Nm .
+You can do this even quicker with
+.Ic C-c .
 .It Ic /quote Ar command
 Send a raw IRC command.
-Use
+Often
+.Nm
+will not know how to interpret the results.
+You can use
 .Ic M--
-to show unknown replies.
+to show unknown server responses
+in the
+.Sy <network>
+or channel windows.
 .It Ic /say Ar message
 Send a regular message.
+This is useful
+if the message you want to send
+begins with a slash.
 .It Ic /setname Ar name
-Update realname
-if supported by the server.
+Update your 
+.Dq realname
+if the server supports it.
+This may be broadcast
+to other users
+with clients that support it.
 .It Ic /topic Op Ar topic
 Show or set the topic of the channel.
 Press
 .Ic Tab
-twice to copy the current topic.
+twice immediately after
+.Ic /topic
+to copy the current topic.
 .It Ic /whois Op Ar nick
 Query information about a user or yourself.
 .It Ic /whowas Ar nick
 Query past information about a user.
 .El
 .
-.Ss UI Commands
+.Ss Interface Commands
 .Bl -tag -width Ds
 .It Ic /close Op Ar name | num
 Close the named, numbered or current window.
@@ -649,6 +751,9 @@ use the
 option.
 .It Ic /move Oo Ar name Oc Ar num
 Move the named or current window to number.
+.It Ic /o ...
+Alias of
+.Ic /open .
 .It Ic /open Op Ar count
 Open each of
 .Ar count
@@ -922,9 +1027,7 @@ The
 .Nm
 client exits 0
 if requested by the user,
-.Dv EX_UNAVAILABLE
-(69)
-if the connection is lost,
+69 if the connection is lost,
 and >0 if any other error occurs.
 .
 .Sh EXAMPLES
diff --git a/chat.c b/chat.c
index 39b1a93..bc23c3f 100644
--- a/chat.c
+++ b/chat.c
@@ -41,7 +41,6 @@
 #include <sys/stat.h>
 #include <sys/time.h>
 #include <sys/wait.h>
-#include <sysexits.h>
 #include <time.h>
 #include <tls.h>
 #include <unistd.h>
@@ -70,7 +69,7 @@ static void genCert(const char *path) {
 		"-nodes", "-subj", subj, "-out", path, "-keyout", path,
 		NULL
 	);
-	err(EX_UNAVAILABLE, "openssl");
+	err(127, "openssl");
 }
 
 char *idNames[IDCap] = {
@@ -93,7 +92,7 @@ static void exitSave(void) {
 	int error = uiSave();
 	if (error) {
 		warn("%s", save);
-		_exit(EX_IOERR);
+		_exit(1);
 	}
 }
 
@@ -104,7 +103,7 @@ int utilPipe[2] = { -1, -1 };
 static void execRead(void) {
 	char buf[1024];
 	ssize_t len = read(execPipe[0], buf, sizeof(buf) - 1);
-	if (len < 0) err(EX_IOERR, "read");
+	if (len < 0) err(1, "read");
 	if (!len) return;
 	buf[len] = '\0';
 	for (char *ptr = buf; ptr;) {
@@ -116,7 +115,7 @@ static void execRead(void) {
 static void utilRead(void) {
 	char buf[1024];
 	ssize_t len = read(utilPipe[0], buf, sizeof(buf) - 1);
-	if (len < 0) err(EX_IOERR, "read");
+	if (len < 0) err(1, "read");
 	if (!len) return;
 	buf[len] = '\0';
 	for (char *ptr = buf; ptr;) {
@@ -135,7 +134,7 @@ static void parseHash(char *str) {
 
 static void parsePlain(char *str) {
 	self.plainUser = strsep(&str, ":");
-	if (!str) errx(EX_USAGE, "SASL PLAIN missing colon");
+	if (!str) errx(1, "SASL PLAIN missing colon");
 	self.plainPass = str;
 }
 
@@ -159,27 +158,27 @@ static void sandboxEarly(bool log) {
 	if (log) {
 		char buf[PATH_MAX];
 		int error = unveil(dataPath(buf, sizeof(buf), "log", 0), "wc");
-		if (error) err(EX_OSERR, "unveil");
+		if (error) err(1, "unveil");
 		ptr = seprintf(ptr, end, " wpath cpath");
 	}
 
 	if (!self.restricted) {
 		int error = unveil("/", "x");
-		if (error) err(EX_OSERR, "unveil");
+		if (error) err(1, "unveil");
 		ptr = seprintf(ptr, end, " proc exec");
 	}
 
 	promisesInitial = ptr;
 	ptr = seprintf(ptr, end, " inet dns");
 	int error = pledge(promises, NULL);
-	if (error) err(EX_OSERR, "pledge");
+	if (error) err(1, "pledge");
 }
 
 static void sandboxLate(int irc) {
 	(void)irc;
 	*promisesInitial = '\0';
 	int error = pledge(promises, NULL);
-	if (error) err(EX_OSERR, "pledge");
+	if (error) err(1, "pledge");
 }
 
 #elif defined __FreeBSD__
@@ -202,7 +201,7 @@ static void sandboxLate(int irc) {
 		|| caph_rights_limit(
 			irc, cap_rights_init(&rights, CAP_SEND, CAP_RECV, CAP_EVENT)
 		);
-	if (error) err(EX_OSERR, "cap_rights_limit");
+	if (error) err(1, "cap_rights_limit");
 
 	// caph_cache_tzdata(3) doesn't load UTC info, which we need for
 	// certificate verification. gmtime(3) does.
@@ -210,7 +209,7 @@ static void sandboxLate(int irc) {
 	gmtime(&(time_t) { time(NULL) });
 
 	error = cap_enter();
-	if (error) err(EX_OSERR, "cap_enter");
+	if (error) err(1, "cap_enter");
 }
 
 #else
@@ -245,7 +244,6 @@ int main(int argc, char *argv[]) {
 		{ .val = 'C', .name = "copy", required_argument },
 		{ .val = 'H', .name = "hash", required_argument },
 		{ .val = 'I', .name = "highlight", required_argument },
-		{ .val = 'K', .name = "kiosk", no_argument },
 		{ .val = 'N', .name = "notify", required_argument },
 		{ .val = 'O', .name = "open", required_argument },
 		{ .val = 'R', .name = "restrict", no_argument },
@@ -286,7 +284,6 @@ int main(int argc, char *argv[]) {
 			break; case 'C': utilPush(&urlCopyUtil, optarg);
 			break; case 'H': parseHash(optarg);
 			break; case 'I': filterAdd(Hot, optarg);
-			break; case 'K': self.kiosk = true;
 			break; case 'N': utilPush(&uiNotifyUtil, optarg);
 			break; case 'O': utilPush(&urlOpenUtil, optarg);
 			break; case 'R': self.restricted = true;
@@ -319,47 +316,40 @@ int main(int argc, char *argv[]) {
 			break; case 'u': user = optarg;
 			break; case 'v': self.debug = true;
 			break; case 'w': pass = optarg;
-			break; default:  return EX_USAGE;
+			break; default:  return 1;
 		}
 	}
-	if (!host) errx(EX_USAGE, "host required");
+	if (!host) errx(1, "host required");
 
 	if (printCert) {
 #ifdef __OpenBSD__
 		int error = pledge("stdio inet dns", NULL);
-		if (error) err(EX_OSERR, "pledge");
+		if (error) err(1, "pledge");
 #endif
 		ircConfig(true, NULL, NULL, NULL);
 		ircConnect(bind, host, port);
 		ircPrintCert();
 		ircClose();
-		return EX_OK;
+		return 0;
 	}
 
 	if (!self.nicks[0]) self.nicks[0] = getenv("USER");
-	if (!self.nicks[0]) errx(EX_CONFIG, "USER unset");
+	if (!self.nicks[0]) errx(1, "USER unset");
 	if (!user) user = self.nicks[0];
 	if (!real) real = self.nicks[0];
 
-	if (self.kiosk) {
-		char *hash;
-		int n = asprintf(&hash, "%08" PRIx32, _hash(user));
-		if (n < 0) err(EX_OSERR, "asprintf");
-		user = hash;
-	}
-
 	if (pass && !pass[0]) {
 		char *buf = malloc(512);
-		if (!buf) err(EX_OSERR, "malloc");
+		if (!buf) err(1, "malloc");
 		pass = readpassphrase("Server password: ", buf, 512, 0);
-		if (!pass) errx(EX_IOERR, "unable to read passphrase");
+		if (!pass) errx(1, "unable to read passphrase");
 	}
 
 	if (self.plainPass && !self.plainPass[0]) {
 		char *buf = malloc(512);
-		if (!buf) err(EX_OSERR, "malloc");
+		if (!buf) err(1, "malloc");
 		self.plainPass = readpassphrase("Account password: ", buf, 512, 0);
-		if (!self.plainPass) errx(EX_IOERR, "unable to read passphrase");
+		if (!self.plainPass) errx(1, "unable to read passphrase");
 	}
 
 	// Modes defined in RFC 1459:
@@ -374,7 +364,7 @@ int main(int argc, char *argv[]) {
 	set(&network.name, host);
 	set(&self.nick, "*");
 
-	inputCache();
+	inputCompletion();
 
 	ircConfig(insecure, trust, cert, priv);
 
@@ -418,10 +408,9 @@ int main(int argc, char *argv[]) {
 	signal(SIGTERM, signalHandler);
 	signal(SIGCHLD, signalHandler);
 
-	bool pipes = !self.kiosk && !self.restricted;
-	if (pipes) {
+	if (!self.restricted) {
 		int error = pipe(utilPipe) || pipe(execPipe);
-		if (error) err(EX_OSERR, "pipe");
+		if (error) err(1, "pipe");
 
 		fcntl(utilPipe[0], F_SETFD, FD_CLOEXEC);
 		fcntl(utilPipe[1], F_SETFD, FD_CLOEXEC);
@@ -437,8 +426,8 @@ int main(int argc, char *argv[]) {
 		{ .events = POLLIN, .fd = execPipe[0] },
 	};
 	while (!self.quit) {
-		int nfds = poll(fds, (pipes ? ARRAY_LEN(fds) : 2), -1);
-		if (nfds < 0 && errno != EINTR) err(EX_IOERR, "poll");
+		int nfds = poll(fds, (self.restricted ? 2 : ARRAY_LEN(fds)), -1);
+		if (nfds < 0 && errno != EINTR) err(1, "poll");
 		if (nfds > 0) {
 			if (fds[0].revents) inputRead();
 			if (fds[1].revents) ircRecv();
@@ -456,12 +445,12 @@ int main(int argc, char *argv[]) {
 				.it_interval.tv_sec = 30,
 			};
 			int error = setitimer(ITIMER_REAL, &timer, NULL);
-			if (error) err(EX_OSERR, "setitimer");
+			if (error) err(1, "setitimer");
 		}
 		if (signals[SIGALRM]) {
 			signals[SIGALRM] = 0;
 			if (ping) {
-				errx(EX_UNAVAILABLE, "ping timeout");
+				errx(69, "ping timeout");
 			} else {
 				ircFormat("PING nyaa\r\n");
 				ping = true;
diff --git a/chat.h b/chat.h
index 15c757f..369747c 100644
--- a/chat.h
+++ b/chat.h
@@ -35,7 +35,6 @@
 #include <stdio.h>
 #include <string.h>
 #include <strings.h>
-#include <sysexits.h>
 #include <time.h>
 #include <wchar.h>
 
@@ -131,7 +130,7 @@ static inline uint idFor(const char *name) {
 	if (idNext == IDCap) return Network;
 	idNames[idNext] = strdup(name);
 	idColors[idNext] = Default;
-	if (!idNames[idNext]) err(EX_OSERR, "strdup");
+	if (!idNames[idNext]) err(1, "strdup");
 	return idNext++;
 }
 
@@ -202,7 +201,6 @@ enum Cap {
 
 extern struct Self {
 	bool debug;
-	bool kiosk;
 	bool restricted;
 	size_t pos;
 	enum Cap caps;
@@ -222,7 +220,7 @@ extern struct Self {
 static inline void set(char **field, const char *value) {
 	free(*field);
 	*field = strdup(value);
-	if (!*field) err(EX_OSERR, "strdup");
+	if (!*field) err(1, "strdup");
 }
 
 #define ENUM_TAG \
@@ -274,7 +272,7 @@ static inline void utilPush(struct Util *util, const char *arg) {
 	if (1 + util->argc < UtilCap) {
 		util->argv[util->argc++] = arg;
 	} else {
-		errx(EX_CONFIG, "too many utility arguments");
+		errx(1, "too many utility arguments");
 	}
 }
 
@@ -304,7 +302,7 @@ const char *commandIsPrivmsg(uint id, const char *input);
 const char *commandIsNotice(uint id, const char *input);
 const char *commandIsAction(uint id, const char *input);
 size_t commandWillSplit(uint id, const char *input);
-void commandCache(void);
+void commandCompletion(void);
 
 enum Heat {
 	Ice,
@@ -346,7 +344,7 @@ void inputWait(void);
 void inputUpdate(void);
 bool inputPending(uint id);
 void inputRead(void);
-void inputCache(void);
+void inputCompletion(void);
 int inputSave(FILE *file);
 void inputLoad(FILE *file, size_t version);
 
@@ -408,26 +406,22 @@ int bufferReflow(
 	struct Buffer *buffer, int cols, enum Heat thresh, size_t tail
 );
 
-struct Entry {
-	enum Color color;
-	uint prefixBits;
-};
 struct Cursor {
 	uint gen;
 	struct Node *node;
-	struct Entry *entry;
 };
-const struct Entry *cacheGet(uint id, const char *key);
-struct Entry *cacheInsert(bool touch, uint id, const char *key);
-void cacheReplace(bool touch, const char *old, const char *new);
-void cacheRemove(uint id, const char *key);
-void cacheClear(uint id);
-const char *cacheComplete(struct Cursor *curs, uint id, const char *prefix);
-const char *cacheSearch(struct Cursor *curs, uint id, const char *substr);
-uint cacheNextID(struct Cursor *curs, const char *key);
-const char *cacheNextKey(struct Cursor *curs, uint id);
-void cacheAccept(struct Cursor *curs);
-void cacheReject(struct Cursor *curs);
+void completePush(uint id, const char *str, enum Color color);
+void completePull(uint id, const char *str, enum Color color);
+void completeReplace(const char *old, const char *new);
+void completeRemove(uint id, const char *str);
+enum Color completeColor(uint id, const char *str);
+uint *completeBits(uint id, const char *str);
+const char *completePrefix(struct Cursor *curs, uint id, const char *prefix);
+const char *completeSubstr(struct Cursor *curs, uint id, const char *substr);
+const char *completeEach(struct Cursor *curs, uint id);
+uint completeEachID(struct Cursor *curs, const char *str);
+void completeAccept(struct Cursor *curs);
+void completeReject(struct Cursor *curs);
 
 extern struct Util urlOpenUtil;
 extern struct Util urlCopyUtil;
diff --git a/command.c b/command.c
index 4fb58da..9b2b4eb 100644
--- a/command.c
+++ b/command.c
@@ -139,7 +139,7 @@ static void commandMsg(uint id, char *params) {
 	char *nick = strsep(&params, " ");
 	uint msg = idFor(nick);
 	if (idColors[msg] == Default) {
-		idColors[msg] = cacheGet(id, nick)->color;
+		idColors[msg] = completeColor(id, nick);
 	}
 	if (params) {
 		splitMessage("PRIVMSG", msg, params);
@@ -227,12 +227,12 @@ static void commandOps(uint id, char *params) {
 	);
 	bool first = true;
 	struct Cursor curs = {0};
-	for (const char *nick; (nick = cacheNextKey(&curs, id));) {
-		char prefix = bitPrefix(curs.entry->prefixBits);
+	for (const char *nick; (nick = completeEach(&curs, id));) {
+		char prefix = bitPrefix(*completeBits(id, nick));
 		if (!prefix || prefix == '+') continue;
 		ptr = seprintf(
 			ptr, end, "%s\3%02d%c%s\3",
-			(first ? "" : ", "), curs.entry->color, prefix, nick
+			(first ? "" : ", "), completeColor(id, nick), prefix, nick
 		);
 		first = false;
 	}
@@ -403,7 +403,7 @@ static void commandQuery(uint id, char *params) {
 	if (!params) return;
 	uint query = idFor(params);
 	if (idColors[query] == Default) {
-		idColors[query] = cacheGet(id, params)->color;
+		idColors[query] = completeColor(id, params);
 	}
 	windowShow(windowFor(query));
 }
@@ -420,10 +420,10 @@ static void commandWindow(uint id, char *params) {
 			return;
 		}
 		struct Cursor curs = {0};
-		for (const char *match; (match = cacheSearch(&curs, None, params));) {
-			id = idFind(match);
+		for (const char *str; (str = completeSubstr(&curs, None, params));) {
+			id = idFind(str);
 			if (!id) continue;
-			cacheAccept(&curs);
+			completeAccept(&curs);
 			windowShow(windowFor(id));
 			break;
 		}
@@ -516,7 +516,7 @@ static void commandExec(uint id, char *params) {
 	execID = id;
 
 	pid_t pid = fork();
-	if (pid < 0) err(EX_OSERR, "fork");
+	if (pid < 0) err(1, "fork");
 	if (pid) return;
 
 	setsid();
@@ -527,7 +527,7 @@ static void commandExec(uint id, char *params) {
 	const char *shell = getenv("SHELL") ?: "/bin/sh";
 	execl(shell, shell, "-c", params, NULL);
 	warn("%s", shell);
-	_exit(EX_UNAVAILABLE);
+	_exit(127);
 }
 
 static void commandHelp(uint id, char *params) {
@@ -545,7 +545,7 @@ static void commandHelp(uint id, char *params) {
 
 	uiHide();
 	pid_t pid = fork();
-	if (pid < 0) err(EX_OSERR, "fork");
+	if (pid < 0) err(1, "fork");
 	if (pid) return;
 
 	char buf[256];
@@ -554,13 +554,12 @@ static void commandHelp(uint id, char *params) {
 	execlp("man", "man", "1", "catgirl", NULL);
 	dup2(utilPipe[1], STDERR_FILENO);
 	warn("man");
-	_exit(EX_UNAVAILABLE);
+	_exit(127);
 }
 
 enum Flag {
 	BIT(Multiline),
 	BIT(Restrict),
-	BIT(Kiosk),
 };
 
 static const struct Handler {
@@ -572,37 +571,37 @@ static const struct Handler {
 	{ "/away", commandAway, 0, 0 },
 	{ "/ban", commandBan, 0, 0 },
 	{ "/close", commandClose, 0, 0 },
-	{ "/copy", commandCopy, Restrict | Kiosk, 0 },
+	{ "/copy", commandCopy, Restrict, 0 },
 	{ "/cs", commandCS, 0, 0 },
-	{ "/debug", commandDebug, Kiosk, 0 },
+	{ "/debug", commandDebug, 0, 0 },
 	{ "/deop", commandDeop, 0, 0 },
 	{ "/devoice", commandDevoice, 0, 0 },
 	{ "/except", commandExcept, 0, 0 },
-	{ "/exec", commandExec, Multiline | Restrict | Kiosk, 0 },
+	{ "/exec", commandExec, Multiline | Restrict, 0 },
 	{ "/help", commandHelp, 0, 0 }, // Restrict special case.
 	{ "/highlight", commandHighlight, 0, 0 },
 	{ "/ignore", commandIgnore, 0, 0 },
 	{ "/invex", commandInvex, 0, 0 },
 	{ "/invite", commandInvite, 0, 0 },
-	{ "/join", commandJoin, Kiosk, 0 },
+	{ "/join", commandJoin, 0, 0 },
 	{ "/kick", commandKick, 0, 0 },
-	{ "/list", commandList, Kiosk, 0 },
+	{ "/list", commandList, 0, 0 },
 	{ "/me", commandMe, Multiline, 0 },
 	{ "/mode", commandMode, 0, 0 },
 	{ "/move", commandMove, 0, 0 },
-	{ "/msg", commandMsg, Multiline | Kiosk, 0 },
+	{ "/msg", commandMsg, Multiline, 0 },
 	{ "/names", commandNames, 0, 0 },
 	{ "/nick", commandNick, 0, 0 },
 	{ "/notice", commandNotice, Multiline, 0 },
 	{ "/ns", commandNS, 0, 0 },
-	{ "/o", commandOpen, Restrict | Kiosk, 0 },
+	{ "/o", commandOpen, Restrict, 0 },
 	{ "/op", commandOp, 0, 0 },
-	{ "/open", commandOpen, Restrict | Kiosk, 0 },
+	{ "/open", commandOpen, Restrict, 0 },
 	{ "/ops", commandOps, 0, 0 },
-	{ "/part", commandPart, Kiosk, 0 },
-	{ "/query", commandQuery, Kiosk, 0 },
+	{ "/part", commandPart, 0, 0 },
+	{ "/query", commandQuery, 0, 0 },
 	{ "/quit", commandQuit, 0, 0 },
-	{ "/quote", commandQuote, Multiline | Kiosk, 0 },
+	{ "/quote", commandQuote, Multiline, 0 },
 	{ "/say", commandPrivmsg, Multiline, 0 },
 	{ "/setname", commandSetname, 0, CapSetname },
 	{ "/topic", commandTopic, 0, 0 },
@@ -672,7 +671,6 @@ size_t commandWillSplit(uint id, const char *input) {
 
 static bool commandAvailable(const struct Handler *handler) {
 	if (handler->flags & Restrict && self.restricted) return false;
-	if (handler->flags & Kiosk && self.kiosk) return false;
 	if (handler->caps && (handler->caps & self.caps) != handler->caps) {
 		return false;
 	}
@@ -695,8 +693,8 @@ void command(uint id, char *input) {
 
 	struct Cursor curs = {0};
 	const char *cmd = strsep(&input, " ");
-	const char *unique = cacheComplete(&curs, None, cmd);
-	if (unique && !cacheComplete(&curs, None, cmd)) {
+	const char *unique = completePrefix(&curs, None, cmd);
+	if (unique && !completePrefix(&curs, None, cmd)) {
 		cmd = unique;
 	}
 
@@ -724,9 +722,9 @@ void command(uint id, char *input) {
 	handler->fn(id, input);
 }
 
-void commandCache(void) {
+void commandCompletion(void) {
 	for (size_t i = 0; i < ARRAY_LEN(Commands); ++i) {
 		if (!commandAvailable(&Commands[i])) continue;
-		cacheInsert(false, None, Commands[i].cmd);
+		completePush(None, Commands[i].cmd, Default);
 	}
 }
diff --git a/cache.c b/complete.c
index 970bd9c..d7108e6 100644
--- a/cache.c
+++ b/complete.c
@@ -26,34 +26,32 @@
  */
 
 #include <err.h>
-#include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sysexits.h>
 
 #include "chat.h"
 
 struct Node {
 	uint id;
-	char *key;
-	struct Entry entry;
+	char *str;
+	enum Color color;
+	uint bits;
 	struct Node *prev;
 	struct Node *next;
 };
 
-static const struct Entry DefaultEntry = { .color = Default };
-
 static uint gen;
 static struct Node *head;
 static struct Node *tail;
 
-static struct Node *alloc(uint id, const char *key) {
+static struct Node *alloc(uint id, const char *str, enum Color color) {
 	struct Node *node = calloc(1, sizeof(*node));
-	if (!node) err(EX_OSERR, "calloc");
+	if (!node) err(1, "calloc");
 	node->id = id;
-	node->key = strdup(key);
-	node->entry = DefaultEntry;
-	if (!node->key) err(EX_OSERR, "strdup");
+	node->str = strdup(str);
+	if (!node->str) err(1, "strdup");
+	node->color = color;
+	node->bits = 0;
 	return node;
 }
 
@@ -85,76 +83,68 @@ static struct Node *append(struct Node *node) {
 	return node;
 }
 
-static struct Node *find(uint id, const char *key) {
+static struct Node *find(uint id, const char *str) {
 	for (struct Node *node = head; node; node = node->next) {
-		if (node->id != id) continue;
-		if (strcmp(node->key, key)) continue;
-		return node;
+		if (node->id == id && !strcmp(node->str, str)) return node;
 	}
 	return NULL;
 }
 
-static struct Node *insert(bool touch, uint id, const char *key) {
-	struct Node *node = find(id, key);
-	if (node && touch) {
-		return prepend(detach(node));
-	} else if (node) {
-		return node;
-	} else if (touch) {
-		return prepend(alloc(id, key));
+void completePush(uint id, const char *str, enum Color color) {
+	struct Node *node = find(id, str);
+	if (node) {
+		if (color != Default) node->color = color;
 	} else {
-		return append(alloc(id, key));
+		append(alloc(id, str, color));
 	}
 }
 
-const struct Entry *cacheGet(uint id, const char *key) {
-	struct Node *node = find(id, key);
-	return (node ? &node->entry : &DefaultEntry);
-}
-
-struct Entry *cacheInsert(bool touch, uint id, const char *key) {
-	return &insert(touch, id, key)->entry;
+void completePull(uint id, const char *str, enum Color color) {
+	struct Node *node = find(id, str);
+	if (node) {
+		if (color != Default) node->color = color;
+		prepend(detach(node));
+	} else {
+		prepend(alloc(id, str, color));
+	}
 }
 
-void cacheReplace(bool touch, const char *old, const char *new) {
+void completeReplace(const char *old, const char *new) {
 	struct Node *next = NULL;
 	for (struct Node *node = head; node; node = next) {
 		next = node->next;
-		if (strcmp(node->key, old)) continue;
-		free(node->key);
-		node->key = strdup(new);
-		if (!node->key) err(EX_OSERR, "strdup");
-		if (touch) prepend(detach(node));
+		if (strcmp(node->str, old)) continue;
+		free(node->str);
+		node->str = strdup(new);
+		if (!node->str) err(1, "strdup");
+		prepend(detach(node));
 	}
 }
 
-void cacheRemove(uint id, const char *key) {
-	gen++;
+void completeRemove(uint id, const char *str) {
 	struct Node *next = NULL;
 	for (struct Node *node = head; node; node = next) {
 		next = node->next;
 		if (id && node->id != id) continue;
-		if (strcmp(node->key, key)) continue;
+		if (str && strcmp(node->str, str)) continue;
 		detach(node);
-		free(node->key);
+		free(node->str);
 		free(node);
-		if (id) break;
 	}
+	gen++;
 }
 
-void cacheClear(uint id) {
-	gen++;
-	struct Node *next = NULL;
-	for (struct Node *node = head; node; node = next) {
-		next = node->next;
-		if (node->id != id) continue;
-		detach(node);
-		free(node->key);
-		free(node);
-	}
+enum Color completeColor(uint id, const char *str) {
+	struct Node *node = find(id, str);
+	return (node ? node->color : Default);
+}
+
+uint *completeBits(uint id, const char *str) {
+	struct Node *node = find(id, str);
+	return (node ? &node->bits : NULL);
 }
 
-const char *cacheComplete(struct Cursor *curs, uint id, const char *prefix) {
+const char *completePrefix(struct Cursor *curs, uint id, const char *prefix) {
 	size_t len = strlen(prefix);
 	if (curs->gen != gen) curs->node = NULL;
 	for (
@@ -163,14 +153,12 @@ const char *cacheComplete(struct Cursor *curs, uint id, const char *prefix) {
 		curs->node = curs->node->next
 	) {
 		if (curs->node->id && curs->node->id != id) continue;
-		if (strncasecmp(curs->node->key, prefix, len)) continue;
-		curs->entry = &curs->node->entry;
-		return curs->node->key;
+		if (!strncasecmp(curs->node->str, prefix, len)) return curs->node->str;
 	}
 	return NULL;
 }
 
-const char *cacheSearch(struct Cursor *curs, uint id, const char *substr) {
+const char *completeSubstr(struct Cursor *curs, uint id, const char *substr) {
 	if (curs->gen != gen) curs->node = NULL;
 	for (
 		curs->gen = gen, curs->node = (curs->node ? curs->node->next : head);
@@ -178,28 +166,24 @@ const char *cacheSearch(struct Cursor *curs, uint id, const char *substr) {
 		curs->node = curs->node->next
 	) {
 		if (curs->node->id && curs->node->id != id) continue;
-		if (!strstr(curs->node->key, substr)) continue;
-		curs->entry = &curs->node->entry;
-		return curs->node->key;
+		if (strstr(curs->node->str, substr)) return curs->node->str;
 	}
 	return NULL;
 }
 
-const char *cacheNextKey(struct Cursor *curs, uint id) {
+const char *completeEach(struct Cursor *curs, uint id) {
 	if (curs->gen != gen) curs->node = NULL;
 	for (
 		curs->gen = gen, curs->node = (curs->node ? curs->node->next : head);
 		curs->node;
 		curs->node = curs->node->next
 	) {
-		if (curs->node->id != id) continue;
-		curs->entry = &curs->node->entry;
-		return curs->node->key;
+		if (curs->node->id == id) return curs->node->str;
 	}
 	return NULL;
 }
 
-uint cacheNextID(struct Cursor *curs, const char *key) {
+uint completeEachID(struct Cursor *curs, const char *str) {
 	if (curs->gen != gen) curs->node = NULL;
 	for (
 		curs->gen = gen, curs->node = (curs->node ? curs->node->next : head);
@@ -207,20 +191,18 @@ uint cacheNextID(struct Cursor *curs, const char *key) {
 		curs->node = curs->node->next
 	) {
 		if (!curs->node->id) continue;
-		if (strcmp(curs->node->key, key)) continue;
-		curs->entry = &curs->node->entry;
-		return curs->node->id;
+		if (!strcmp(curs->node->str, str)) return curs->node->id;
 	}
 	return None;
 }
 
-void cacheAccept(struct Cursor *curs) {
+void completeAccept(struct Cursor *curs) {
 	if (curs->gen == gen && curs->node) {
 		prepend(detach(curs->node));
 	}
 	curs->node = NULL;
 }
 
-void cacheReject(struct Cursor *curs) {
+void completeReject(struct Cursor *curs) {
 	curs->node = NULL;
 }
diff --git a/config.c b/config.c
index be88f2f..e568e40 100644
--- a/config.c
+++ b/config.c
@@ -97,13 +97,6 @@ int getopt_config(
 			}
 
 			char *equal = &name[len] + strspn(&name[len], WS);
-			if (*equal && *equal != '=') {
-				warnx(
-					"%s:%zu: option `%s' missing equals sign",
-					path, num, option->name
-				);
-				return clean('?');
-			}
 			if (option->has_arg == no_argument && *equal) {
 				warnx(
 					"%s:%zu: option `%s' doesn't allow an argument",
@@ -121,8 +114,11 @@ int getopt_config(
 
 			optarg = NULL;
 			if (*equal) {
-				char *arg = &equal[1] + strspn(&equal[1], WS);
-				optarg = strdup(arg);
+				if (*equal == '=') {
+					optarg = strdup(&equal[1] + strspn(&equal[1], WS));
+				} else {
+					optarg = strdup(equal);
+				}
 				if (!optarg) {
 					warn("getopt_config");
 					return clean('?');
diff --git a/configure b/configure
index 9465b77..1fd4ad2 100755
--- a/configure
+++ b/configure
@@ -29,6 +29,7 @@ for opt; do
 		(--prefix=*) echo "PREFIX = ${opt#*=}" ;;
 		(--bindir=*) echo "BINDIR = ${opt#*=}" ;;
 		(--mandir=*) echo "MANDIR = ${opt#*=}" ;;
+		(--enable-sandman) echo 'BINS += sandman' ;;
 		(*) echo "warning: unsupported option ${opt}" >&2 ;;
 	esac
 done
@@ -53,6 +54,13 @@ case "$(uname)" in
 		config libtls ncursesw
 		defvar OPENSSL_BIN openssl exec_prefix /bin/openssl
 		;;
+	(NetBSD)
+		cflags "-D'explicit_bzero(b,l)=explicit_memset((b),0,(l))'"
+		config libtls ncurses
+		echo 'LDADD.ncursesw = ${LDADD.ncurses}'
+		echo 'OBJS += compat_readpassphrase.o'
+		defstr OPENSSL_BIN /usr/bin/openssl
+		;;
 	(*)
 		config libtls ncursesw
 		defvar OPENSSL_BIN openssl exec_prefix /bin/openssl
diff --git a/filter.c b/filter.c
index a7f9a29..bbe40c8 100644
--- a/filter.c
+++ b/filter.c
@@ -31,7 +31,6 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sysexits.h>
 
 #include "chat.h"
 
@@ -48,14 +47,14 @@ struct Filter filterParse(enum Heat heat, char *pattern) {
 }
 
 struct Filter filterAdd(enum Heat heat, const char *pattern) {
-	if (len == FilterCap) errx(EX_CONFIG, "filter limit exceeded");
+	if (len == FilterCap) errx(1, "filter limit exceeded");
 	char *own;
 	if (!strchr(pattern, '!') && !strchr(pattern, ' ')) {
 		int n = asprintf(&own, "%s!*@*", pattern);
-		if (n < 0) err(EX_OSERR, "asprintf");
+		if (n < 0) err(1, "asprintf");
 	} else {
 		own = strdup(pattern);
-		if (!own) err(EX_OSERR, "strdup");
+		if (!own) err(1, "strdup");
 	}
 	struct Filter filter = filterParse(heat, own);
 	filters[len++] = filter;
@@ -105,7 +104,7 @@ static void icedPush(const char *msgID) {
 	size_t i = iced.len % IcedCap;
 	free(iced.msgIDs[i]);
 	iced.msgIDs[i] = strdup(msgID);
-	if (!iced.msgIDs[i]) err(EX_OSERR, "strdup");
+	if (!iced.msgIDs[i]) err(1, "strdup");
 	iced.len++;
 }
 
diff --git a/handle.c b/handle.c
index bbb3c99..0cc7c04 100644
--- a/handle.c
+++ b/handle.c
@@ -32,7 +32,6 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sysexits.h>
 #include <wchar.h>
 
 #include "chat.h"
@@ -83,7 +82,7 @@ static void require(struct Message *msg, bool origin, uint len) {
 	}
 	for (uint i = 0; i < len; ++i) {
 		if (msg->params[i]) continue;
-		errx(EX_PROTOCOL, "%s missing parameter %u", msg->cmd, 1 + i);
+		errx(1, "%s missing parameter %u", msg->cmd, 1 + i);
 	}
 }
 
@@ -162,7 +161,7 @@ static void handleErrorNicknameInUse(struct Message *msg) {
 static void handleErrorErroneousNickname(struct Message *msg) {
 	require(msg, false, 3);
 	if (!strcmp(self.nick, "*")) {
-		errx(EX_CONFIG, "%s: %s", msg->params[1], msg->params[2]);
+		errx(1, "%s: %s", msg->params[1], msg->params[2]);
 	} else {
 		handleErrorGeneric(msg);
 	}
@@ -193,7 +192,7 @@ static void handleCap(struct Message *msg) {
 		}
 		if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n");
 	} else if (!strcmp(msg->params[1], "NAK")) {
-		errx(EX_CONFIG, "server does not support %s", msg->params[2]);
+		errx(1, "server does not support %s", msg->params[2]);
 	}
 }
 
@@ -237,7 +236,7 @@ static void handleAuthenticate(struct Message *msg) {
 	size_t userLen = strlen(self.plainUser);
 	size_t passLen = strlen(self.plainPass);
 	size_t len = 1 + userLen + 1 + passLen;
-	if (sizeof(buf) < len) errx(EX_USAGE, "SASL PLAIN is too long");
+	if (sizeof(buf) < len) errx(1, "SASL PLAIN is too long");
 	memcpy(&buf[1], self.plainUser, userLen);
 	memcpy(&buf[1 + userLen + 1], self.plainPass, passLen);
 
@@ -260,13 +259,13 @@ static void handleReplyLoggedIn(struct Message *msg) {
 
 static void handleErrorSASLFail(struct Message *msg) {
 	require(msg, false, 2);
-	errx(EX_CONFIG, "%s", msg->params[1]);
+	errx(1, "%s", msg->params[1]);
 }
 
 static void handleReplyWelcome(struct Message *msg) {
 	require(msg, false, 1);
 	set(&self.nick, msg->params[0]);
-	cacheInsert(true, Network, self.nick);
+	completePull(Network, self.nick, Default);
 	if (self.mode) ircFormat("MODE %s %s\r\n", self.nick, self.mode);
 	if (self.join) {
 		uint count = 1;
@@ -278,7 +277,7 @@ static void handleReplyWelcome(struct Message *msg) {
 		replies[ReplyTopicAuto] += count;
 		replies[ReplyNamesAuto] += count;
 	}
-	commandCache();
+	commandCompletion();
 	handleReplyGeneric(msg);
 }
 
@@ -315,7 +314,7 @@ static void handleReplyISupport(struct Message *msg) {
 			char *modes = strsep(&msg->params[i], ")");
 			char *prefixes = msg->params[i];
 			if (!modes || !prefixes || strlen(modes) != strlen(prefixes)) {
-				errx(EX_PROTOCOL, "invalid PREFIX value");
+				errx(1, "invalid PREFIX value");
 			}
 			set(&network.prefixModes, modes);
 			set(&network.prefixes, prefixes);
@@ -325,7 +324,7 @@ static void handleReplyISupport(struct Message *msg) {
 			char *setParam = strsep(&msg->params[i], ",");
 			char *channel = strsep(&msg->params[i], ",");
 			if (!list || !param || !setParam || !channel) {
-				errx(EX_PROTOCOL, "invalid CHANMODES value");
+				errx(1, "invalid CHANMODES value");
 			}
 			set(&network.listModes, list);
 			set(&network.paramModes, param);
@@ -372,13 +371,13 @@ static void handleJoin(struct Message *msg) {
 			set(&self.host, msg->host);
 		}
 		idColors[id] = hash(msg->params[0]);
-		cacheInsert(true, None, msg->params[0])->color = idColors[id];
+		completePull(None, msg->params[0], idColors[id]);
 		if (replies[ReplyJoin]) {
 			windowShow(windowFor(id));
 			replies[ReplyJoin]--;
 		}
 	}
-	cacheInsert(true, id, msg->nick)->color = hash(msg->user);
+	completePull(id, msg->nick, hash(msg->user));
 	if (msg->params[2] && !strcasecmp(msg->params[2], msg->nick)) {
 		msg->params[2] = NULL;
 	}
@@ -410,9 +409,9 @@ static void handlePart(struct Message *msg) {
 	require(msg, true, 1);
 	uint id = idFor(msg->params[0]);
 	if (!strcmp(msg->nick, self.nick)) {
-		cacheClear(id);
+		completeRemove(id, NULL);
 	}
-	cacheRemove(id, msg->nick);
+	completeRemove(id, msg->nick);
 	enum Heat heat = filterCheck(Cold, id, msg);
 	if (heat > Ice) urlScan(id, msg->nick, msg->params[1]);
 	uiFormat(
@@ -432,14 +431,14 @@ static void handleKick(struct Message *msg) {
 	require(msg, true, 2);
 	uint id = idFor(msg->params[0]);
 	bool kicked = !strcmp(msg->params[1], self.nick);
-	cacheInsert(true, id, msg->nick)->color = hash(msg->user);
+	completePull(id, msg->nick, hash(msg->user));
 	urlScan(id, msg->nick, msg->params[2]);
 	uiFormat(
 		id, (kicked ? Hot : Cold), tagTime(msg),
 		"%s\3%02d%s\17\tkicks \3%02d%s\3 out of \3%02d%s\3%s%s",
 		(kicked ? "\26" : ""),
 		hash(msg->user), msg->nick,
-		cacheGet(id, msg->params[1])->color, msg->params[1],
+		completeColor(id, msg->params[1]), msg->params[1],
 		hash(msg->params[0]), msg->params[0],
 		(msg->params[2] ? ": " : ""), (msg->params[2] ?: "")
 	);
@@ -448,8 +447,8 @@ static void handleKick(struct Message *msg) {
 		msg->nick, msg->params[1], msg->params[0],
 		(msg->params[2] ? ": " : ""), (msg->params[2] ?: "")
 	);
-	cacheRemove(id, msg->params[1]);
-	if (kicked) cacheClear(id);
+	completeRemove(id, msg->params[1]);
+	if (kicked) completeRemove(id, NULL);
 }
 
 static void handleNick(struct Message *msg) {
@@ -459,7 +458,7 @@ static void handleNick(struct Message *msg) {
 		inputUpdate();
 	}
 	struct Cursor curs = {0};
-	for (uint id; (id = cacheNextID(&curs, msg->nick));) {
+	for (uint id; (id = completeEachID(&curs, msg->nick));) {
 		if (!strcmp(idNames[id], msg->nick)) {
 			set(&idNames[id], msg->params[0]);
 		}
@@ -474,13 +473,13 @@ static void handleNick(struct Message *msg) {
 			msg->nick, msg->params[0]
 		);
 	}
-	cacheReplace(true, msg->nick, msg->params[0]);
+	completeReplace(msg->nick, msg->params[0]);
 }
 
 static void handleSetname(struct Message *msg) {
 	require(msg, true, 1);
 	struct Cursor curs = {0};
-	for (uint id; (id = cacheNextID(&curs, msg->nick));) {
+	for (uint id; (id = completeEachID(&curs, msg->nick));) {
 		uiFormat(
 			id, filterCheck(Cold, id, msg), tagTime(msg),
 			"\3%02d%s\3\tis now known as \3%02d%s\3 (%s\17)",
@@ -493,7 +492,7 @@ static void handleSetname(struct Message *msg) {
 static void handleQuit(struct Message *msg) {
 	require(msg, true, 0);
 	struct Cursor curs = {0};
-	for (uint id; (id = cacheNextID(&curs, msg->nick));) {
+	for (uint id; (id = completeEachID(&curs, msg->nick));) {
 		enum Heat heat = filterCheck(Cold, id, msg);
 		if (heat > Ice) urlScan(id, msg->nick, msg->params[0]);
 		uiFormat(
@@ -509,7 +508,7 @@ static void handleQuit(struct Message *msg) {
 			(msg->params[0] ? ": " : ""), (msg->params[0] ?: "")
 		);
 	}
-	cacheRemove(None, msg->nick);
+	completeRemove(None, msg->nick);
 }
 
 static void handleInvite(struct Message *msg) {
@@ -555,7 +554,7 @@ static void handleErrorUserOnChannel(struct Message *msg) {
 	uiFormat(
 		id, Warm, tagTime(msg),
 		"\3%02d%s\3 is already in \3%02d%s\3",
-		cacheGet(id, msg->params[1])->color, msg->params[1],
+		completeColor(id, msg->params[1]), msg->params[1],
 		hash(msg->params[2]), msg->params[2]
 	);
 }
@@ -575,9 +574,8 @@ static void handleReplyNames(struct Message *msg) {
 		for (char *p = prefixes; p < nick; ++p) {
 			bits |= prefixBit(*p);
 		}
-		struct Entry *entry = cacheInsert(false, id, nick);
-		if (user) entry->color = color;
-		entry->prefixBits = bits;
+		completePush(id, nick, color);
+		*completeBits(id, nick) = bits;
 		if (!replies[ReplyNames] && !replies[ReplyNamesAuto]) continue;
 		ptr = seprintf(
 			ptr, end, "%s\3%02d%s\3", (ptr > buf ? ", " : ""), color, prefixes
@@ -609,24 +607,24 @@ static void handleReplyNoTopic(struct Message *msg) {
 	);
 }
 
-static void topicCache(uint id, const char *topic) {
+static void topicComplete(uint id, const char *topic) {
 	char buf[512];
 	struct Cursor curs = {0};
-	const char *prev = cacheComplete(&curs, id, "/topic ");
+	const char *prev = completePrefix(&curs, id, "/topic ");
 	if (prev) {
 		snprintf(buf, sizeof(buf), "%s", prev);
-		cacheRemove(id, buf);
+		completeRemove(id, buf);
 	}
 	if (topic) {
 		snprintf(buf, sizeof(buf), "/topic %s", topic);
-		cacheInsert(false, id, buf);
+		completePush(id, buf, Default);
 	}
 }
 
 static void handleReplyTopic(struct Message *msg) {
 	require(msg, false, 3);
 	uint id = idFor(msg->params[1]);
-	topicCache(id, msg->params[2]);
+	topicComplete(id, msg->params[2]);
 	if (!replies[ReplyTopic] && !replies[ReplyTopicAuto]) return;
 	urlScan(id, NULL, msg->params[2]);
 	uiFormat(
@@ -677,7 +675,7 @@ static void handleTopic(struct Message *msg) {
 	require(msg, true, 2);
 	uint id = idFor(msg->params[0]);
 	if (!msg->params[1][0]) {
-		topicCache(id, NULL);
+		topicComplete(id, NULL);
 		uiFormat(
 			id, Warm, tagTime(msg),
 			"\3%02d%s\3\tremoves the sign in \3%02d%s\3",
@@ -691,7 +689,7 @@ static void handleTopic(struct Message *msg) {
 	}
 
 	struct Cursor curs = {0};
-	const char *prev = cacheComplete(&curs, id, "/topic ");
+	const char *prev = completePrefix(&curs, id, "/topic ");
 	if (prev) {
 		prev += 7;
 	} else {
@@ -741,7 +739,7 @@ log:
 		id, tagTime(msg), "%s places a new sign in %s: %s",
 		msg->nick, msg->params[0], msg->params[1]
 	);
-	topicCache(id, msg->params[1]);
+	topicComplete(id, msg->params[1]);
 	urlScan(id, msg->nick, msg->params[1]);
 }
 
@@ -858,22 +856,23 @@ static void handleMode(struct Message *msg) {
 
 		if (strchr(network.prefixModes, *ch)) {
 			if (i >= ParamCap || !msg->params[i]) {
-				errx(EX_PROTOCOL, "MODE missing %s parameter", mode);
+				errx(1, "MODE missing %s parameter", mode);
 			}
 			char *nick = msg->params[i++];
 			char prefix = network.prefixes[
 				strchr(network.prefixModes, *ch) - network.prefixModes
 			];
+			completePush(id, nick, Default);
 			if (set) {
-				cacheInsert(false, id, nick)->prefixBits |= prefixBit(prefix);
+				*completeBits(id, nick) |= prefixBit(prefix);
 			} else {
-				cacheInsert(false, id, nick)->prefixBits &= ~prefixBit(prefix);
+				*completeBits(id, nick) &= ~prefixBit(prefix);
 			}
 			uiFormat(
 				id, Cold, tagTime(msg),
 				"\3%02d%s\3\t%s \3%02d%c%s\3 %s%s in \3%02d%s\3",
 				hash(msg->user), msg->nick, verb,
-				cacheGet(id, nick)->color, prefix, nick,
+				completeColor(id, nick), prefix, nick,
 				mode, name, hash(msg->params[0]), msg->params[0]
 			);
 			logFormat(
@@ -884,7 +883,7 @@ static void handleMode(struct Message *msg) {
 
 		if (strchr(network.listModes, *ch)) {
 			if (i >= ParamCap || !msg->params[i]) {
-				errx(EX_PROTOCOL, "MODE missing %s parameter", mode);
+				errx(1, "MODE missing %s parameter", mode);
 			}
 			char *mask = msg->params[i++];
 			if (*ch == 'b') {
@@ -917,7 +916,7 @@ static void handleMode(struct Message *msg) {
 
 		if (strchr(network.paramModes, *ch)) {
 			if (i >= ParamCap || !msg->params[i]) {
-				errx(EX_PROTOCOL, "MODE missing %s parameter", mode);
+				errx(1, "MODE missing %s parameter", mode);
 			}
 			char *param = msg->params[i++];
 			uiFormat(
@@ -934,7 +933,7 @@ static void handleMode(struct Message *msg) {
 
 		if (strchr(network.setParamModes, *ch) && set) {
 			if (i >= ParamCap || !msg->params[i]) {
-				errx(EX_PROTOCOL, "MODE missing %s parameter", mode);
+				errx(1, "MODE missing %s parameter", mode);
 			}
 			char *param = msg->params[i++];
 			uiFormat(
@@ -1011,7 +1010,7 @@ static void handleReplyBanList(struct Message *msg) {
 			id, Warm, tagTime(msg),
 			"Banned from \3%02d%s\3 since %s by \3%02d%s\3: %s",
 			hash(msg->params[1]), msg->params[1],
-			since, cacheGet(id, msg->params[3])->color, msg->params[3],
+			since, completeColor(id, msg->params[3]), msg->params[3],
 			msg->params[2]
 		);
 	} else {
@@ -1034,7 +1033,7 @@ static void onList(const char *list, struct Message *msg) {
 			id, Warm, tagTime(msg),
 			"On the \3%02d%s\3 %s list since %s by \3%02d%s\3: %s",
 			hash(msg->params[1]), msg->params[1], list,
-			since, cacheGet(id, msg->params[3])->color, msg->params[3],
+			since, completeColor(id, msg->params[3]), msg->params[3],
 			msg->params[2]
 		);
 	} else {
@@ -1055,19 +1054,19 @@ static void handleReplyInviteList(struct Message *msg) {
 }
 
 static void handleReplyList(struct Message *msg) {
-	require(msg, false, 4);
+	require(msg, false, 3);
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"In \3%02d%s\3 are %ld under the banner: %s",
 		hash(msg->params[1]), msg->params[1],
 		strtol(msg->params[2], NULL, 10),
-		msg->params[3]
+		(msg->params[3] ?: "")
 	);
 }
 
 static void handleReplyWhoisUser(struct Message *msg) {
 	require(msg, false, 6);
-	cacheInsert(true, Network, msg->params[1])->color = hash(msg->params[2]);
+	completePull(Network, msg->params[1], hash(msg->params[2]));
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"\3%02d%s\3\tis %s!%s@%s (%s\17)",
@@ -1082,7 +1081,7 @@ static void handleReplyWhoisServer(struct Message *msg) {
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"\3%02d%s\3\t%s connected to %s (%s)",
-		cacheGet(Network, msg->params[1])->color, msg->params[1],
+		completeColor(Network, msg->params[1]), msg->params[1],
 		(replies[ReplyWhowas] ? "was" : "is"), msg->params[2], msg->params[3]
 	);
 }
@@ -1106,7 +1105,7 @@ static void handleReplyWhoisIdle(struct Message *msg) {
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"\3%02d%s\3\tis idle for %lu %s%s%s%s",
-		cacheGet(Network, msg->params[1])->color, msg->params[1],
+		completeColor(Network, msg->params[1]), msg->params[1],
 		idle, unit, (idle != 1 ? "s" : ""),
 		(msg->params[3] ? ", signed on " : ""), (msg->params[3] ? signon : "")
 	);
@@ -1128,7 +1127,7 @@ static void handleReplyWhoisChannels(struct Message *msg) {
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"\3%02d%s\3\tis in %s",
-		cacheGet(Network, msg->params[1])->color, msg->params[1], buf
+		completeColor(Network, msg->params[1]), msg->params[1], buf
 	);
 }
 
@@ -1142,7 +1141,7 @@ static void handleReplyWhoisGeneric(struct Message *msg) {
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"\3%02d%s\3\t%s%s%s",
-		cacheGet(Network, msg->params[1])->color, msg->params[1],
+		completeColor(Network, msg->params[1]), msg->params[1],
 		msg->params[2], (msg->params[3] ? " " : ""), (msg->params[3] ?: "")
 	);
 }
@@ -1150,13 +1149,13 @@ static void handleReplyWhoisGeneric(struct Message *msg) {
 static void handleReplyEndOfWhois(struct Message *msg) {
 	require(msg, false, 2);
 	if (strcmp(msg->params[1], self.nick)) {
-		cacheRemove(Network, msg->params[1]);
+		completeRemove(Network, msg->params[1]);
 	}
 }
 
 static void handleReplyWhowasUser(struct Message *msg) {
 	require(msg, false, 6);
-	cacheInsert(true, Network, msg->params[1])->color = hash(msg->params[2]);
+	completePull(Network, msg->params[1], hash(msg->params[2]));
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"\3%02d%s\3\twas %s!%s@%s (%s)",
@@ -1168,7 +1167,7 @@ static void handleReplyWhowasUser(struct Message *msg) {
 static void handleReplyEndOfWhowas(struct Message *msg) {
 	require(msg, false, 2);
 	if (strcmp(msg->params[1], self.nick)) {
-		cacheRemove(Network, msg->params[1]);
+		completeRemove(Network, msg->params[1]);
 	}
 }
 
@@ -1179,7 +1178,7 @@ static void handleReplyAway(struct Message *msg) {
 	uiFormat(
 		id, (id == Network ? Warm : Cold), tagTime(msg),
 		"\3%02d%s\3\tis away: %s",
-		cacheGet(id, msg->params[1])->color, msg->params[1], msg->params[2]
+		completeColor(id, msg->params[1]), msg->params[1], msg->params[2]
 	);
 	logFormat(
 		id, tagTime(msg), "%s is away: %s",
@@ -1250,7 +1249,7 @@ static char *colorMentions(char *ptr, char *end, uint id, const char *msg) {
 
 		size_t len = strcspn(msg, ",:<> ");
 		char *p = seprintf(ptr, end, "%.*s", (int)len, msg);
-		enum Color color = cacheGet(id, ptr)->color;
+		enum Color color = completeColor(id, ptr);
 		if (color != Default) {
 			ptr = seprintf(ptr, end, "\3%02d%.*s\3", color, (int)len, msg);
 		} else {
@@ -1290,7 +1289,7 @@ static void handlePrivmsg(struct Message *msg) {
 	heat = filterCheck(heat, id, msg);
 	if (heat > Warm && !mine && !query) highlight = true;
 	if (!notice && !mine && heat > Ice) {
-		cacheInsert(true, id, msg->nick)->color = hash(msg->user);
+		completePull(id, msg->nick, hash(msg->user));
 	}
 	if (heat > Ice) urlScan(id, msg->nick, msg->params[1]);
 
@@ -1337,7 +1336,7 @@ static void handlePing(struct Message *msg) {
 
 static void handleError(struct Message *msg) {
 	require(msg, false, 1);
-	errx(EX_UNAVAILABLE, "%s", msg->params[0]);
+	errx(69, "%s", msg->params[0]);
 }
 
 static const struct Handler {
diff --git a/input.c b/input.c
index bcefee5..7e1f9c1 100644
--- a/input.c
+++ b/input.c
@@ -35,7 +35,6 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sysexits.h>
 #include <termios.h>
 #include <unistd.h>
 #include <wchar.h>
@@ -100,7 +99,7 @@ void inputInit(void) {
 
 	struct termios term;
 	int error = tcgetattr(STDOUT_FILENO, &term);
-	if (error) err(EX_OSERR, "tcgetattr");
+	if (error) err(1, "tcgetattr");
 
 	// Gain use of C-q, C-s, C-c, C-z, C-y, C-v, C-o.
 	term.c_iflag &= ~IXON;
@@ -113,7 +112,7 @@ void inputInit(void) {
 	term.c_cc[VDISCARD] = _POSIX_VDISABLE;
 
 	error = tcsetattr(STDOUT_FILENO, TCSANOW, &term);
-	if (error) err(EX_OSERR, "tcsetattr");
+	if (error) err(1, "tcsetattr");
 
 	def_prog_mode();
 
@@ -172,7 +171,7 @@ void inputUpdate(void) {
 
 	size_t pos = 0;
 	const char *ptr = editString(&edits[id], &buf, &cap, &pos);
-	if (!ptr) err(EX_OSERR, "editString");
+	if (!ptr) err(1, "editString");
 
 	const char *prefix = "";
 	const char *prompt = self.nick;
@@ -261,12 +260,12 @@ static const struct {
 	{ L"\\wave", L"ヾ(^∇^)" },
 };
 
-void inputCache(void) {
+void inputCompletion(void) {
 	char mbs[256];
 	for (size_t i = 0; i < ARRAY_LEN(Macros); ++i) {
 		size_t n = wcstombs(mbs, Macros[i].name, sizeof(mbs));
 		assert(n != (size_t)-1);
-		cacheInsert(false, None, mbs);
+		completePush(None, mbs, Default);
 	}
 }
 
@@ -300,12 +299,12 @@ static struct {
 } tab;
 
 static void tabAccept(void) {
-	cacheAccept(&tab.curs);
+	completeAccept(&tab.curs);
 	tab.len = 0;
 }
 
 static void tabReject(void) {
-	cacheReject(&tab.curs);
+	completeReject(&tab.curs);
 	tab.len = 0;
 }
 
@@ -333,9 +332,9 @@ static int tabComplete(struct Edit *e, uint id) {
 		tab.suffix = true;
 	}
 
-	const char *comp = cacheComplete(&tab.curs, id, tab.pre);
+	const char *comp = completePrefix(&tab.curs, id, tab.pre);
 	if (!comp) {
-		comp = cacheComplete(&tab.curs, id, tab.pre);
+		comp = completePrefix(&tab.curs, id, tab.pre);
 		tab.suffix ^= true;
 	}
 	if (!comp) {
@@ -394,7 +393,7 @@ fail:
 static void inputEnter(void) {
 	uint id = windowID();
 	char *cmd = editString(&edits[id], &buf, &cap, NULL);
-	if (!cmd) err(EX_OSERR, "editString");
+	if (!cmd) err(1, "editString");
 
 	tabAccept();
 	editFn(&edits[id], EditClear);
@@ -450,7 +449,7 @@ static void keyCode(int code) {
 		break; case KEY_SHOME: windowScroll(ScrollAll, +1);
 		break; case KEY_UP: windowScroll(ScrollOne, +1);
 	}
-	if (error) err(EX_OSERR, "editFn");
+	if (error) err(1, "editFn");
 }
 
 static void keyCtrl(wchar_t ch) {
@@ -480,7 +479,7 @@ static void keyCtrl(wchar_t ch) {
 		break; case L'X': error = macroExpand(edit); tabAccept();
 		break; case L'Y': error = editFn(edit, EditPaste);
 	}
-	if (error) err(EX_OSERR, "editFn");
+	if (error) err(1, "editFn");
 }
 
 static void keyStyle(wchar_t ch) {
@@ -516,7 +515,7 @@ static void keyStyle(wchar_t ch) {
 	struct Edit *edit = &edits[windowID()];
 	for (char *ch = buf; *ch; ++ch) {
 		int error = editInsert(edit, *ch);
-		if (error) err(EX_OSERR, "editInsert");
+		if (error) err(1, "editInsert");
 	}
 }
 
@@ -552,7 +551,7 @@ void inputRead(void) {
 			paste ^= true;
 		} else if (paste || literal) {
 			int error = editInsert(&edits[windowID()], ch);
-			if (error) err(EX_OSERR, "editInsert");
+			if (error) err(1, "editInsert");
 		} else if (ret == KEY_CODE_YES) {
 			keyCode(ch);
 		} else if (ch == (L'Z' ^ L'@')) {
@@ -568,7 +567,7 @@ void inputRead(void) {
 			keyCtrl(ch);
 		} else {
 			int error = editInsert(&edits[windowID()], ch);
-			if (error) err(EX_OSERR, "editInsert");
+			if (error) err(1, "editInsert");
 		}
 		style = false;
 		literal = false;
@@ -609,7 +608,7 @@ int inputSave(FILE *file) {
 
 static ssize_t readString(FILE *file, char **buf, size_t *cap) {
 	ssize_t len = getdelim(buf, cap, '\0', file);
-	if (len < 0 && !feof(file)) err(EX_IOERR, "getdelim");
+	if (len < 0 && !feof(file)) err(1, "getdelim");
 	return len;
 }
 
@@ -620,7 +619,7 @@ void inputLoad(FILE *file, size_t version) {
 		readString(file, &buf, &cap);
 		size_t max = strlen(buf);
 		int error = editReserve(&edits[id], 0, max);
-		if (error) err(EX_OSERR, "editReserve");
+		if (error) err(1, "editReserve");
 		size_t len = mbstowcs(edits[id].buf, buf, max);
 		assert(len != (size_t)-1);
 		edits[id].len = len;
diff --git a/irc.c b/irc.c
index 1fc2c3f..28e557b 100644
--- a/irc.c
+++ b/irc.c
@@ -38,7 +38,6 @@
 #include <string.h>
 #include <sys/socket.h>
 #include <sys/stat.h>
-#include <sysexits.h>
 #include <tls.h>
 #include <unistd.h>
 
@@ -54,7 +53,7 @@ void ircConfig(
 	char buf[PATH_MAX];
 
 	config = tls_config_new();
-	if (!config) errx(EX_SOFTWARE, "tls_config_new");
+	if (!config) errx(1, "tls_config_new");
 
 	if (insecure) {
 		tls_config_insecure_noverifycert(config);
@@ -66,7 +65,7 @@ void ircConfig(
 			error = tls_config_set_ca_file(config, buf);
 			if (!error) break;
 		}
-		if (error) errx(EX_NOINPUT, "%s: %s", trust, tls_config_error(config));
+		if (error) errx(1, "%s: %s", trust, tls_config_error(config));
 	}
 
 	// Explicitly load the default CA cert file on OpenBSD now so it doesn't
@@ -76,7 +75,7 @@ void ircConfig(
 	if (!insecure && !trust) {
 		const char *ca = tls_default_ca_cert_file();
 		error = tls_config_set_ca_file(config, ca);
-		if (error) errx(EX_OSFILE, "%s: %s", ca, tls_config_error(config));
+		if (error) errx(1, "%s: %s", ca, tls_config_error(config));
 	}
 #endif
 
@@ -89,21 +88,21 @@ void ircConfig(
 			}
 			if (!error) break;
 		}
-		if (error) errx(EX_NOINPUT, "%s: %s", cert, tls_config_error(config));
+		if (error) errx(1, "%s: %s", cert, tls_config_error(config));
 	}
 	if (priv) {
 		for (int i = 0; configPath(buf, sizeof(buf), priv, i); ++i) {
 			error = tls_config_set_key_file(config, buf);
 			if (!error) break;
 		}
-		if (error) errx(EX_NOINPUT, "%s: %s", priv, tls_config_error(config));
+		if (error) errx(1, "%s: %s", priv, tls_config_error(config));
 	}
 
 	client = tls_client();
-	if (!client) errx(EX_SOFTWARE, "tls_client");
+	if (!client) errx(1, "tls_client");
 
 	error = tls_configure(client, config);
-	if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client));
+	if (error) errx(1, "tls_configure: %s", tls_error(client));
 }
 
 int ircConnect(const char *bindHost, const char *host, const char *port) {
@@ -120,11 +119,11 @@ int ircConnect(const char *bindHost, const char *host, const char *port) {
 
 	if (bindHost) {
 		error = getaddrinfo(bindHost, NULL, &hints, &head);
-		if (error) errx(EX_NOHOST, "%s: %s", bindHost, gai_strerror(error));
+		if (error) errx(1, "%s: %s", bindHost, gai_strerror(error));
 
 		for (struct addrinfo *ai = head; ai; ai = ai->ai_next) {
 			sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
-			if (sock < 0) err(EX_OSERR, "socket");
+			if (sock < 0) err(1, "socket");
 
 			error = bind(sock, ai->ai_addr, ai->ai_addrlen);
 			if (!error) {
@@ -135,17 +134,17 @@ int ircConnect(const char *bindHost, const char *host, const char *port) {
 			close(sock);
 			sock = -1;
 		}
-		if (sock < 0) err(EX_UNAVAILABLE, "%s", bindHost);
+		if (sock < 0) err(1, "%s", bindHost);
 		freeaddrinfo(head);
 	}
 
 	error = getaddrinfo(host, port, &hints, &head);
-	if (error) errx(EX_NOHOST, "%s:%s: %s", host, port, gai_strerror(error));
+	if (error) errx(1, "%s:%s: %s", host, port, gai_strerror(error));
 
 	for (struct addrinfo *ai = head; ai; ai = ai->ai_next) {
 		if (sock < 0) {
 			sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
-			if (sock < 0) err(EX_OSERR, "socket");
+			if (sock < 0) err(1, "socket");
 		}
 
 		error = connect(sock, ai->ai_addr, ai->ai_addrlen);
@@ -155,12 +154,12 @@ int ircConnect(const char *bindHost, const char *host, const char *port) {
 		close(sock);
 		sock = -1;
 	}
-	if (sock < 0) err(EX_UNAVAILABLE, "%s:%s", host, port);
+	if (sock < 0) err(69, "%s:%s", host, port);
 	freeaddrinfo(head);
 
 	fcntl(sock, F_SETFD, FD_CLOEXEC);
 	error = tls_connect_socket(client, sock, host);
-	if (error) errx(EX_PROTOCOL, "tls_connect: %s", tls_error(client));
+	if (error) errx(1, "tls_connect: %s", tls_error(client));
 
 	return sock;
 }
@@ -170,7 +169,7 @@ void ircHandshake(void) {
 	do {
 		error = tls_handshake(client);
 	} while (error == TLS_WANT_POLLIN || error == TLS_WANT_POLLOUT);
-	if (error) errx(EX_PROTOCOL, "tls_handshake: %s", tls_error(client));
+	if (error) errx(1, "tls_handshake: %s", tls_error(client));
 
 	tls_config_clear_keys(config);
 }
@@ -202,7 +201,7 @@ void ircSend(const char *ptr, size_t len) {
 	while (len) {
 		ssize_t ret = tls_write(client, ptr, len);
 		if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue;
-		if (ret < 0) errx(EX_IOERR, "tls_write: %s", tls_error(client));
+		if (ret < 0) errx(1, "tls_write: %s", tls_error(client));
 		ptr += ret;
 		len -= ret;
 	}
@@ -287,8 +286,8 @@ void ircRecv(void) {
 	assert(client);
 	ssize_t ret = tls_read(client, &buf[len], sizeof(buf) - len);
 	if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) return;
-	if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client));
-	if (!ret) errx(EX_PROTOCOL, "server closed connection");
+	if (ret < 0) errx(1, "tls_read: %s", tls_error(client));
+	if (!ret) errx(69, "server closed connection");
 	len += ret;
 
 	char *crlf;
diff --git a/log.c b/log.c
index d6b3f2a..181c009 100644
--- a/log.c
+++ b/log.c
@@ -34,7 +34,6 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <sys/stat.h>
-#include <sysexits.h>
 #include <time.h>
 #include <unistd.h>
 
@@ -49,13 +48,13 @@ static int logDir = -1;
 void logOpen(void) {
 	char buf[PATH_MAX];
 	int error = mkdir(dataPath(buf, sizeof(buf), "", 0), S_IRWXU);
-	if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", buf);
+	if (error && errno != EEXIST) err(1, "%s", buf);
 
 	error = mkdir(dataPath(buf, sizeof(buf), "log", 0), S_IRWXU);
-	if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", buf);
+	if (error && errno != EEXIST) err(1, "%s", buf);
 
 	logDir = open(buf, O_RDONLY | O_CLOEXEC);
-	if (logDir < 0) err(EX_CANTCREAT, "%s", buf);
+	if (logDir < 0) err(1, "%s", buf);
 
 #ifdef __FreeBSD__
 	cap_rights_t rights;
@@ -64,13 +63,13 @@ void logOpen(void) {
 		/* for fdopen(3) */ CAP_FCNTL, CAP_FSTAT
 	);
 	error = caph_rights_limit(logDir, &rights);
-	if (error) err(EX_OSERR, "cap_rights_limit");
+	if (error) err(1, "cap_rights_limit");
 #endif
 }
 
 static void logMkdir(const char *path) {
 	int error = mkdirat(logDir, path, S_IRWXU);
-	if (error && errno != EEXIST) err(EX_CANTCREAT, "log/%s", path);
+	if (error && errno != EEXIST) err(1, "log/%s", path);
 }
 
 static void sanitize(char *ptr, char *end) {
@@ -99,7 +98,7 @@ static FILE *logFile(uint id, const struct tm *tm) {
 
 	if (logs[id].file) {
 		int error = fclose(logs[id].file);
-		if (error) err(EX_IOERR, "%s", idNames[id]);
+		if (error) err(1, "%s", idNames[id]);
 	}
 
 	logs[id].year = tm->tm_year;
@@ -119,16 +118,16 @@ static FILE *logFile(uint id, const struct tm *tm) {
 	logMkdir(path);
 
 	size_t len = strftime(ptr, end - ptr, "/%F.log", tm);
-	if (!len) errx(EX_CANTCREAT, "log path too long");
+	if (!len) errx(1, "log path too long");
 
 	int fd = openat(
 		logDir, path,
 		O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC,
 		S_IRUSR | S_IWUSR
 	);
-	if (fd < 0) err(EX_CANTCREAT, "log/%s", path);
+	if (fd < 0) err(1, "log/%s", path);
 	logs[id].file = fdopen(fd, "a");
-	if (!logs[id].file) err(EX_OSERR, "fdopen");
+	if (!logs[id].file) err(1, "fdopen");
 
 	setlinebuf(logs[id].file);
 	return logs[id].file;
@@ -139,7 +138,7 @@ void logClose(void) {
 	for (uint id = 0; id < IDCap; ++id) {
 		if (!logs[id].file) continue;
 		int error = fclose(logs[id].file);
-		if (error) err(EX_IOERR, "%s", idNames[id]);
+		if (error) err(1, "%s", idNames[id]);
 	}
 	close(logDir);
 }
@@ -149,21 +148,21 @@ void logFormat(uint id, const time_t *src, const char *format, ...) {
 
 	time_t ts = (src ? *src : time(NULL));
 	struct tm *tm = localtime(&ts);
-	if (!tm) err(EX_OSERR, "localtime");
+	if (!tm) err(1, "localtime");
 
 	FILE *file = logFile(id, tm);
 
 	char buf[sizeof("0000-00-00T00:00:00+0000")];
 	strftime(buf, sizeof(buf), "%FT%T%z", tm);
 	int n = fprintf(file, "[%s] ", buf);
-	if (n < 0) err(EX_IOERR, "%s", idNames[id]);
+	if (n < 0) err(1, "%s", idNames[id]);
 
 	va_list ap;
 	va_start(ap, format);
 	n = vfprintf(file, format, ap);
 	va_end(ap);
-	if (n < 0) err(EX_IOERR, "%s", idNames[id]);
+	if (n < 0) err(1, "%s", idNames[id]);
 
 	n = fprintf(file, "\n");
-	if (n < 0) err(EX_IOERR, "%s", idNames[id]);
+	if (n < 0) err(1, "%s", idNames[id]);
 }
diff --git a/scripts/sandman.1 b/sandman.1
index 92828c0..92828c0 100644
--- a/scripts/sandman.1
+++ b/sandman.1
diff --git a/scripts/sandman.m b/sandman.m
index 2e5c4db..c9d0705 100644
--- a/scripts/sandman.m
+++ b/sandman.m
@@ -19,7 +19,6 @@
 #import <signal.h>
 #import <stdio.h>
 #import <stdlib.h>
-#import <sysexits.h>
 #import <unistd.h>
 
 typedef unsigned uint;
@@ -27,17 +26,17 @@ typedef unsigned uint;
 static pid_t pid;
 static void spawn(char *argv[]) {
 	pid = fork();
-	if (pid < 0) err(EX_OSERR, "fork");
+	if (pid < 0) err(1, "fork");
 	if (pid) return;
 	execvp(argv[0], argv);
-	err(EX_CONFIG, "%s", argv[0]);
+	err(127, "%s", argv[0]);
 }
 
 static void handler(int signal) {
 	(void)signal;
 	int status;
 	pid_t pid = wait(&status);
-	if (pid < 0) _exit(EX_OSERR);
+	if (pid < 0) _exit(1);
 	_exit(status);
 }
 
@@ -47,12 +46,12 @@ int main(int argc, char *argv[]) {
 	for (int opt; 0 < (opt = getopt(argc, argv, "t:"));) {
 		switch (opt) {
 			break; case 't': delay = strtoul(optarg, NULL, 10);
-			break; default:  return EX_USAGE;
+			break; default:  return 1;
 		}
 	}
 	argc -= optind;
 	argv += optind;
-	if (!argc) errx(EX_USAGE, "command required");
+	if (!argc) errx(1, "command required");
 
 	NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
 	NSNotificationCenter *notifCenter = [workspace notificationCenter];
@@ -64,7 +63,7 @@ int main(int argc, char *argv[]) {
 							 (void)notif;
 							 signal(SIGCHLD, SIG_IGN);
 							 int error = kill(pid, SIGHUP);
-							 if (error) err(EX_UNAVAILABLE, "kill");
+							 if (error) err(1, "kill");
 							 int status;
 							 wait(&status);
 						 }];
diff --git a/scripts/.gitignore b/scripts/.gitignore
deleted file mode 100644
index f6dc107..0000000
--- a/scripts/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-sandman
diff --git a/scripts/Makefile b/scripts/Makefile
deleted file mode 100644
index 179a2d3..0000000
--- a/scripts/Makefile
+++ /dev/null
@@ -1,22 +0,0 @@
-PREFIX ?= /usr/local
-BINDIR ?= ${PREFIX}/bin
-MANDIR ?= ${PREFIX}/man
-
-CFLAGS += -Wall -Wextra
-
--include ../config.mk
-
-LDLIBS = -framework Cocoa
-
-all: sandman
-
-clean:
-	rm -f sandman
-
-install: sandman sandman.1
-	install -d ${DESTDIR}${BINDIR} ${DESTDIR}${MANDIR}/man1
-	install sandman ${DESTDIR}${BINDIR}
-	install -m 644 sandman.1 ${DESTDIR}${MANDIR}/man1
-
-uninstall:
-	rm -f ${DESTDIR}${BINDIR}/sandman ${DESTDIR}/man/man1/sandman.1
diff --git a/scripts/build-chroot.sh b/scripts/build-chroot.sh
deleted file mode 100644
index a0fcf32..0000000
--- a/scripts/build-chroot.sh
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/bin/sh
-set -eux
-
-CHROOT_USER=$1
-CHROOT_GROUP=$2
-
-if [ "$(uname)" = 'OpenBSD' ]; then
-	install -d -o root -g wheel \
-		root \
-		root/bin \
-		root/etc/ssl \
-		root/home \
-		root/usr/bin \
-		root/usr/lib \
-		root/usr/libexec \
-		root/usr/share/man
-	install -d -o ${CHROOT_USER} -g ${CHROOT_GROUP} \
-		root/home/${CHROOT_USER} \
-		root/home/${CHROOT_USER}/.local/share
-
-	cp -fp /bin/sh root/bin
-	cp -fp /usr/libexec/ld.so root/usr/libexec
-	export LD_TRACE_LOADED_OBJECTS_FMT1='%p\n'
-	export LD_TRACE_LOADED_OBJECTS_FMT2=''
-	for bin in ./catgirl /usr/bin/mandoc /usr/bin/less; do
-		LD_TRACE_LOADED_OBJECTS=1 $bin | xargs -t -J % cp -fp % root/usr/lib
-	done
-	cp -fp /usr/bin/printf /usr/bin/mandoc /usr/bin/less root/usr/bin
-	make install DESTDIR=root PREFIX=/usr MANDIR=/usr/share/man
-	install scripts/chroot-prompt.sh root/usr/bin/catgirl-prompt
-	install scripts/chroot-man.sh root/usr/bin/man
-
-	cp -fp /etc/hosts /etc/resolv.conf root/etc
-	cp -fp /etc/ssl/cert.pem root/etc/ssl
-	cp -af /usr/share/locale /usr/share/terminfo root/usr/share
-
-	tar -c -f chroot.tar -C root bin etc home usr
-
-elif [ "$(uname)" = 'FreeBSD' ]; then
-	install -d -o root -g wheel \
-		root \
-		root/bin \
-		root/etc \
-		root/home \
-		root/lib \
-		root/libexec \
-		root/usr/bin \
-		root/usr/local/etc/ssl \
-		root/usr/share/man \
-		root/usr/share/misc
-	install -d -o ${CHROOT_USER} -g ${CHROOT_GROUP} \
-		root/home/${CHROOT_USER} \
-		root/home/${CHROOT_USER}/.local/share
-
-	cp -fp /libexec/ld-elf.so.1 root/libexec
-	ldd -f '%p\n' catgirl /usr/bin/mandoc /usr/bin/less \
-		| sort -u | xargs -t -J % cp -fp % root/lib
-	chflags noschg root/libexec/* root/lib/*
-	cp -fp /rescue/sh /usr/bin/mandoc /usr/bin/less root/bin
-	make install DESTDIR=root PREFIX=/usr MANDIR=/usr/share/man
-	install scripts/chroot-prompt.sh root/usr/bin/catgirl-prompt
-	install scripts/chroot-man.sh root/usr/bin/man
-
-	cp -fp /etc/hosts /etc/resolv.conf root/etc
-	cp -fp /usr/local/etc/ssl/cert.pem root/usr/local/etc/ssl
-	cp -af /usr/share/locale root/usr/share
-	cp -fp /usr/share/misc/termcap.db root/usr/share/misc
-
-	tar -c -f chroot.tar -C root bin etc home lib libexec usr
-
-else
-	echo "Don't know how to build chroot on $(uname)" >&2
-	exit 1
-fi
diff --git a/scripts/chroot-man.sh b/scripts/chroot-man.sh
deleted file mode 100644
index 9d686f9..0000000
--- a/scripts/chroot-man.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-exec mandoc /usr/share/man/man1/catgirl.1 | LESSSECURE=1 less
diff --git a/scripts/chroot-prompt.sh b/scripts/chroot-prompt.sh
deleted file mode 100644
index 2b34426..0000000
--- a/scripts/chroot-prompt.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/sh
-set -eu
-
-printf 'Name: '
-read -r nick rest
-printf '%s %s\n' "$nick" "$SSH_CLIENT" >>nicks.log
-exec catgirl -K -n "$nick" -s "${nick##*/}" -u "${SSH_CLIENT%% *}" "$@"
diff --git a/scripts/reconnect.sh b/scripts/reconnect.sh
new file mode 100644
index 0000000..92d9668
--- /dev/null
+++ b/scripts/reconnect.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+set -u
+
+while :; do
+	catgirl "$@"
+	status=$?
+	if [ $status -ne 69 ]; then
+		exit $status
+	fi
+done
diff --git a/scripts/sshd_config b/scripts/sshd_config
deleted file mode 100644
index c7e99ec..0000000
--- a/scripts/sshd_config
+++ /dev/null
@@ -1,9 +0,0 @@
-UsePAM no
-
-Match User chat
-	PasswordAuthentication yes
-	PermitEmptyPasswords yes
-	ChrootDirectory /home/chat
-	ForceCommand catgirl-prompt
-	DisableForwarding yes
-	MaxSessions 1
diff --git a/ui.c b/ui.c
index 7728402..df675b1 100644
--- a/ui.c
+++ b/ui.c
@@ -39,7 +39,6 @@
 #include <stdlib.h>
 #include <string.h>
 #include <sys/file.h>
-#include <sysexits.h>
 #include <term.h>
 #include <time.h>
 #include <unistd.h>
@@ -122,13 +121,13 @@ void uiInit(void) {
 	}
 
 	uiStatus = newwin(StatusLines, COLS, 0, 0);
-	if (!uiStatus) err(EX_OSERR, "newwin");
+	if (!uiStatus) err(1, "newwin");
 
 	uiMain = newwin(MAIN_LINES, COLS, StatusLines, 0);
-	if (!uiMain) err(EX_OSERR, "newwin");
+	if (!uiMain) err(1, "newwin");
 
 	uiInput = newpad(InputLines, InputCols);
-	if (!uiInput) err(EX_OSERR, "newpad");
+	if (!uiInput) err(1, "newpad");
 
 	windowInit();
 	uiShow();
@@ -240,7 +239,7 @@ static void notify(uint id, const char *str) {
 	utilPush(&util, buf);
 
 	pid_t pid = fork();
-	if (pid < 0) err(EX_OSERR, "fork");
+	if (pid < 0) err(1, "fork");
 	if (pid) return;
 
 	setsid();
@@ -249,7 +248,7 @@ static void notify(uint id, const char *str) {
 	dup2(utilPipe[1], STDERR_FILENO);
 	execvp(util.argv[0], (char *const *)util.argv);
 	warn("%s", util.argv[0]);
-	_exit(EX_CONFIG);
+	_exit(127);
 }
 
 void uiWrite(uint id, enum Heat heat, const time_t *src, const char *str) {
@@ -296,7 +295,7 @@ static size_t signatureVersion(uint64_t signature) {
 	for (size_t i = 0; i < ARRAY_LEN(Signatures); ++i) {
 		if (signature == Signatures[i]) return i;
 	}
-	errx(EX_DATAERR, "unknown file signature %" PRIX64, signature);
+	errx(1, "unknown save file signature %" PRIX64, signature);
 }
 
 static int writeUint64(FILE *file, uint64_t u) {
@@ -317,32 +316,32 @@ int uiSave(void) {
 static uint64_t readUint64(FILE *file) {
 	uint64_t u;
 	fread(&u, sizeof(u), 1, file);
-	if (ferror(file)) err(EX_IOERR, "fread");
-	if (feof(file)) errx(EX_DATAERR, "unexpected eof");
+	if (ferror(file)) err(1, "fread");
+	if (feof(file)) errx(1, "unexpected end of save file");
 	return u;
 }
 
 void uiLoad(const char *name) {
 	int error;
 	saveFile = dataOpen(name, "a+e");
-	if (!saveFile) exit(EX_CANTCREAT);
+	if (!saveFile) exit(1);
 	rewind(saveFile);
 
 #ifdef __FreeBSD__
 	cap_rights_t rights;
 	cap_rights_init(&rights, CAP_READ, CAP_WRITE, CAP_FLOCK, CAP_FTRUNCATE);
 	error = caph_rights_limit(fileno(saveFile), &rights);
-	if (error) err(EX_OSERR, "cap_rights_limit");
+	if (error) err(1, "cap_rights_limit");
 #endif
 
 	error = flock(fileno(saveFile), LOCK_EX | LOCK_NB);
 	if (error && errno == EWOULDBLOCK) {
-		errx(EX_CANTCREAT, "%s: save file in use", name);
+		errx(1, "%s: save file in use", name);
 	}
 
 	time_t signature;
 	fread(&signature, sizeof(signature), 1, saveFile);
-	if (ferror(saveFile)) err(EX_IOERR, "fread");
+	if (ferror(saveFile)) err(1, "fread");
 	if (feof(saveFile)) {
 		return;
 	}
diff --git a/url.c b/url.c
index 7da0968..349dc00 100644
--- a/url.c
+++ b/url.c
@@ -32,7 +32,6 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sysexits.h>
 #include <unistd.h>
 
 #include "chat.h"
@@ -67,7 +66,7 @@ static void compile(void) {
 	if (!error) return;
 	char buf[256];
 	regerror(error, &Regex, buf, sizeof(buf));
-	errx(EX_SOFTWARE, "regcomp: %s: %s", buf, Pattern);
+	errx(1, "regcomp: %s: %s", buf, Pattern);
 }
 
 struct URL {
@@ -92,10 +91,10 @@ static void push(uint id, const char *nick, const char *str, size_t len) {
 	url->nick = NULL;
 	if (nick) {
 		url->nick = strdup(nick);
-		if (!url->nick) err(EX_OSERR, "strdup");
+		if (!url->nick) err(1, "strdup");
 	}
 	url->url = malloc(len + 1);
-	if (!url->url) err(EX_OSERR, "malloc");
+	if (!url->url) err(1, "malloc");
 
 	char buf[1024];
 	snprintf(buf, sizeof(buf), "%.*s", (int)len, str);
@@ -120,7 +119,7 @@ static const struct Util OpenUtils[] = {
 
 static void urlOpen(const char *url) {
 	pid_t pid = fork();
-	if (pid < 0) err(EX_OSERR, "fork");
+	if (pid < 0) err(1, "fork");
 	if (pid) return;
 
 	setsid();
@@ -132,7 +131,7 @@ static void urlOpen(const char *url) {
 		utilPush(&util, url);
 		execvp(util.argv[0], (char *const *)util.argv);
 		warn("%s", util.argv[0]);
-		_exit(EX_CONFIG);
+		_exit(127);
 	}
 	for (size_t i = 0; i < ARRAY_LEN(OpenUtils); ++i) {
 		struct Util util = OpenUtils[i];
@@ -140,11 +139,11 @@ static void urlOpen(const char *url) {
 		execvp(util.argv[0], (char *const *)util.argv);
 		if (errno != ENOENT) {
 			warn("%s", util.argv[0]);
-			_exit(EX_CONFIG);
+			_exit(127);
 		}
 	}
 	warnx("no open utility found");
-	_exit(EX_CONFIG);
+	_exit(127);
 }
 
 struct Util urlCopyUtil;
@@ -158,18 +157,18 @@ static const struct Util CopyUtils[] = {
 static void urlCopy(const char *url) {
 	int rw[2];
 	int error = pipe(rw);
-	if (error) err(EX_OSERR, "pipe");
+	if (error) err(1, "pipe");
 
 	size_t len = strlen(url);
 	if (len > PIPE_BUF) len = PIPE_BUF;
 	ssize_t n = write(rw[1], url, len);
-	if (n < 0) err(EX_IOERR, "write");
+	if (n < 0) err(1, "write");
 
 	error = close(rw[1]);
-	if (error) err(EX_IOERR, "close");
+	if (error) err(1, "close");
 
 	pid_t pid = fork();
-	if (pid < 0) err(EX_OSERR, "fork");
+	if (pid < 0) err(1, "fork");
 	if (pid) {
 		close(rw[0]);
 		return;
@@ -183,17 +182,17 @@ static void urlCopy(const char *url) {
 	if (urlCopyUtil.argc) {
 		execvp(urlCopyUtil.argv[0], (char *const *)urlCopyUtil.argv);
 		warn("%s", urlCopyUtil.argv[0]);
-		_exit(EX_CONFIG);
+		_exit(127);
 	}
 	for (size_t i = 0; i < ARRAY_LEN(CopyUtils); ++i) {
 		execvp(CopyUtils[i].argv[0], (char *const *)CopyUtils[i].argv);
 		if (errno != ENOENT) {
 			warn("%s", CopyUtils[i].argv[0]);
-			_exit(EX_CONFIG);
+			_exit(127);
 		}
 	}
 	warnx("no copy utility found");
-	_exit(EX_CONFIG);
+	_exit(127);
 }
 
 void urlOpenCount(uint id, uint count) {
@@ -239,7 +238,7 @@ static int writeString(FILE *file, const char *str) {
 }
 static ssize_t readString(FILE *file, char **buf, size_t *cap) {
 	ssize_t len = getdelim(buf, cap, '\0', file);
-	if (len < 0 && !feof(file)) err(EX_IOERR, "getdelim");
+	if (len < 0 && !feof(file)) err(1, "getdelim");
 	return len;
 }
 
@@ -269,11 +268,11 @@ void urlLoad(FILE *file, size_t version) {
 		readString(file, &buf, &cap);
 		if (buf[0]) {
 			url->nick = strdup(buf);
-			if (!url->nick) err(EX_OSERR, "strdup");
+			if (!url->nick) err(1, "strdup");
 		}
 		readString(file, &buf, &cap);
 		url->url = strdup(buf);
-		if (!url->url) err(EX_OSERR, "strdup");
+		if (!url->url) err(1, "strdup");
 	}
 	free(buf);
 }
diff --git a/window.c b/window.c
index 0c675e9..2e79a65 100644
--- a/window.c
+++ b/window.c
@@ -35,7 +35,6 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sysexits.h>
 #include <time.h>
 
 #include "chat.h"
@@ -93,7 +92,7 @@ static struct Window *windowRemove(uint num) {
 }
 
 static void windowFree(struct Window *window) {
-	cacheRemove(None, idNames[window->id]);
+	completeRemove(None, idNames[window->id]);
 	bufferFree(window->buffer);
 	free(window);
 }
@@ -107,7 +106,7 @@ uint windowFor(uint id) {
 	}
 
 	struct Window *window = calloc(1, sizeof(*window));
-	if (!window) err(EX_OSERR, "malloc");
+	if (!window) err(1, "malloc");
 
 	window->id = id;
 	window->mark = true;
@@ -118,7 +117,7 @@ uint windowFor(uint id) {
 		window->thresh = windowThreshold;
 	}
 	window->buffer = bufferAlloc();
-	cacheInsert(false, None, idNames[id])->color = idColors[id];
+	completePush(None, idNames[id], idColors[id]);
 
 	return windowPush(window);
 }
@@ -132,7 +131,7 @@ void windowInit(void) {
 
 	struct tm *time = localtime(&(time_t) { -22100400 });
 	size_t len = strftime(buf, sizeof(buf), fmt, time);
-	if (!len) errx(EX_CONFIG, "invalid timestamp format: %s", fmt);
+	if (!len) errx(1, "invalid timestamp format: %s", fmt);
 
 	int y;
 	waddstr(uiMain, buf);
@@ -147,6 +146,7 @@ static int styleAdd(WINDOW *win, struct Style init, const char *str) {
 	struct Style style = init;
 	while (*str) {
 		size_t len = styleParse(&style, &str);
+		if (!len) continue;
 		wattr_set(win, uiAttr(style), uiPair(style), NULL);
 		if (waddnstr(win, str, len) == ERR)
 			return -1;
@@ -477,7 +477,7 @@ void windowClose(uint num) {
 	if (num >= count) return;
 	if (windows[num]->id == Network) return;
 	struct Window *window = windowRemove(num);
-	cacheClear(window->id);
+	completeRemove(window->id, NULL);
 	windowFree(window);
 	if (swap >= num) swap--;
 	if (show == num) {
@@ -621,14 +621,14 @@ int windowSave(FILE *file) {
 static time_t readTime(FILE *file) {
 	time_t time;
 	fread(&time, sizeof(time), 1, file);
-	if (ferror(file)) err(EX_IOERR, "fread");
-	if (feof(file)) errx(EX_DATAERR, "unexpected eof");
+	if (ferror(file)) err(1, "fread");
+	if (feof(file)) errx(1, "unexpected end of save file");
 	return time;
 }
 
 static ssize_t readString(FILE *file, char **buf, size_t *cap) {
 	ssize_t len = getdelim(buf, cap, '\0', file);
-	if (len < 0 && !feof(file)) err(EX_IOERR, "getdelim");
+	if (len < 0 && !feof(file)) err(1, "getdelim");
 	return len;
 }
 
diff --git a/xdg.c b/xdg.c
index 75ee871..6f61cf9 100644
--- a/xdg.c
+++ b/xdg.c
@@ -32,7 +32,6 @@
 #include <stdlib.h>
 #include <string.h>
 #include <sys/stat.h>
-#include <sysexits.h>
 
 #include "chat.h"
 
@@ -90,7 +89,7 @@ static char *basePath(
 	} else if (home) {
 		snprintf(buf, cap, "%s/%s/" SUBDIR "/%s", home, base.defHome, path);
 	} else {
-		errx(EX_USAGE, "HOME unset");
+		errx(1, "HOME unset");
 	}
 	return buf;
 }