about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--Darwin.mk4
-rw-r--r--LICENSE661
-rw-r--r--Linux.mk6
-rw-r--r--Makefile99
-rw-r--r--NetBSD.mk3
-rw-r--r--README.7111
-rw-r--r--catgirl.1413
-rw-r--r--chat.c91
-rw-r--r--chat.h221
-rw-r--r--color.c51
-rw-r--r--edit.c186
-rw-r--r--event.c168
-rw-r--r--format.c162
-rw-r--r--handle.c568
-rw-r--r--input.c276
-rw-r--r--irc.c147
-rw-r--r--log.c157
-rw-r--r--man.sh2
-rw-r--r--pls.c186
-rw-r--r--sandman.130
-rw-r--r--sandman.m88
-rw-r--r--sshd_config13
-rw-r--r--tab.c148
-rw-r--r--tag.c53
-rw-r--r--term.c100
-rw-r--r--ui.c615
-rw-r--r--url.c111
28 files changed, 0 insertions, 4678 deletions
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index fb17842..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-*.o
-*.t
-catgirl
-chroot.tar
-config.mk
-root
-sandman
-tags
diff --git a/Darwin.mk b/Darwin.mk
deleted file mode 100644
index d1f26cc..0000000
--- a/Darwin.mk
+++ /dev/null
@@ -1,4 +0,0 @@
-LIBRESSL_PREFIX = /usr/local/opt/libressl
-LDLIBS = -lcurses -ltls -framework Cocoa
-BINS += sandman
-MANS += sandman.1
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index dba13ed..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,661 +0,0 @@
-                    GNU AFFERO GENERAL PUBLIC LICENSE
-                       Version 3, 19 November 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The GNU Affero General Public License is a free, copyleft license for
-software and other kinds of works, specifically designed to ensure
-cooperation with the community in the case of network server software.
-
-  The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works.  By contrast,
-our General Public Licenses are intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
-  Developers that use our General Public Licenses protect your rights
-with two steps: (1) assert copyright on the software, and (2) offer
-you this License which gives you legal permission to copy, distribute
-and/or modify the software.
-
-  A secondary benefit of defending all users' freedom is that
-improvements made in alternate versions of the program, if they
-receive widespread use, become available for other developers to
-incorporate.  Many developers of free software are heartened and
-encouraged by the resulting cooperation.  However, in the case of
-software used on network servers, this result may fail to come about.
-The GNU General Public License permits making a modified version and
-letting the public access it on a server without ever releasing its
-source code to the public.
-
-  The GNU Affero General Public License is designed specifically to
-ensure that, in such cases, the modified source code becomes available
-to the community.  It requires the operator of a network server to
-provide the source code of the modified version running there to the
-users of that server.  Therefore, public use of a modified version, on
-a publicly accessible server, gives the public access to the source
-code of the modified version.
-
-  An older license, called the Affero General Public License and
-published by Affero, was designed to accomplish similar goals.  This is
-a different license, not a version of the Affero GPL, but Affero has
-released a new version of the Affero GPL which permits relicensing under
-this license.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                       TERMS AND CONDITIONS
-
-  0. Definitions.
-
-  "This License" refers to version 3 of the GNU Affero General Public License.
-
-  "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
-  "The Program" refers to any copyrightable work licensed under this
-License.  Each licensee is addressed as "you".  "Licensees" and
-"recipients" may be individuals or organizations.
-
-  To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy.  The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
-  A "covered work" means either the unmodified Program or a work based
-on the Program.
-
-  To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy.  Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
-  To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies.  Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
-  An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License.  If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
-  1. Source Code.
-
-  The "source code" for a work means the preferred form of the work
-for making modifications to it.  "Object code" means any non-source
-form of a work.
-
-  A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
-  The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form.  A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
-  The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities.  However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work.  For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
-  The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
-  The Corresponding Source for a work in source code form is that
-same work.
-
-  2. Basic Permissions.
-
-  All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met.  This License explicitly affirms your unlimited
-permission to run the unmodified Program.  The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work.  This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
-  You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force.  You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright.  Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
-  Conveying under any other circumstances is permitted solely under
-the conditions stated below.  Sublicensing is not allowed; section 10
-makes it unnecessary.
-
-  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
-  No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
-  When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
-  4. Conveying Verbatim Copies.
-
-  You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
-  You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
-  5. Conveying Modified Source Versions.
-
-  You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
-    a) The work must carry prominent notices stating that you modified
-    it, and giving a relevant date.
-
-    b) The work must carry prominent notices stating that it is
-    released under this License and any conditions added under section
-    7.  This requirement modifies the requirement in section 4 to
-    "keep intact all notices".
-
-    c) You must license the entire work, as a whole, under this
-    License to anyone who comes into possession of a copy.  This
-    License will therefore apply, along with any applicable section 7
-    additional terms, to the whole of the work, and all its parts,
-    regardless of how they are packaged.  This License gives no
-    permission to license the work in any other way, but it does not
-    invalidate such permission if you have separately received it.
-
-    d) If the work has interactive user interfaces, each must display
-    Appropriate Legal Notices; however, if the Program has interactive
-    interfaces that do not display Appropriate Legal Notices, your
-    work need not make them do so.
-
-  A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit.  Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
-  6. Conveying Non-Source Forms.
-
-  You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
-    a) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by the
-    Corresponding Source fixed on a durable physical medium
-    customarily used for software interchange.
-
-    b) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by a
-    written offer, valid for at least three years and valid for as
-    long as you offer spare parts or customer support for that product
-    model, to give anyone who possesses the object code either (1) a
-    copy of the Corresponding Source for all the software in the
-    product that is covered by this License, on a durable physical
-    medium customarily used for software interchange, for a price no
-    more than your reasonable cost of physically performing this
-    conveying of source, or (2) access to copy the
-    Corresponding Source from a network server at no charge.
-
-    c) Convey individual copies of the object code with a copy of the
-    written offer to provide the Corresponding Source.  This
-    alternative is allowed only occasionally and noncommercially, and
-    only if you received the object code with such an offer, in accord
-    with subsection 6b.
-
-    d) Convey the object code by offering access from a designated
-    place (gratis or for a charge), and offer equivalent access to the
-    Corresponding Source in the same way through the same place at no
-    further charge.  You need not require recipients to copy the
-    Corresponding Source along with the object code.  If the place to
-    copy the object code is a network server, the Corresponding Source
-    may be on a different server (operated by you or a third party)
-    that supports equivalent copying facilities, provided you maintain
-    clear directions next to the object code saying where to find the
-    Corresponding Source.  Regardless of what server hosts the
-    Corresponding Source, you remain obligated to ensure that it is
-    available for as long as needed to satisfy these requirements.
-
-    e) Convey the object code using peer-to-peer transmission, provided
-    you inform other peers where the object code and Corresponding
-    Source of the work are being offered to the general public at no
-    charge under subsection 6d.
-
-  A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
-  A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling.  In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage.  For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product.  A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
-  "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source.  The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
-  If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information.  But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
-  The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed.  Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
-  Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
-  7. Additional Terms.
-
-  "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law.  If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
-  When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it.  (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.)  You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
-  Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
-    a) Disclaiming warranty or limiting liability differently from the
-    terms of sections 15 and 16 of this License; or
-
-    b) Requiring preservation of specified reasonable legal notices or
-    author attributions in that material or in the Appropriate Legal
-    Notices displayed by works containing it; or
-
-    c) Prohibiting misrepresentation of the origin of that material, or
-    requiring that modified versions of such material be marked in
-    reasonable ways as different from the original version; or
-
-    d) Limiting the use for publicity purposes of names of licensors or
-    authors of the material; or
-
-    e) Declining to grant rights under trademark law for use of some
-    trade names, trademarks, or service marks; or
-
-    f) Requiring indemnification of licensors and authors of that
-    material by anyone who conveys the material (or modified versions of
-    it) with contractual assumptions of liability to the recipient, for
-    any liability that these contractual assumptions directly impose on
-    those licensors and authors.
-
-  All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10.  If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term.  If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
-  If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
-  Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
-  8. Termination.
-
-  You may not propagate or modify a covered work except as expressly
-provided under this License.  Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
-  However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
-  Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
-  Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License.  If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
-  9. Acceptance Not Required for Having Copies.
-
-  You are not required to accept this License in order to receive or
-run a copy of the Program.  Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance.  However,
-nothing other than this License grants you permission to propagate or
-modify any covered work.  These actions infringe copyright if you do
-not accept this License.  Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
-  10. Automatic Licensing of Downstream Recipients.
-
-  Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License.  You are not responsible
-for enforcing compliance by third parties with this License.
-
-  An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations.  If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
-  You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License.  For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
-  11. Patents.
-
-  A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based.  The
-work thus licensed is called the contributor's "contributor version".
-
-  A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version.  For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
-  Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
-  In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement).  To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
-  If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients.  "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
-  If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
-  A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License.  You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
-  Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
-  12. No Surrender of Others' Freedom.
-
-  If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all.  For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
-  13. Remote Network Interaction; Use with the GNU General Public License.
-
-  Notwithstanding any other provision of this License, if you modify the
-Program, your modified version must prominently offer all users
-interacting with it remotely through a computer network (if your version
-supports such interaction) an opportunity to receive the Corresponding
-Source of your version by providing access to the Corresponding Source
-from a network server at no charge, through some standard or customary
-means of facilitating copying of software.  This Corresponding Source
-shall include the Corresponding Source for any work covered by version 3
-of the GNU General Public License that is incorporated pursuant to the
-following paragraph.
-
-  Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU General Public License into a single
-combined work, and to convey the resulting work.  The terms of this
-License will continue to apply to the part which is the covered work,
-but the work with which it is combined will remain governed by version
-3 of the GNU General Public License.
-
-  14. Revised Versions of this License.
-
-  The Free Software Foundation may publish revised and/or new versions of
-the GNU Affero General Public License from time to time.  Such new versions
-will be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-  Each version is given a distinguishing version number.  If the
-Program specifies that a certain numbered version of the GNU Affero General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation.  If the Program does not specify a version number of the
-GNU Affero General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
-  If the Program specifies that a proxy can decide which future
-versions of the GNU Affero General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
-  Later license versions may give you additional or different
-permissions.  However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
-  15. Disclaimer of Warranty.
-
-  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-  16. Limitation of Liability.
-
-  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
-  17. Interpretation of Sections 15 and 16.
-
-  If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
-                     END OF TERMS AND CONDITIONS
-
-            How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
-  If your software can interact with users remotely through a computer
-network, you should also make sure that it provides a way for users to
-get its source.  For example, if your program is a web application, its
-interface could display a "Source" link that leads users to an archive
-of the code.  There are many ways you could offer source, and different
-solutions will be better for different programs; see section 13 for the
-specific requirements.
-
-  You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU AGPL, see
-<http://www.gnu.org/licenses/>.
diff --git a/Linux.mk b/Linux.mk
deleted file mode 100644
index f6f5535..0000000
--- a/Linux.mk
+++ /dev/null
@@ -1,6 +0,0 @@
-LIBRESSL_PREFIX = /usr/local
-CFLAGS += -D_GNU_SOURCE
-LDLIBS = -lncursesw -lpthread
-LDLIBS += ${LIBRESSL_PREFIX}/lib/libtls.a
-LDLIBS += ${LIBRESSL_PREFIX}/lib/libssl.a
-LDLIBS += ${LIBRESSL_PREFIX}/lib/libcrypto.a
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 8519d59..0000000
--- a/Makefile
+++ /dev/null
@@ -1,99 +0,0 @@
-PREFIX = ~/.local
-MANDIR = ${PREFIX}/share/man
-CHROOT_USER = chat
-CHROOT_GROUP = ${CHROOT_USER}
-LIBRESSL_PREFIX = /usr/local
-
-CFLAGS += -std=c11 -Wall -Wextra -Wpedantic
-CFLAGS += -I${LIBRESSL_PREFIX}/include
-LDFLAGS += -L${LIBRESSL_PREFIX}/lib
-LDLIBS = -lcursesw -ltls
-
-BINS = catgirl
-MANS = catgirl.1
-
--include config.mk
-
-OBJS += chat.o
-OBJS += color.o
-OBJS += edit.o
-OBJS += event.o
-OBJS += format.o
-OBJS += handle.o
-OBJS += input.o
-OBJS += irc.o
-OBJS += log.o
-OBJS += pls.o
-OBJS += tab.o
-OBJS += tag.o
-OBJS += term.o
-OBJS += ui.o
-OBJS += url.o
-
-TESTS += format.t
-TESTS += pls.t
-TESTS += term.t
-
-all: tags ${BINS} test
-
-catgirl: ${OBJS}
-	${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@
-
-${OBJS}: chat.h
-
-test: ${TESTS}
-	set -e; ${TESTS:%=./%;}
-
-.SUFFIXES: .t
-
-.c.t:
-	${CC} ${CFLAGS} -DTEST ${LDFLAGS} $< ${LDLIBS} -o $@
-
-tags: *.c *.h
-	ctags -w *.c *.h
-
-install: ${BINS} ${MANS}
-	install -d ${PREFIX}/bin ${MANDIR}/man1
-	install ${BINS} ${PREFIX}/bin
-	install -m 644 ${MANS} ${MANDIR}/man1
-
-uninstall:
-	rm -f ${BINS:%=${PREFIX}/bin/%}
-	rm -f ${MANS:%=${MANDIR}/man1/%}
-
-chroot.tar: catgirl catgirl.1 man.sh
-	install -d -o root -g wheel \
-		root \
-		root/bin \
-		root/etc/ssl \
-		root/home \
-		root/lib \
-		root/libexec \
-		root/usr/bin \
-		root/usr/share/man \
-		root/usr/share/misc
-	install -d -o ${CHROOT_USER} -g ${CHROOT_GROUP} root/home/${CHROOT_USER}
-	cp -fp /libexec/ld-elf.so.1 root/libexec
-	cp -fp \
-		/lib/libc.so.7 \
-		/lib/libncursesw.so.8 \
-		/lib/libthr.so.3 \
-		/lib/libz.so.6 \
-		/usr/local/lib/libcrypto.so.45 \
-		/usr/local/lib/libssl.so.47 \
-		/usr/local/lib/libtls.so.19 \
-		root/lib
-	cp -fp /etc/hosts /etc/resolv.conf root/etc
-	cp -fp /etc/ssl/cert.pem root/etc/ssl
-	cp -af /usr/share/locale root/usr/share
-	cp -fp /usr/share/misc/termcap.db root/usr/share/misc
-	cp -fp /rescue/sh /usr/bin/mandoc /usr/bin/less root/bin
-	${MAKE} install PREFIX=root/usr
-	install man.sh root/usr/bin/man
-	tar -c -f chroot.tar -C root bin etc home lib libexec usr
-
-install-chroot: chroot.tar
-	tar -x -f chroot.tar -C /home/${CHROOT_USER}
-
-clean:
-	rm -fr ${BINS} ${OBJS} ${TESTS} tags root chroot.tar
diff --git a/NetBSD.mk b/NetBSD.mk
deleted file mode 100644
index 7c47665..0000000
--- a/NetBSD.mk
+++ /dev/null
@@ -1,3 +0,0 @@
-LIBRESSL_PREFIX = /usr/pkg/libressl
-LDFLAGS += -rpath=${LIBRESSL_PREFIX}/lib
-LDLIBS = -lcurses -ltls
diff --git a/README.7 b/README.7
deleted file mode 100644
index cae56bb..0000000
--- a/README.7
+++ /dev/null
@@ -1,111 +0,0 @@
-.Dd February 25, 2019
-.Dt CATGIRL 7
-.Os "Causal Agency"
-.
-.Sh NAME
-.Nm catgirl
-.Nd IRC client
-.
-.Sh DESCRIPTION
-.Nm
-is a curses IRC client
-originally intended for
-use over anonymous SSH.
-.
-.Pp
-It requires LibreSSL
-.Pq Fl ltls
-and targets
-.Fx ,
-Darwin,
-.Nx
-and
-GNU/Linux.
-.
-.Sh INSTALL
-On platforms other than
-.Fx ,
-copy the appropriate file to
-.Pa config.mk
-and modify as needed.
-The default install
-.Va PREFIX
-is
-.Pa ~/.local .
-.
-.Pp
-.Bd -literal -offset indent
-cp $(uname).mk config.mk
-make
-make install
-.Ed
-.
-.Ss Darwin
-LibreSSL is assumed to be installed with
-.Xr brew 1 .
-The
-.Xr sandman 1
-wrapper is also installed.
-.
-.Ss NetBSD
-LibreSSL is assumed to be installed with
-.Xr pkgsrc 7 .
-Due to bugs in
-.Nx Ap s
-.Xr curses 3
-implementation,
-some of the UI is currently broken.
-.
-.Ss GNU/Linux
-LibreSSL is assumed to be manually installed in
-.Pa /usr/local
-and is statically linked.
-.
-.Sh FILES
-.Bl -tag -width sandman.m -compact
-.It Pa chat.h
-shared state and function prototypes
-.It Pa chat.c
-command line parsing
-.It Pa event.c
-event loop and process spawning
-.It Pa tag.c
-tag (channel, query) ID assignment
-.It Pa handle.c
-incoming command handling
-.It Pa input.c
-input command handling
-.It Pa irc.c
-TLS client connection
-.It Pa format.c
-IRC formatting
-.It Pa color.c
-nick and channel coloring
-.It Pa ui.c
-cursed UI
-.It Pa term.c
-terminal features unsupported by curses
-.It Pa edit.c
-line editing
-.It Pa tab.c
-tab-complete
-.It Pa url.c
-URL detection
-.It Pa pls.c
-functions which should not have to be written
-.It Pa sandman.m
-utility for Darwin to signal sleep
-.El
-.
-.Pp
-.Bl -tag -width sshd_config -compact
-.It Pa sshd_config
-anonymous SSH configuration
-.It Pa man.sh
-.Xr man 1
-implementation for chroot
-.El
-.
-.Sh SEE ALSO
-.Xr catgirl 1 ,
-.Xr sandman 1
diff --git a/catgirl.1 b/catgirl.1
deleted file mode 100644
index 5511ed4..0000000
--- a/catgirl.1
+++ /dev/null
@@ -1,413 +0,0 @@
-.Dd October 3, 2019
-.Dt CATGIRL 1
-.Os
-.
-.Sh NAME
-.Nm catgirl
-.Nd IRC client
-.
-.Sh SYNOPSIS
-.Nm
-.Op Fl NPRv
-.Op Fl a Ar auth
-.Op Fl h Ar host
-.Op Fl j Ar chan
-.Op Fl k Ar keys
-.Op Fl l Ar path
-.Op Fl n Ar nick
-.Op Fl p Ar port
-.Op Fl r Ar real
-.Op Fl u Ar user
-.Op Fl w Ar pass
-.
-.Sh DESCRIPTION
-.Nm
-is a curses, TLS-only IRC client.
-.
-.Pp
-The arguments are as follows:
-.
-.Bl -tag -width "-w pass"
-.It Fl N
-Send notifications with
-.Xr notify-send 1 .
-.
-.It Fl P
-Prompt for nickname.
-.
-.It Fl R
-Restrict the use of the
-.Ic /join ,
-.Ic /query ,
-.Ic /quote ,
-.Ic /raw
-commands.
-.
-.It Fl a Ar auth
-Authenticate with SASL PLAIN.
-.Ar auth
-is a colon-separated
-username and password pair.
-.
-.It Fl h Ar host
-Connect to
-.Ar host .
-.
-.It Fl j Ar chan
-Join
-.Ar chan
-after connecting.
-.Ar chan
-may be a comma-separated list.
-.
-.It Fl k Ar keys
-Set keys for channels in
-.Fl j .
-.Ar keys
-may be a comma-separated list.
-.
-.It Fl l Ar path
-Log messages to
-subdirectories of
-.Ar path
-named by channel or nick
-in files named by date.
-.
-.It Fl n Ar nick
-Set nickname to
-.Ar nick .
-The default nickname
-is the user's name.
-.
-.It Fl p Ar port
-Connect to
-.Ar port .
-The default port is 6697.
-.
-.It Fl r Ar real
-Set realname to
-.Ar real .
-The default realname is
-the same as the nickname.
-.
-.It Fl u Ar user
-Set username to
-.Ar user .
-The default username is
-the same as the nickname.
-.
-.It Fl v
-Show raw IRC protocol in the
-.Sy <raw>
-window.
-If standard error is not a terminal,
-output raw IRC protocol
-to standard error.
-.
-.It Fl w Ar pass
-Log in with
-.Ar pass .
-.El
-.
-.Sh COMMANDS
-Any unique prefix
-may be used to abbreviate a command.
-.
-.Ss Chat Commands
-.Bl -tag -width Ds
-.It Ic /join Ar chan Op Ar key
-Join a channel.
-.
-.It Ic /list Op Ar chan
-List channels.
-.
-.It Ic /me Op Ar action
-Send an action message.
-.
-.It Ic /names , /who
-List users in the current channel.
-.
-.It Ic /nick Ar nick
-Change nicknames.
-.
-.It Ic /part Op Ar message
-Leave the current channel.
-.
-.It Ic /query Ar nick
-Open a private message view.
-.
-.It Ic /quit Op Ar message
-Quit IRC.
-.
-.It Ic /quote Ar command
-Send a raw IRC command.
-.
-.It Ic /topic Op Ar topic
-Show or set the topic of the current channel.
-.
-.It Ic /whois Ar nick
-Query information about a user.
-.
-.It Ic /znc Ar command
-Send
-.Xr znc 1
-command.
-.El
-.
-.Pp
-Any messages entered in the
-.Sy <raw>
-window will be sent as raw IRC commands.
-.
-.Ss UI Commands
-.Bl -tag -width Ds
-.It Ic /close
-Close the current window.
-.
-.It Ic /help , /man
-View this manual.
-.
-.It Ic /move Ar num
-Move window to number.
-If
-.Ar num
-starts with
-.Cm +
-or
-.Cm - ,
-the number is relative to the current window.
-.
-.It Ic /open Op Ar range
-Open a
-.Ar range
-of recent URLs
-in the current window with
-.Xr open 1 .
-URLs are numbered
-from the most recent
-starting at 1.
-The
-.Ar range
-may be a single number,
-or a hyphen- or comma-separated range.
-.
-.It Ic /open Ar substring
-Open the most recent URL
-in the current window
-matching the
-.Ar substring .
-.
-.It Ic /raw
-Toggle the
-.Sy <raw>
-window.
-.
-.It Ic /url
-Hide the UI
-and list the most recent URLs
-in the current window.
-Press
-.Ic Enter
-to resume the UI.
-.
-.It Ic /window Ar name
-Switch to window by name.
-.
-.It Ic /window Ar num , Ic / Ns Ar num
-Switch to window by number.
-If
-.Ar num
-starts with
-.Cm +
-or
-.Cm - ,
-the number is relative to the current window.
-.El
-.
-.Sh KEY BINDINGS
-.Nm
-provides
-.Xr emacs 1 Ns -like
-line editing keys
-as well as keys for applying IRC formatting.
-The prefixes
-.Ic C- , M- , S-
-represent the control, meta (alt) and shift modifiers,
-respectively.
-.Ic M- Ns Ar x
-sequences can also be typed as
-.Ic Esc
-followed by
-.Ar x .
-.
-.Ss Line Editing
-.Bl -tag -width Ds -compact
-.It Ic C-a
-Move cursor to beginning of line.
-.It Ic C-b
-Move cursor left.
-.It Ic C-d
-Delete character under cursor.
-.It Ic C-e
-Move cursor to end of line.
-.It Ic C-f
-Move cursor right.
-.It Ic C-k
-Delete line after cursor.
-.It Ic C-u
-Delete line.
-.It Ic C-w
-Delete word before cursor.
-.It Ic M-b
-Move cursor to beginning of word.
-.It Ic M-d
-Delete word after cursor.
-.It Ic M-f
-Move cursor to end of word.
-.It Ic Tab
-Cycle through completions for
-commands, nicks and channels.
-.El
-.
-.Ss IRC Formatting
-.Bl -tag -width Ds -compact
-.It Ic C-_
-Toggle underline.
-.It Ic C-o
-Toggle bold.
-.It Ic C-r
-Set or reset color.
-.It Ic C-s
-Reset formatting.
-.It Ic C-t
-Toggle italics.
-.It Ic C-v
-Toggle reverse video.
-This must usually be typed as
-.Ic C-v C-v .
-.El
-.
-.Pp
-To reset color, follow
-.Ic C-r
-by a non-digit.
-To set colors, follow
-.Ic C-r
-by one or two digits
-to set the foreground color,
-optionally followed by a comma
-and one or two digits
-to set the background color.
-.
-.Pp
-The color numbers are as follows:
-.Pp
-.Bl -column "7" "orange (dark yellow)" "15" "pink (light magenta)"
-.It 0 Ta white Ta \ 8 Ta yellow
-.It 1 Ta black Ta \ 9 Ta light green
-.It 2 Ta blue Ta 10 Ta cyan
-.It 3 Ta green Ta 11 Ta light cyan
-.It 4 Ta red Ta 12 Ta light blue
-.It 5 Ta brown (dark red) Ta 13 Ta pink (light magenta)
-.It 6 Ta magenta Ta 14 Ta gray
-.It 7 Ta orange (dark yellow) Ta 15 Ta light gray
-.El
-.
-.Ss Window Keys
-.Bl -tag -width "PageDown" -compact
-.It Ic C-l
-Redraw the UI.
-.It Ic C-n
-Switch to the next window.
-.It Ic C-p
-Switch to the previous window.
-.It Ic M-/
-Switch to the previously active window.
-.It Ic M-a
-Switch to next hot or unread window.
-.It Ic M-l
-Hide the UI and list the log for the current window.
-.It Ic M-m
-Insert a blank line in the window.
-.It Ic M- Ns Ar n
-Switch to window by number 0\(en9.
-.It Ic Down
-Scroll window down by one line.
-.It Ic PageDown
-Scroll window down by one page.
-.It Ic PageUp
-Scroll window up by one page.
-.It Ic Up
-Scroll window up by one line.
-.El
-.
-.Sh ENVIRONMENT
-.Bl -tag -width Ds
-.It Ev USER
-The default nickname.
-.El
-.
-.Sh EXAMPLES
-.Dl catgirl -h chat.freenode.net -j '#ascii.town'
-.
-.Sh STANDARDS
-.Nm
-is a partial implementation of the following:
-.
-.Bl -item
-.It
-.Rs
-.%A C. Kalt
-.%T Internet Relay Chat: Client Protocol
-.%I IETF
-.%N RFC 2812
-.%D April 2000
-.%U https://tools.ietf.org/html/rfc2812
-.Re
-.
-.It
-.Rs
-.%A Kevin L. Mitchell
-.%A Perry Lorier
-.%A Lee Hardy
-.%A William Pitcock
-.%T IRCv3.1 Client Capability Negotiation
-.%I IRCv3 Working Group
-.%U https://ircv3.net/specs/core/capability-negotiation-3.1.html
-.Re
-.
-.It
-.Rs
-.%A Jilles Tjoelker
-.%A William Pitcock
-.%T IRCv3.1 SASL Authentication
-.%I IRCv3 Working Group
-.%U https://ircv3.net/specs/extensions/sasl-3.1.html
-.Re
-.
-.It
-.Rs
-.%A K. Zeilenga, Ed.
-.%Q OpenLDAP Foundation
-.%T The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
-.%I IETF
-.%N RFC 4616
-.%D August 2006
-.%U https://tools.ietf.org/html/rfc4616
-.Re
-.
-.It
-.Rs
-.%A S. Josefsson
-.%Q SJD
-.%T The Base16, Base32, and Base64 Data Encodings
-.%I IETF
-.%N RFC 4648
-.%D October 2006
-.%U https://tools.ietf.org/html/rfc4648
-.Re
-.El
-.
-.Sh CAVEATS
-.Nm
-does not support unencrypted connections.
diff --git a/chat.c b/chat.c
deleted file mode 100644
index 3a1cfe9..0000000
--- a/chat.c
+++ /dev/null
@@ -1,91 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#define _WITH_GETLINE
-
-#include <err.h>
-#include <locale.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sysexits.h>
-#include <unistd.h>
-
-#include "chat.h"
-
-static char *dupe(const char *str) {
-	char *dup = strdup(str);
-	if (!dup) err(EX_OSERR, "strdup");
-	return dup;
-}
-
-static char *prompt(const char *prompt) {
-	char *line = NULL;
-	size_t cap;
-	for (;;) {
-		printf("%s", prompt);
-		fflush(stdout);
-
-		ssize_t len = getline(&line, &cap, stdin);
-		if (ferror(stdin)) err(EX_IOERR, "getline");
-		if (feof(stdin)) exit(EX_OK);
-		if (len < 2) continue;
-
-		line[len - 1] = '\0';
-		return line;
-	}
-}
-
-int main(int argc, char *argv[]) {
-	setlocale(LC_CTYPE, "");
-
-	int opt;
-	while (0 < (opt = getopt(argc, argv, "!NPRa:h:j:k:l:n:p:r:u:vw:"))) {
-		switch (opt) {
-			break; case '!': self.insecure = true;
-			break; case 'N': self.notify = true;
-			break; case 'P': self.nick = prompt("Name: ");
-			break; case 'R': self.limit = true;
-			break; case 'a': self.auth = dupe(optarg);
-			break; case 'h': self.host = dupe(optarg);
-			break; case 'j': self.join = dupe(optarg);
-			break; case 'k': self.keys = dupe(optarg);
-			break; case 'l': logOpen(optarg);
-			break; case 'n': self.nick = dupe(optarg);
-			break; case 'p': self.port = dupe(optarg);
-			break; case 'r': self.real = dupe(optarg);
-			break; case 'u': self.user = dupe(optarg);
-			break; case 'v': self.raw = true;
-			break; case 'w': self.pass = dupe(optarg);
-			break; default:  return EX_USAGE;
-		}
-	}
-
-	if (!self.nick) {
-		const char *user = getenv("USER");
-		if (!user) errx(EX_USAGE, "USER unset");
-		self.nick = dupe(user);
-	}
-
-	if (!self.host) self.host = prompt("Host: ");
-	if (!self.port) self.port = dupe("6697");
-	if (!self.user) self.user = dupe(self.nick);
-	if (!self.real) self.real = dupe(self.nick);
-
-	inputTab();
-	uiInit();
-	eventLoop();
-}
diff --git a/chat.h b/chat.h
deleted file mode 100644
index 01bab21..0000000
--- a/chat.h
+++ /dev/null
@@ -1,221 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#define SOURCE_URL "https://git.causal.agency/catgirl"
-
-#include <stdarg.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdnoreturn.h>
-#include <time.h>
-#include <wchar.h>
-
-#define MIN(a, b) ((a) < (b) ? (a) : (b))
-#define MAX(a, b) ((a) > (b) ? (a) : (b))
-
-#define err(...) do { uiHide(); err(__VA_ARGS__); } while (0)
-#define errx(...) do { uiHide(); errx(__VA_ARGS__); } while (0)
-
-typedef unsigned uint;
-typedef unsigned char byte;
-
-struct {
-	bool insecure;
-	char *host;
-	char *port;
-	char *auth;
-	char *pass;
-	char *nick;
-	char *user;
-	char *real;
-	char *join;
-	char *keys;
-	bool limit;
-	bool raw;
-	bool notify;
-	bool quit;
-} self;
-
-void eventWait(const char *argv[static 2]);
-void eventPipe(const char *argv[static 2]);
-noreturn void eventLoop(void);
-
-struct Tag {
-	size_t id;
-	const char *name;
-};
-
-enum { TagsLen = 256 };
-const struct Tag TagNone;
-const struct Tag TagStatus;
-const struct Tag TagRaw;
-struct Tag tagFind(const char *name);
-struct Tag tagFor(const char *name);
-
-enum IRCColor {
-	IRCWhite,
-	IRCBlack,
-	IRCBlue,
-	IRCGreen,
-	IRCRed,
-	IRCBrown,
-	IRCMagenta,
-	IRCOrange,
-	IRCYellow,
-	IRCLightGreen,
-	IRCCyan,
-	IRCLightCyan,
-	IRCLightBlue,
-	IRCPink,
-	IRCGray,
-	IRCLightGray,
-	IRCDefault = 99,
-};
-enum {
-	IRCBold      = 002,
-	IRCColor     = 003,
-	IRCReverse   = 026,
-	IRCReset     = 017,
-	IRCItalic    = 035,
-	IRCUnderline = 037,
-};
-
-struct Format {
-	const wchar_t *str;
-	size_t len;
-	bool split;
-	bool bold, italic, underline, reverse;
-	enum IRCColor fg, bg;
-};
-void formatReset(struct Format *format);
-bool formatParse(struct Format *format, const wchar_t *split);
-
-enum IRCColor colorGen(const char *str);
-struct Tag colorTag(struct Tag tag, const char *gen);
-enum IRCColor colorFor(struct Tag tag);
-
-void handle(char *line);
-void input(struct Tag tag, char *line);
-void inputTab(void);
-
-int ircConnect(void);
-void ircRead(void);
-void ircWrite(const char *ptr, size_t len);
-void ircFmt(const char *format, ...) __attribute__((format(printf, 1, 2)));
-void ircQuit(const char *mesg);
-
-void uiInit(void);
-void uiShow(void);
-void uiHide(void);
-void uiDraw(void);
-void uiRead(void);
-void uiExit(int status);
-
-void uiPrompt(bool nickChanged);
-void uiShowTag(struct Tag tag);
-void uiShowNum(int num, bool relative);
-void uiMoveTag(struct Tag tag, int num, bool relative);
-void uiCloseTag(struct Tag tag);
-
-enum UIHeat {
-	UICold,
-	UIWarm,
-	UIHot,
-};
-void uiLog(struct Tag tag, enum UIHeat heat, const wchar_t *str);
-void uiFmt(struct Tag tag, enum UIHeat heat, const wchar_t *format, ...);
-
-enum TermMode {
-	TermFocus,
-	TermPaste,
-};
-enum TermEvent {
-	TermNone,
-	TermFocusIn,
-	TermFocusOut,
-	TermPasteStart,
-	TermPasteEnd,
-};
-void termInit(void);
-void termNoFlow(void);
-void termTitle(const char *title);
-void termMode(enum TermMode mode, bool set);
-enum TermEvent termEvent(char ch);
-
-enum Edit {
-	EditLeft,
-	EditRight,
-	EditHome,
-	EditEnd,
-	EditBackWord,
-	EditForeWord,
-	EditInsert,
-	EditBackspace,
-	EditDelete,
-	EditKill,
-	EditKillBackWord,
-	EditKillForeWord,
-	EditKillEnd,
-	EditComplete,
-	EditEnter,
-};
-void edit(struct Tag tag, enum Edit op, wchar_t ch);
-const wchar_t *editHead(void);
-const wchar_t *editTail(void);
-
-void tabTouch(struct Tag tag, const char *word);
-void tabAdd(struct Tag tag, const char *word);
-void tabRemove(struct Tag tag, const char *word);
-void tabReplace(struct Tag tag, const char *prev, const char *next);
-void tabClear(struct Tag tag);
-struct Tag tabTag(const char *word);
-const char *tabNext(struct Tag tag, const char *prefix);
-void tabAccept(void);
-void tabReject(void);
-
-void urlScan(struct Tag tag, const char *str);
-void urlList(struct Tag tag);
-void urlOpenMatch(struct Tag tag, const char *substr);
-void urlOpenRange(struct Tag tag, size_t at, size_t to);
-
-void logOpen(const char *path);
-void logFmt(
-	struct Tag tag, const time_t *ts, const char *format, ...
-) __attribute__((format(printf, 3, 4)));
-void logList(struct Tag tag);
-void logReplay(struct Tag tag);
-
-wchar_t *wcsnchr(const wchar_t *wcs, size_t len, wchar_t chr);
-wchar_t *wcsnrchr(const wchar_t *wcs, size_t len, wchar_t chr);
-wchar_t *ambstowcs(const char *src);
-char *awcstombs(const wchar_t *src);
-char *awcsntombs(const wchar_t *src, size_t nwc);
-int vaswprintf(wchar_t **ret, const wchar_t *format, va_list ap);
-int aswprintf(wchar_t **ret, const wchar_t *format, ...);
-
-size_t base64Size(size_t len);
-void base64(char *dst, const byte *src, size_t len);
-
-// HACK: clang won't check wchar_t *format strings.
-#ifdef NDEBUG
-#define uiFmt(tag, heat, format, ...) uiFmt(tag, heat, L##format, __VA_ARGS__)
-#else
-#define uiFmt(tag, heat, format, ...) do { \
-	snprintf(NULL, 0, format, __VA_ARGS__); \
-	uiFmt(tag, heat, L##format, __VA_ARGS__); \
-} while(0)
-#endif
diff --git a/color.c b/color.c
deleted file mode 100644
index dad5647..0000000
--- a/color.c
+++ /dev/null
@@ -1,51 +0,0 @@
-/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <stdint.h>
-
-#include "chat.h"
-
-// Adapted from <https://github.com/cbreeden/fxhash/blob/master/lib.rs>.
-static uint32_t hashChar(uint32_t hash, char ch) {
-	hash = (hash << 5) | (hash >> 27);
-	hash ^= ch;
-	hash *= 0x27220A95;
-	return hash;
-}
-
-enum IRCColor colorGen(const char *str) {
-	if (!str) return IRCDefault;
-	uint32_t hash = 0;
-	if (*str == '~') str++;
-	for (; *str; ++str) {
-		hash = hashChar(hash, *str);
-	}
-	while (IRCBlack == (hash & IRCLightGray)) {
-		hash = hashChar(hash, '\0');
-	}
-	return (hash & IRCLightGray);
-}
-
-static enum IRCColor colors[TagsLen];
-
-struct Tag colorTag(struct Tag tag, const char *gen) {
-	if (!colors[tag.id]) colors[tag.id] = 1 + colorGen(gen);
-	return tag;
-}
-
-enum IRCColor colorFor(struct Tag tag) {
-	return colors[tag.id] ? colors[tag.id] - 1 : IRCDefault;
-}
diff --git a/edit.c b/edit.c
deleted file mode 100644
index c63e4a2..0000000
--- a/edit.c
+++ /dev/null
@@ -1,186 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <err.h>
-#include <stdbool.h>
-#include <stdlib.h>
-#include <sysexits.h>
-#include <wchar.h>
-#include <wctype.h>
-
-#include "chat.h"
-
-enum { BufLen = 512 };
-static struct {
-	wchar_t buf[BufLen];
-	wchar_t *ptr;
-	wchar_t *end;
-	wchar_t *tab;
-} line = {
-	.ptr = line.buf,
-	.end = line.buf,
-};
-
-const wchar_t *editHead(void) {
-	return line.buf;
-}
-const wchar_t *editTail(void) {
-	return line.ptr;
-}
-
-static void left(void) {
-	if (line.ptr > line.buf) line.ptr--;
-}
-static void right(void) {
-	if (line.ptr < line.end) line.ptr++;
-}
-
-static void backWord(void) {
-	left();
-	wchar_t *word = wcsnrchr(line.buf, line.ptr - line.buf, L' ');
-	line.ptr = (word ? &word[1] : line.buf);
-}
-static void foreWord(void) {
-	right();
-	wchar_t *word = wcsnchr(line.ptr, line.end - line.ptr, L' ');
-	line.ptr = (word ? word : line.end);
-}
-
-static void insert(wchar_t ch) {
-	if (line.end == &line.buf[BufLen - 1]) return;
-	if (line.ptr != line.end) {
-		wmemmove(line.ptr + 1, line.ptr, line.end - line.ptr);
-	}
-	*line.ptr++ = ch;
-	line.end++;
-}
-static void backspace(void) {
-	if (line.ptr == line.buf) return;
-	if (line.ptr != line.end) {
-		wmemmove(line.ptr - 1, line.ptr, line.end - line.ptr);
-	}
-	line.ptr--;
-	line.end--;
-}
-static void delete(void) {
-	if (line.ptr == line.end) return;
-	right();
-	backspace();
-}
-
-static void killBackWord(void) {
-	wchar_t *from = line.ptr;
-	backWord();
-	wmemmove(line.ptr, from, line.end - from);
-	line.end -= from - line.ptr;
-}
-static void killForeWord(void) {
-	wchar_t *from = line.ptr;
-	foreWord();
-	wmemmove(from, line.ptr, line.end - line.ptr);
-	line.end -= line.ptr - from;
-	line.ptr = from;
-}
-
-static char *prefix;
-static void complete(struct Tag tag) {
-	if (!line.tab) {
-		line.tab = wcsnrchr(line.buf, line.ptr - line.buf, L' ');
-		line.tab = (line.tab ? &line.tab[1] : line.buf);
-		prefix = awcsntombs(line.tab, line.ptr - line.tab);
-		if (!prefix) err(EX_DATAERR, "awcstombs");
-	}
-
-	const char *next = tabNext(tag, prefix);
-	if (!next) return;
-
-	wchar_t *wcs = ambstowcs(next);
-	if (!wcs) err(EX_DATAERR, "ambstowcs");
-
-	size_t i = 0;
-	for (; wcs[i] && line.ptr > &line.tab[i]; ++i) {
-		line.tab[i] = wcs[i];
-	}
-	while (line.ptr > &line.tab[i]) {
-		backspace();
-	}
-	for (; wcs[i]; ++i) {
-		insert(wcs[i]);
-	}
-	free(wcs);
-
-	size_t pos = line.tab - line.buf;
-	if (!pos && line.tab[0] != L'/') {
-		insert(L':');
-	} else if (pos >= 2) {
-		if (line.buf[pos - 2] == L':') {
-			line.buf[pos - 2] = L',';
-			insert(L':');
-		}
-	}
-	insert(L' ');
-}
-
-static void accept(void) {
-	if (!line.tab) return;
-	line.tab = NULL;
-	free(prefix);
-	tabAccept();
-}
-static void reject(void) {
-	if (!line.tab) return;
-	line.tab = NULL;
-	free(prefix);
-	tabReject();
-}
-
-static void enter(struct Tag tag) {
-	if (line.end == line.buf) return;
-	*line.end = L'\0';
-	char *str = awcstombs(line.buf);
-	if (!str) err(EX_DATAERR, "awcstombs");
-	input(tag, str);
-	free(str);
-	line.ptr = line.buf;
-	line.end = line.buf;
-}
-
-void edit(struct Tag tag, enum Edit op, wchar_t ch) {
-	switch (op) {
-		break; case EditLeft:  reject(); left();
-		break; case EditRight: reject(); right();
-		break; case EditHome:  reject(); line.ptr = line.buf;
-		break; case EditEnd:   reject(); line.ptr = line.end;
-
-		break; case EditBackWord: reject(); backWord();
-		break; case EditForeWord: reject(); foreWord();
-
-		break; case EditInsert:    accept(); insert(ch);
-		break; case EditBackspace: reject(); backspace();
-		break; case EditDelete:    reject(); delete();
-
-		break; case EditKill:         reject(); line.ptr = line.end = line.buf;
-		break; case EditKillBackWord: reject(); killBackWord();
-		break; case EditKillForeWord: reject(); killForeWord();
-		break; case EditKillEnd:      reject(); line.end = line.ptr;
-
-		break; case EditComplete: complete(tag);
-
-		break; case EditEnter: accept(); enter(tag);
-	}
-
-	*line.end = L'\0';
-}
diff --git a/event.c b/event.c
deleted file mode 100644
index c6e0987..0000000
--- a/event.c
+++ /dev/null
@@ -1,168 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <assert.h>
-#include <err.h>
-#include <errno.h>
-#include <poll.h>
-#include <signal.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdnoreturn.h>
-#include <string.h>
-#include <sys/wait.h>
-#include <sysexits.h>
-#include <unistd.h>
-
-#include "chat.h"
-
-static struct {
-	bool wait;
-	int pipe;
-} child = {
-	.pipe = -1,
-};
-
-void eventWait(const char *argv[static 2]) {
-	uiHide();
-	pid_t pid = fork();
-	if (pid < 0) err(EX_OSERR, "fork");
-	if (!pid) {
-		execvp(argv[0], (char *const *)argv);
-		err(EX_CONFIG, "%s", argv[0]);
-	}
-	child.wait = true;
-}
-
-static void childWait(void) {
-	uiShow();
-	int status;
-	pid_t pid = wait(&status);
-	if (pid < 0) err(EX_OSERR, "wait");
-	if (WIFEXITED(status) && WEXITSTATUS(status)) {
-		uiFmt(TagStatus, UIHot, "event: exit %d", WEXITSTATUS(status));
-	} else if (WIFSIGNALED(status)) {
-		uiFmt(
-			TagStatus, UIHot,
-			"event: signal %s", strsignal(WTERMSIG(status))
-		);
-	}
-	child.wait = false;
-}
-
-void eventPipe(const char *argv[static 2]) {
-	if (child.pipe > 0) {
-		uiLog(TagStatus, UIHot, L"event: existing pipe");
-		return;
-	}
-
-	int rw[2];
-	int error = pipe(rw);
-	if (error) err(EX_OSERR, "pipe");
-
-	pid_t pid = fork();
-	if (pid < 0) err(EX_OSERR, "fork");
-	if (!pid) {
-		close(rw[0]);
-		close(STDIN_FILENO);
-		dup2(rw[1], STDOUT_FILENO);
-		dup2(rw[1], STDERR_FILENO);
-		close(rw[1]);
-		execvp(argv[0], (char *const *)argv);
-		perror(argv[0]);
-		exit(EX_CONFIG);
-	}
-
-	close(rw[1]);
-	child.pipe = rw[0];
-}
-
-static void childRead(void) {
-	char buf[256];
-	ssize_t len = read(child.pipe, buf, sizeof(buf) - 1);
-	if (len < 0) err(EX_IOERR, "read");
-	if (len) {
-		buf[len] = '\0';
-		buf[strcspn(buf, "\n")] = '\0';
-		uiFmt(TagStatus, UIHot, "event: %s", buf);
-	} else {
-		close(child.pipe);
-		child.pipe = -1;
-	}
-}
-
-static volatile sig_atomic_t sig[NSIG];
-static void handler(int n) {
-	sig[n] = 1;
-}
-
-noreturn void eventLoop(void) {
-	sigset_t mask;
-	sigemptyset(&mask);
-	struct sigaction action = {
-		.sa_handler = handler,
-		.sa_mask = mask,
-		.sa_flags = SA_RESTART | SA_NOCLDSTOP,
-	};
-	sigaction(SIGCHLD, &action, NULL);
-	sigaction(SIGINT, &action, NULL);
-	sigaction(SIGHUP, &action, NULL);
-
-	struct sigaction curses;
-	sigaction(SIGWINCH, &action, &curses);
-	assert(!(curses.sa_flags & SA_SIGINFO));
-
-	uiShowTag(TagStatus);
-	uiFmt(TagStatus, UICold, "Traveling to %s...", self.host);
-	uiDraw();
-	int irc = ircConnect();
-
-	for (;;) {
-		if (sig[SIGCHLD]) childWait();
-		if (sig[SIGHUP]) ircQuit("zzz");
-		if (sig[SIGINT]) {
-			signal(SIGINT, SIG_DFL);
-			ircQuit("Goodbye");
-		}
-		if (sig[SIGWINCH]) {
-			curses.sa_handler(SIGWINCH);
-			uiRead();
-			uiDraw();
-		}
-		sig[SIGCHLD] = sig[SIGHUP] = sig[SIGINT] = sig[SIGWINCH] = 0;
-
-		struct pollfd fds[3] = {
-			{ .events = POLLIN, .fd = irc },
-			{ .events = POLLIN, .fd = STDIN_FILENO },
-			{ .events = POLLIN, .fd = child.pipe },
-		};
-		if (child.wait) fds[1].events = 0;
-		if (child.pipe < 0) fds[2].events = 0;
-
-		int nfds = poll(fds, 3, -1);
-		if (nfds < 0) {
-			if (errno == EINTR) continue;
-			err(EX_IOERR, "poll");
-		}
-
-		if (fds[0].revents) ircRead();
-		if (fds[1].revents) uiRead();
-		if (fds[2].revents) childRead();
-
-		uiDraw();
-	}
-}
diff --git a/format.c b/format.c
deleted file mode 100644
index 71d1d93..0000000
--- a/format.c
+++ /dev/null
@@ -1,162 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdlib.h>
-#include <wchar.h>
-
-#include "chat.h"
-
-void formatReset(struct Format *format) {
-	format->bold = false;
-	format->italic = false;
-	format->underline = false;
-	format->reverse = false;
-	format->fg = IRCDefault;
-	format->bg = IRCDefault;
-}
-
-static void parseColor(struct Format *format) {
-	size_t len = MIN(wcsspn(format->str, L"0123456789"), 2);
-	if (!len) {
-		format->fg = IRCDefault;
-		format->bg = IRCDefault;
-		return;
-	}
-	format->fg = 0;
-	for (size_t i = 0; i < len; ++i) {
-		format->fg *= 10;
-		format->fg += format->str[i] - L'0';
-	}
-	if (format->fg > IRCLightGray) format->fg = IRCDefault;
-	format->str = &format->str[len];
-
-	len = 0;
-	if (format->str[0] == L',') {
-		len = MIN(wcsspn(&format->str[1], L"0123456789"), 2);
-	}
-	if (!len) return;
-	format->bg = 0;
-	for (size_t i = 0; i < len; ++i) {
-		format->bg *= 10;
-		format->bg += format->str[1 + i] - L'0';
-	}
-	if (format->bg > IRCLightGray) format->bg = IRCDefault;
-	format->str = &format->str[1 + len];
-}
-
-static const wchar_t Codes[] = {
-	IRCBold, IRCColor, IRCReverse, IRCReset, IRCItalic, IRCUnderline, L'\0',
-};
-
-bool formatParse(struct Format *format, const wchar_t *split) {
-	format->str += format->len;
-	if (!format->str[0]) {
-		if (split == format->str && !format->split) {
-			format->len = 0;
-			format->split = true;
-			return true;
-		}
-		return false;
-	}
-
-	const wchar_t *init = format->str;
-	for (bool done = false; !done;) {
-		switch (format->str[0]) {
-			break; case IRCBold:      format->str++; format->bold ^= true;
-			break; case IRCItalic:    format->str++; format->italic ^= true;
-			break; case IRCUnderline: format->str++; format->underline ^= true;
-			break; case IRCReverse:   format->str++; format->reverse ^= true;
-			break; case IRCColor:     format->str++; parseColor(format);
-			break; case IRCReset:     format->str++; formatReset(format);
-			break; default:           done = true;
-		}
-	}
-	format->split = (split >= init && split <= format->str);
-
-	format->len = wcscspn(format->str, Codes);
-	if (split > format->str && split < &format->str[format->len]) {
-		format->len = split - format->str;
-	}
-	return true;
-}
-
-#ifdef TEST
-#include <assert.h>
-
-static bool testColor(
-	const wchar_t *str, enum IRCColor fg, enum IRCColor bg, size_t index
-) {
-	struct Format format = { .str = str };
-	formatReset(&format);
-	if (!formatParse(&format, NULL)) return false;
-	if (format.fg != fg) return false;
-	if (format.bg != bg) return false;
-	return (format.str == &str[index]);
-}
-
-static bool testSplit(const wchar_t *str, size_t index) {
-	struct Format format = { .str = str };
-	formatReset(&format);
-	bool split = false;
-	while (formatParse(&format, &str[index])) {
-		if (format.split && split) return false;
-		if (format.split) split = true;
-	}
-	return split;
-}
-
-static bool testSplits(const wchar_t *str) {
-	for (size_t i = 0; i <= wcslen(str); ++i) {
-		if (!testSplit(str, i)) return false;
-	}
-	return true;
-}
-
-int main() {
-	assert(testColor(L"\003a",      IRCDefault,   IRCDefault,   1));
-	assert(testColor(L"\003,a",     IRCDefault,   IRCDefault,   1));
-	assert(testColor(L"\003,1",     IRCDefault,   IRCDefault,   1));
-	assert(testColor(L"\0031a",     IRCBlack,     IRCDefault,   2));
-	assert(testColor(L"\0031,a",    IRCBlack,     IRCDefault,   2));
-	assert(testColor(L"\00312a",    IRCLightBlue, IRCDefault,   3));
-	assert(testColor(L"\00312,a",   IRCLightBlue, IRCDefault,   3));
-	assert(testColor(L"\003123",    IRCLightBlue, IRCDefault,   3));
-	assert(testColor(L"\0031,1a",   IRCBlack,     IRCBlack,     4));
-	assert(testColor(L"\0031,12a",  IRCBlack,     IRCLightBlue, 5));
-	assert(testColor(L"\0031,123",  IRCBlack,     IRCLightBlue, 5));
-	assert(testColor(L"\00312,1a",  IRCLightBlue, IRCBlack,     5));
-	assert(testColor(L"\00312,12a", IRCLightBlue, IRCLightBlue, 6));
-	assert(testColor(L"\00312,123", IRCLightBlue, IRCLightBlue, 6));
-
-	assert(testColor(L"\00316,16a", IRCDefault, IRCDefault, 6));
-	assert(testColor(L"\00399,99a", IRCDefault, IRCDefault, 6));
-
-	assert(testSplits(L""));
-	assert(testSplits(L"ab"));
-	assert(testSplits(L"\002"));
-	assert(testSplits(L"\002ab"));
-	assert(testSplits(L"a\002b"));
-	assert(testSplits(L"\002\003"));
-	assert(testSplits(L"a\002\003b"));
-	assert(testSplits(L"a\0031b"));
-	assert(testSplits(L"a\00312b"));
-	assert(testSplits(L"a\00312,1b"));
-	assert(testSplits(L"a\00312,12b"));
-}
-
-#endif
diff --git a/handle.c b/handle.c
deleted file mode 100644
index fe15d9a..0000000
--- a/handle.c
+++ /dev/null
@@ -1,568 +0,0 @@
-/* Copyright (C) 2018, 2019  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <ctype.h>
-#include <err.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sysexits.h>
-#include <time.h>
-
-#include "chat.h"
-
-static char *paramField(char **params) {
-	char *rest = *params;
-	if (rest[0] == ':') {
-		*params = NULL;
-		return &rest[1];
-	}
-	return strsep(params, " ");
-}
-
-static void parse(
-	char *prefix, char **nick, char **user, char **host,
-	char *params, size_t req, size_t opt, /* (char **) */ ...
-) {
-	char *field;
-	if (prefix) {
-		field = strsep(&prefix, "!");
-		if (nick) *nick = field;
-		field = strsep(&prefix, "@");
-		if (user) *user = field;
-		if (host) *host = prefix;
-	}
-
-	va_list ap;
-	va_start(ap, opt);
-	for (size_t i = 0; i < req; ++i) {
-		if (!params) errx(EX_PROTOCOL, "%zu params required, found %zu", req, i);
-		field = paramField(&params);
-		char **param = va_arg(ap, char **);
-		if (param) *param = field;
-	}
-	for (size_t i = 0; i < opt; ++i) {
-		char **param = va_arg(ap, char **);
-		if (params) {
-			*param = paramField(&params);
-		} else {
-			*param = NULL;
-		}
-	}
-	va_end(ap);
-}
-
-static bool isPing(const char *mesg) {
-	size_t len = strlen(self.nick);
-	const char *match = mesg;
-	while (NULL != (match = strcasestr(match, self.nick))) {
-		char b = (match > mesg ? *(match - 1) : ' ');
-		char a = (match[len] ? match[len] : ' ');
-		match = &match[len];
-		if (!isspace(b) && !ispunct(b)) continue;
-		if (!isspace(a) && !ispunct(a)) continue;
-		return true;
-	}
-	return false;
-}
-
-static char *dequote(char *mesg) {
-	if (mesg[0] == '"') mesg = &mesg[1];
-	size_t len = strlen(mesg);
-	if (mesg[len - 1] == '"') mesg[len - 1] = '\0';
-	return mesg;
-}
-
-typedef void Handler(char *prefix, char *params);
-
-static void handlePing(char *prefix, char *params) {
-	(void)prefix;
-	ircFmt("PONG %s\r\n", params);
-}
-
-static void handleError(char *prefix, char *params) {
-	char *mesg;
-	parse(prefix, NULL, NULL, NULL, params, 1, 0, &mesg);
-	if (self.quit) {
-		uiExit(EX_OK);
-	} else {
-		errx(EX_PROTOCOL, "%s", mesg);
-	}
-}
-
-static void handleCap(char *prefix, char *params) {
-	char *subc, *list;
-	parse(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &subc, &list);
-	if (!strcmp(subc, "ACK") && self.auth) {
-		size_t len = strlen(self.auth);
-		byte plain[1 + len];
-		plain[0] = 0;
-		for (size_t i = 0; i < len; ++i) {
-			plain[1 + i] = (self.auth[i] == ':' ? 0 : self.auth[i]);
-		}
-		char b64[base64Size(sizeof(plain))];
-		base64(b64, plain, sizeof(plain));
-		ircFmt("AUTHENTICATE PLAIN\r\n");
-		ircFmt("AUTHENTICATE %s\r\n", b64);
-	}
-	ircFmt("CAP END\r\n");
-}
-
-static void handleErrorErroneousNickname(char *prefix, char *params) {
-	char *mesg;
-	parse(prefix, NULL, NULL, NULL, params, 3, 0, NULL, NULL, &mesg);
-	uiFmt(TagStatus, UIHot, "You can't use that name here: \"%s\"", mesg);
-	uiLog(TagStatus, UICold, L"Type /nick <name> to choose a new one");
-}
-
-static void handleReplyWelcome(char *prefix, char *params) {
-	char *nick;
-	parse(prefix, NULL, NULL, NULL, params, 1, 0, &nick);
-
-	if (strcmp(nick, self.nick)) {
-		free(self.nick);
-		self.nick = strdup(nick);
-		if (!self.nick) err(EX_OSERR, "strdup");
-		uiPrompt(true);
-	}
-	if (self.join && self.keys) {
-		ircFmt("JOIN %s %s\r\n", self.join, self.keys);
-	} else if (self.join) {
-		ircFmt("JOIN %s\r\n", self.join);
-	}
-	tabTouch(TagStatus, self.nick);
-
-	uiLog(TagStatus, UICold, L"You have arrived");
-}
-
-static void handleReplyMOTD(char *prefix, char *params) {
-	char *mesg;
-	parse(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &mesg);
-	if (mesg[0] == '-' && mesg[1] == ' ') mesg = &mesg[2];
-
-	urlScan(TagStatus, mesg);
-	uiFmt(TagStatus, UICold, "%s", mesg);
-}
-
-static void handleReplyList(char *prefix, char *params) {
-	char *chan, *count, *topic;
-	parse(prefix, NULL, NULL, NULL, params, 4, 0, NULL, &chan, &count, &topic);
-	if (topic[0] == '[') {
-		char *skip = strstr(topic, "] ");
-		if (skip) topic = &skip[2];
-	}
-	const char *people = (strcmp(count, "1") ? "people" : "person");
-	if (topic[0]) {
-		uiFmt(
-			TagStatus, UIWarm,
-			"You see %s %s in \3%d%s\3 under the banner, \"%s\"",
-			count, people, colorGen(chan), chan, topic
-		);
-	} else {
-		uiFmt(
-			TagStatus, UIWarm,
-			"You see %s %s in \3%d%s\3",
-			count, people, colorGen(chan), chan
-		);
-	}
-}
-
-static void handleReplyListEnd(char *prefix, char *params) {
-	(void)prefix;
-	(void)params;
-	uiLog(TagStatus, UICold, L"You don't see anyone else");
-}
-
-static enum IRCColor whoisColor;
-static void handleReplyWhoisUser(char *prefix, char *params) {
-	char *nick, *user, *host, *real;
-	parse(
-		prefix, NULL, NULL, NULL,
-		params, 6, 0, NULL, &nick, &user, &host, NULL, &real
-	);
-	whoisColor = colorGen(user);
-	uiFmt(
-		TagStatus, UIWarm,
-		"\3%d%s\3 is %s@%s, \"%s\"",
-		whoisColor, nick, user, host, real
-	);
-}
-
-static void handleReplyWhoisServer(char *prefix, char *params) {
-	char *nick, *serv, *info;
-	parse(prefix, NULL, NULL, NULL, params, 4, 0, NULL, &nick, &serv, &info);
-	uiFmt(
-		TagStatus, UIWarm,
-		"\3%d%s\3 is connected to %s, \"%s\"",
-		whoisColor, nick, serv, info
-	);
-}
-
-static void handleReplyWhoisOperator(char *prefix, char *params) {
-	char *nick, *oper;
-	parse(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &nick, &oper);
-	uiFmt(TagStatus, UIWarm, "\3%d%s\3 %s", whoisColor, nick, oper);
-}
-
-static void handleReplyWhoisIdle(char *prefix, char *params) {
-	char *nick, *idle, *sign;
-	parse(prefix, NULL, NULL, NULL, params, 4, 0, NULL, &nick, &idle, &sign);
-	time_t time = strtoul(sign, NULL, 10);
-	const char *at = ctime(&time);
-	unsigned long secs  = strtoul(idle, NULL, 10);
-	unsigned long mins  = secs / 60; secs %= 60;
-	unsigned long hours = mins / 60; mins %= 60;
-	uiFmt(
-		TagStatus, UIWarm,
-		"\3%d%s\3 signed on at %.24s and has been idle for %02lu:%02lu:%02lu",
-		whoisColor, nick, at, hours, mins, secs
-	);
-}
-
-static void handleReplyWhoisChannels(char *prefix, char *params) {
-	char *nick, *chans;
-	parse(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &nick, &chans);
-	uiFmt(TagStatus, UIWarm, "\3%d%s\3 is in %s", whoisColor, nick, chans);
-}
-
-static void handleErrorNoSuchNick(char *prefix, char *params) {
-	char *nick, *mesg;
-	parse(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &nick, &mesg);
-	uiFmt(TagStatus, UIWarm, "%s, \"%s\"", mesg, nick);
-}
-
-static void handleJoin(char *prefix, char *params) {
-	char *nick, *user, *chan;
-	parse(prefix, &nick, &user, NULL, params, 1, 0, &chan);
-	struct Tag tag = colorTag(tagFor(chan), chan);
-
-	if (!strcmp(nick, self.nick)) {
-		tabTouch(TagNone, chan);
-		uiShowTag(tag);
-		logReplay(tag);
-	}
-	tabTouch(tag, nick);
-
-	uiFmt(
-		tag, UICold,
-		"\3%d%s\3 arrives in \3%d%s\3",
-		colorGen(user), nick, colorGen(chan), chan
-	);
-	logFmt(tag, NULL, "%s arrives in %s", nick, chan);
-}
-
-static void handlePart(char *prefix, char *params) {
-	char *nick, *user, *chan, *mesg;
-	parse(prefix, &nick, &user, NULL, params, 1, 1, &chan, &mesg);
-	struct Tag tag = colorTag(tagFor(chan), chan);
-
-	if (!strcmp(nick, self.nick)) {
-		tabClear(tag);
-	} else {
-		tabRemove(tag, nick);
-	}
-
-	if (mesg) {
-		urlScan(tag, mesg);
-		uiFmt(
-			tag, UICold,
-			"\3%d%s\3 leaves \3%d%s\3, \"%s\"",
-			colorGen(user), nick, colorGen(chan), chan, dequote(mesg)
-		);
-		logFmt(tag, NULL, "%s leaves %s, \"%s\"", nick, chan, dequote(mesg));
-	} else {
-		uiFmt(
-			tag, UICold,
-			"\3%d%s\3 leaves \3%d%s\3",
-			colorGen(user), nick, colorGen(chan), chan
-		);
-		logFmt(tag, NULL, "%s leaves %s", nick, chan);
-	}
-}
-
-static void handleKick(char *prefix, char *params) {
-	char *nick, *user, *chan, *kick, *mesg;
-	parse(prefix, &nick, &user, NULL, params, 2, 1, &chan, &kick, &mesg);
-	struct Tag tag = colorTag(tagFor(chan), chan);
-	bool kicked = !strcmp(kick, self.nick);
-
-	if (kicked) {
-		tabClear(tag);
-	} else {
-		tabRemove(tag, kick);
-	}
-
-	if (mesg) {
-		urlScan(tag, mesg);
-		uiFmt(
-			tag, (kicked ? UIHot : UICold),
-			"\3%d%s\3 kicks \3%d%s\3 out of \3%d%s\3, \"%s\"",
-			colorGen(user), nick,
-			colorGen(kick), kick,
-			colorGen(chan), chan,
-			dequote(mesg)
-		);
-		logFmt(
-			tag, NULL,
-			"%s kicks %s out of %s, \"%s\"", nick, kick, chan, dequote(mesg)
-		);
-	} else {
-		uiFmt(
-			tag, (kicked ? UIHot : UICold),
-			"\3%d%s\3 kicks \3%d%s\3 out of \3%d%s\3",
-			colorGen(user), nick,
-			colorGen(kick), kick,
-			colorGen(chan), chan
-		);
-		logFmt(tag, NULL, "%s kicks %s out of %s", nick, kick, chan);
-	}
-}
-
-static void handleQuit(char *prefix, char *params) {
-	char *nick, *user, *mesg;
-	parse(prefix, &nick, &user, NULL, params, 0, 1, &mesg);
-
-	struct Tag tag;
-	while (TagNone.id != (tag = tabTag(nick)).id) {
-		tabRemove(tag, nick);
-
-		if (mesg) {
-			urlScan(tag, mesg);
-			uiFmt(
-				tag, UICold,
-				"\3%d%s\3 leaves, \"%s\"",
-				colorGen(user), nick, dequote(mesg)
-			);
-			logFmt(tag, NULL, "%s leaves, \"%s\"", nick, dequote(mesg));
-		} else {
-			uiFmt(tag, UICold, "\3%d%s\3 leaves", colorGen(user), nick);
-			logFmt(tag, NULL, "%s leaves", nick);
-		}
-	}
-}
-
-static void handleReplyTopic(char *prefix, char *params) {
-	char *chan, *topic;
-	parse(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &chan, &topic);
-	struct Tag tag = colorTag(tagFor(chan), chan);
-
-	urlScan(tag, topic);
-	uiFmt(
-		tag, UICold,
-		"The sign in \3%d%s\3 reads, \"%s\"",
-		colorGen(chan), chan, topic
-	);
-	logFmt(tag, NULL, "The sign in %s reads, \"%s\"", chan, topic);
-}
-
-static void handleTopic(char *prefix, char *params) {
-	char *nick, *user, *chan, *topic;
-	parse(prefix, &nick, &user, NULL, params, 2, 0, &chan, &topic);
-	struct Tag tag = colorTag(tagFor(chan), chan);
-
-	if (strcmp(nick, self.nick)) tabTouch(tag, nick);
-
-	urlScan(tag, topic);
-	uiFmt(
-		tag, UICold,
-		"\3%d%s\3 places a new sign in \3%d%s\3, \"%s\"",
-		colorGen(user), nick, colorGen(chan), chan, topic
-	);
-	logFmt(tag, NULL, "%s places a new sign in %s, \"%s\"", nick, chan, topic);
-}
-
-static void handleReplyEndOfNames(char *prefix, char *params) {
-	char *chan;
-	parse(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &chan);
-	ircFmt("WHO %s\r\n", chan);
-}
-
-static struct {
-	char buf[4096];
-	size_t len;
-} who;
-
-static void handleReplyWho(char *prefix, char *params) {
-	char *chan, *user, *nick;
-	parse(
-		prefix, NULL, NULL, NULL,
-		params, 6, 0, NULL, &chan, &user, NULL, NULL, &nick
-	);
-	struct Tag tag = colorTag(tagFor(chan), chan);
-
-	tabAdd(tag, nick);
-
-	size_t cap = sizeof(who.buf) - who.len;
-	int len = snprintf(
-		&who.buf[who.len], cap,
-		"%s\3%d%s\3",
-		(who.len ? ", " : ""), colorGen(user), nick
-	);
-	if ((size_t)len < cap) who.len += len;
-}
-
-static void handleReplyEndOfWho(char *prefix, char *params) {
-	char *chan;
-	parse(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &chan);
-	struct Tag tag = colorTag(tagFor(chan), chan);
-
-	uiFmt(
-		tag, UICold,
-		"In \3%d%s\3 are %s",
-		colorGen(chan), chan, who.buf
-	);
-	who.len = 0;
-}
-
-static void handleNick(char *prefix, char *params) {
-	char *prev, *user, *next;
-	parse(prefix, &prev, &user, NULL, params, 1, 0, &next);
-
-	if (!strcmp(prev, self.nick)) {
-		free(self.nick);
-		self.nick = strdup(next);
-		if (!self.nick) err(EX_OSERR, "strdup");
-		uiPrompt(true);
-	}
-
-	struct Tag tag;
-	while (TagNone.id != (tag = tabTag(prev)).id) {
-		tabReplace(tag, prev, next);
-
-		uiFmt(
-			tag, UICold,
-			"\3%d%s\3 is now known as \3%d%s\3",
-			colorGen(user), prev, colorGen(user), next
-		);
-		logFmt(tag, NULL, "%s is now known as %s", prev, next);
-	}
-}
-
-static void handleCTCP(struct Tag tag, char *nick, char *user, char *mesg) {
-	mesg = &mesg[1];
-	char *ctcp = strsep(&mesg, " ");
-	char *params = strsep(&mesg, "\1");
-	if (strcmp(ctcp, "ACTION")) return;
-
-	if (strcmp(nick, self.nick)) tabTouch(tag, nick);
-
-	urlScan(tag, params);
-	bool ping = strcmp(nick, self.nick) && isPing(params);
-	uiFmt(
-		tag, (ping ? UIHot : UIWarm),
-		"%c\3%d* %s\17 %s",
-		ping["\17\26"], colorGen(user), nick, params
-	);
-	logFmt(tag, NULL, "* %s %s", nick, params);
-}
-
-static void handlePrivmsg(char *prefix, char *params) {
-	char *nick, *user, *chan, *mesg;
-	parse(prefix, &nick, &user, NULL, params, 2, 0, &chan, &mesg);
-	bool direct = !strcmp(chan, self.nick);
-	struct Tag tag = tagFor(direct ? nick : chan);
-	colorTag(tag, direct ? user : chan);
-	if (mesg[0] == '\1') {
-		handleCTCP(tag, nick, user, mesg);
-		return;
-	}
-
-	bool me = !strcmp(nick, self.nick);
-	if (!me) tabTouch(tag, nick);
-
-	urlScan(tag, mesg);
-	bool hot = !me && (direct || isPing(mesg));
-	bool ping = !me && isPing(mesg);
-	uiFmt(
-		tag, (hot ? UIHot : UIWarm),
-		"%c%c\3%d<%s>%c %s",
-		(me ? IRCUnderline : IRCColor), (ping ? IRCReverse : IRCColor),
-		colorGen(user), nick, IRCReset, mesg
-	);
-	logFmt(tag, NULL, "<%s> %s", nick, mesg);
-}
-
-static void handleNotice(char *prefix, char *params) {
-	char *nick, *user, *chan, *mesg;
-	parse(prefix, &nick, &user, NULL, params, 2, 0, &chan, &mesg);
-	bool direct = !strcmp(chan, self.nick);
-	struct Tag tag = TagStatus;
-	if (user) {
-		tag = tagFor(direct ? nick : chan);
-		colorTag(tag, direct ? user : chan);
-	}
-
-	if (strcmp(nick, self.nick)) tabTouch(tag, nick);
-
-	urlScan(tag, mesg);
-	bool ping = strcmp(nick, self.nick) && isPing(mesg);
-	uiFmt(
-		tag, (ping ? UIHot : UIWarm),
-		"%c\3%d-%s-\17 %s",
-		ping["\17\26"], colorGen(user), nick, mesg
-	);
-	logFmt(tag, NULL, "-%s- %s", nick, mesg);
-}
-
-static const struct {
-	const char *command;
-	Handler *handler;
-} Handlers[] = {
-	{ "001", handleReplyWelcome },
-	{ "311", handleReplyWhoisUser },
-	{ "312", handleReplyWhoisServer },
-	{ "313", handleReplyWhoisOperator },
-	{ "315", handleReplyEndOfWho },
-	{ "317", handleReplyWhoisIdle },
-	{ "319", handleReplyWhoisChannels },
-	{ "322", handleReplyList },
-	{ "323", handleReplyListEnd },
-	{ "332", handleReplyTopic },
-	{ "352", handleReplyWho },
-	{ "366", handleReplyEndOfNames },
-	{ "372", handleReplyMOTD },
-	{ "375", handleReplyMOTD },
-	{ "401", handleErrorNoSuchNick },
-	{ "432", handleErrorErroneousNickname },
-	{ "433", handleErrorErroneousNickname },
-	{ "CAP", handleCap },
-	{ "ERROR", handleError },
-	{ "JOIN", handleJoin },
-	{ "KICK", handleKick },
-	{ "NICK", handleNick },
-	{ "NOTICE", handleNotice },
-	{ "PART", handlePart },
-	{ "PING", handlePing },
-	{ "PRIVMSG", handlePrivmsg },
-	{ "QUIT", handleQuit },
-	{ "TOPIC", handleTopic },
-};
-static const size_t HandlersLen = sizeof(Handlers) / sizeof(Handlers[0]);
-
-void handle(char *line) {
-	char *prefix = NULL;
-	if (line[0] == ':') {
-		prefix = strsep(&line, " ") + 1;
-		if (!line) errx(EX_PROTOCOL, "unexpected eol");
-	}
-	char *command = strsep(&line, " ");
-	for (size_t i = 0; i < HandlersLen; ++i) {
-		if (strcmp(command, Handlers[i].command)) continue;
-		Handlers[i].handler(prefix, line);
-		break;
-	}
-}
diff --git a/input.c b/input.c
deleted file mode 100644
index 8be8eaf..0000000
--- a/input.c
+++ /dev/null
@@ -1,276 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <ctype.h>
-#include <err.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sysexits.h>
-
-#include "chat.h"
-
-static void privmsg(struct Tag tag, bool action, const char *mesg) {
-	char *line;
-	int send;
-	asprintf(
-		&line, ":%s!%s %nPRIVMSG %s :%s%s%s",
-		self.nick, self.user, &send, tag.name,
-		(action ? "\1ACTION " : ""), mesg, (action ? "\1" : "")
-	);
-	if (!line) err(EX_OSERR, "asprintf");
-	ircFmt("%s\r\n", &line[send]);
-	handle(line);
-	free(line);
-}
-
-typedef void Handler(struct Tag tag, char *params);
-
-static void inputJoin(struct Tag tag, char *params) {
-	char *chan = strsep(&params, " ");
-	char *key = strsep(&params, " ");
-	if (key) {
-		ircFmt("JOIN %s %s\r\n", chan, key);
-	} else {
-		ircFmt("JOIN %s\r\n", chan ? chan : tag.name);
-	}
-}
-
-static void inputList(struct Tag tag, char *params) {
-	(void)tag;
-	char *chan = strsep(&params, " ");
-	if (chan) {
-		ircFmt("LIST %s\r\n", chan);
-	} else {
-		ircFmt("LIST\r\n");
-	}
-}
-
-static void inputMe(struct Tag tag, char *params) {
-	privmsg(tag, true, params ? params : "");
-}
-
-static void inputNick(struct Tag tag, char *params) {
-	char *nick = strsep(&params, " ");
-	if (!nick) {
-		uiLog(tag, UIHot, L"/nick requires a name");
-		return;
-	}
-	ircFmt("NICK %s\r\n", nick);
-}
-
-static void inputPart(struct Tag tag, char *params) {
-	ircFmt("PART %s :%s\r\n", tag.name, params ? params : "Goodbye");
-}
-
-static void inputQuery(struct Tag tag, char *params) {
-	char *nick = strsep(&params, " ");
-	if (!nick) {
-		uiLog(tag, UIHot, L"/query requires a nick");
-		return;
-	}
-	tabTouch(TagNone, nick);
-	uiShowTag(tagFor(nick));
-	logReplay(tagFor(nick));
-}
-
-static void inputQuit(struct Tag tag, char *params) {
-	(void)tag;
-	ircQuit(params ? params : "Goodbye");
-}
-
-static void inputQuote(struct Tag tag, char *params) {
-	(void)tag;
-	if (params) ircFmt("%s\r\n", params);
-}
-
-static void inputTopic(struct Tag tag, char *params) {
-	if (params) {
-		ircFmt("TOPIC %s :%s\r\n", tag.name, params);
-	} else {
-		ircFmt("TOPIC %s\r\n", tag.name);
-	}
-}
-
-static void inputWho(struct Tag tag, char *params) {
-	(void)params;
-	ircFmt("WHO :%s\r\n", tag.name);
-}
-
-static void inputWhois(struct Tag tag, char *params) {
-	char *nick = strsep(&params, " ");
-	if (!nick) {
-		uiLog(tag, UIHot, L"/whois requires a nick");
-		return;
-	}
-	ircFmt("WHOIS %s\r\n", nick);
-}
-
-static void inputZNC(struct Tag tag, char *params) {
-	(void)tag;
-	ircFmt("ZNC %s\r\n", params ? params : "");
-}
-
-static void inputClose(struct Tag tag, char *params) {
-	(void)params;
-	uiCloseTag(tag);
-	tabRemove(TagNone, tag.name);
-}
-
-static void inputMan(struct Tag tag, char *params) {
-	(void)tag;
-	(void)params;
-	eventWait((const char *[]) { "man", "1", "catgirl", NULL });
-}
-
-static void inputMove(struct Tag tag, char *params) {
-	char *num = strsep(&params, " ");
-	if (!num) {
-		uiLog(tag, UIHot, L"/move requires a number");
-		return;
-	}
-	uiMoveTag(tag, strtol(num, NULL, 0), num[0] == '+' || num[0] == '-');
-}
-
-static void inputOpen(struct Tag tag, char *params) {
-	if (params && !isdigit(params[0])) {
-		urlOpenMatch(tag, params);
-	} else {
-		size_t at = (params ? strtoul(strsep(&params, "-,"), NULL, 0) : 1);
-		size_t to = (params ? strtoul(params, NULL, 0) : at);
-		urlOpenRange(tag, at - 1, to);
-	}
-}
-
-static void inputRaw(struct Tag tag, char *params) {
-	(void)tag;
-	(void)params;
-	self.raw ^= true;
-	uiFmt(
-		TagRaw, UIWarm, "\3%d%s\3 %s raw mode!",
-		colorGen(self.user), self.nick, (self.raw ? "engages" : "disengages")
-	);
-}
-
-static void inputURL(struct Tag tag, char *params) {
-	(void)params;
-	urlList(tag);
-}
-
-static void inputWindow(struct Tag tag, char *params) {
-	char *word = strsep(&params, " ");
-	if (!word) {
-		uiLog(tag, UIHot, L"/window requires a name or number");
-		return;
-	}
-	bool relative = (word[0] == '+' || word[0] == '-');
-	char *trail;
-	int num = strtol(word, &trail, 0);
-	if (!trail[0]) {
-		uiShowNum(num, relative);
-	} else {
-		struct Tag name = tagFind(word);
-		if (name.id != TagNone.id) {
-			uiShowTag(name);
-		} else {
-			uiFmt(tag, UIHot, "No window for %s", word);
-		}
-	}
-}
-
-static const struct {
-	const char *command;
-	Handler *handler;
-	bool limit;
-} Commands[] = {
-	{ "/close", .handler = inputClose },
-	{ "/help", .handler = inputMan },
-	{ "/join", .handler = inputJoin, .limit = true },
-	{ "/list", .handler = inputList },
-	{ "/man", .handler = inputMan },
-	{ "/me", .handler = inputMe },
-	{ "/move", .handler = inputMove },
-	{ "/names", .handler = inputWho },
-	{ "/nick", .handler = inputNick },
-	{ "/open", .handler = inputOpen },
-	{ "/part", .handler = inputPart },
-	{ "/query", .handler = inputQuery, .limit = true },
-	{ "/quit", .handler = inputQuit },
-	{ "/quote", .handler = inputQuote, .limit = true },
-	{ "/raw", .handler = inputRaw, .limit = true },
-	{ "/topic", .handler = inputTopic },
-	{ "/url", .handler = inputURL },
-	{ "/who", .handler = inputWho },
-	{ "/whois", .handler = inputWhois },
-	{ "/window", .handler = inputWindow },
-	{ "/znc", .handler = inputZNC },
-};
-static const size_t CommandsLen = sizeof(Commands) / sizeof(Commands[0]);
-
-void inputTab(void) {
-	for (size_t i = 0; i < CommandsLen; ++i) {
-		tabTouch(TagNone, Commands[i].command);
-	}
-}
-
-void input(struct Tag tag, char *input) {
-	bool slash = (input[0] == '/');
-	if (slash) {
-		char *space = strchr(&input[1], ' ');
-		char *extra = strchr(&input[1], '/');
-		if (extra && (!space || extra < space)) slash = false;
-	}
-
-	if (!slash) {
-		if (tag.id == TagRaw.id) {
-			ircFmt("%s\r\n", input);
-		} else if (tag.id != TagStatus.id) {
-			privmsg(tag, false, input);
-		}
-		return;
-	}
-
-	char *word = strsep(&input, " ");
-	if (input && !input[0]) input = NULL;
-
-	char *trail;
-	strtol(&word[1], &trail, 0);
-	if (!trail[0]) {
-		inputWindow(tag, &word[1]);
-		return;
-	}
-
-	const char *command = word;
-	const char *uniq = tabNext(TagNone, command);
-	if (uniq && tabNext(TagNone, command) == uniq) {
-		command = uniq;
-		tabAccept();
-	} else {
-		tabReject();
-	}
-
-	for (size_t i = 0; i < CommandsLen; ++i) {
-		if (strcasecmp(command, Commands[i].command)) continue;
-		if (self.limit && Commands[i].limit) {
-			uiFmt(tag, UIHot, "%s isn't available in restricted mode", command);
-			return;
-		}
-		Commands[i].handler(tag, input);
-		return;
-	}
-	uiFmt(tag, UIHot, "%s isn't a recognized command", command);
-}
diff --git a/irc.c b/irc.c
deleted file mode 100644
index 56a5dc0..0000000
--- a/irc.c
+++ /dev/null
@@ -1,147 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <err.h>
-#include <fcntl.h>
-#include <netdb.h>
-#include <netinet/in.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/socket.h>
-#include <sysexits.h>
-#include <tls.h>
-#include <unistd.h>
-
-#include "chat.h"
-
-static struct tls *client;
-
-int ircConnect(void) {
-	int error;
-
-	struct tls_config *config = tls_config_new();
-	error = tls_config_set_ciphers(config, "compat");
-	if (error) errx(EX_SOFTWARE, "tls_config");
-
-	client = tls_client();
-	if (!client) errx(EX_SOFTWARE, "tls_client");
-
-	error = tls_configure(client, config);
-	if (self.insecure) {
-		tls_config_insecure_noverifycert(config);
-		tls_config_insecure_noverifyname(config);
-	}
-	if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client));
-	tls_config_free(config);
-
-	struct addrinfo *head;
-	struct addrinfo hints = {
-		.ai_family = AF_UNSPEC,
-		.ai_socktype = SOCK_STREAM,
-		.ai_protocol = IPPROTO_TCP,
-	};
-	error = getaddrinfo(self.host, self.port, &hints, &head);
-	if (error) errx(EX_NOHOST, "getaddrinfo: %s", gai_strerror(error));
-
-	int sock = -1;
-	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");
-		error = connect(sock, ai->ai_addr, ai->ai_addrlen);
-		if (!error) break;
-
-		close(sock);
-		sock = -1;
-	}
-	if (sock < 0) err(EX_UNAVAILABLE, "connect");
-	freeaddrinfo(head);
-
-	error = fcntl(sock, F_SETFD, FD_CLOEXEC);
-	if (error) err(EX_IOERR, "fcntl");
-
-	error = tls_connect_socket(client, sock, self.host);
-	if (error) errx(EX_PROTOCOL, "tls_connect: %s", tls_error(client));
-
-	if (self.auth) ircFmt("CAP REQ :sasl\r\n");
-	if (self.pass) ircFmt("PASS :%s\r\n", self.pass);
-	ircFmt("NICK %s\r\n", self.nick);
-	ircFmt("USER %s 0 * :%s\r\n", self.user, self.real);
-
-	return sock;
-}
-
-void ircWrite(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));
-		ptr += ret;
-		len -= ret;
-	}
-}
-
-void ircFmt(const char *format, ...) {
-	char *buf;
-	va_list ap;
-	va_start(ap, format);
-	int len =  vasprintf(&buf, format, ap);
-	va_end(ap);
-	if (!buf) err(EX_OSERR, "vasprintf");
-	if (self.raw) {
-		if (!isatty(STDERR_FILENO)) fprintf(stderr, "<<< %.*s\n", len - 2, buf);
-		uiFmt(TagRaw, UICold, "\3%d<<<\3 %.*s", IRCWhite, len - 2, buf);
-	}
-	ircWrite(buf, len);
-	free(buf);
-}
-
-void ircQuit(const char *mesg) {
-	ircFmt("QUIT :%s\r\n", mesg);
-	self.quit = true;
-}
-
-void ircRead(void) {
-	static char buf[4096];
-	static size_t len;
-
-	ssize_t read;
-retry:
-	read = tls_read(client, &buf[len], sizeof(buf) - len);
-	if (read == TLS_WANT_POLLIN || read == TLS_WANT_POLLOUT) goto retry;
-	if (read < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client));
-	if (!read) {
-		if (!self.quit) errx(EX_PROTOCOL, "unexpected eof");
-		uiExit(EX_OK);
-	}
-	len += read;
-
-	char *crlf;
-	char *line = buf;
-	while (NULL != (crlf = memmem(line, &buf[len] - line, "\r\n", 2))) {
-		crlf[0] = '\0';
-		if (self.raw) {
-			if (!isatty(STDERR_FILENO)) fprintf(stderr, ">>> %s\n", line);
-			uiFmt(TagRaw, UICold, "\3%d>>>\3 %s", IRCGray, line);
-		}
-		handle(line);
-		line = &crlf[2];
-	}
-
-	len -= line - buf;
-	memmove(buf, line, len);
-}
diff --git a/log.c b/log.c
deleted file mode 100644
index 2681bac..0000000
--- a/log.c
+++ /dev/null
@@ -1,157 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <err.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <sysexits.h>
-#include <time.h>
-
-#include "chat.h"
-
-static int logRoot = -1;
-
-static struct Log {
-	int dir;
-	int year;
-	int month;
-	int day;
-	FILE *file;
-} logs[TagsLen];
-
-void logOpen(const char *path) {
-	logRoot = open(path, O_RDONLY | O_CLOEXEC);
-	if (logRoot < 0) err(EX_CANTCREAT, "%s", path);
-}
-
-static void sanitize(char *name) {
-	for (; name[0]; ++name) {
-		if (name[0] == '/') name[0] = '_';
-	}
-}
-
-static FILE *logFile(struct Tag tag, const struct tm *time) {
-	struct Log *log = &logs[tag.id];
-	if (
-		log->file
-		&& log->year == time->tm_year
-		&& log->month == time->tm_mon
-		&& log->day == time->tm_mday
-	) return log->file;
-
-	if (log->file) {
-		fclose(log->file);
-
-	} else {
-		char *name = strdup(tag.name);
-		if (!name) err(EX_OSERR, "strdup");
-		sanitize(name);
-
-		int error = mkdirat(logRoot, name, 0700);
-		if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", name);
-
-		log->dir = openat(logRoot, name, O_RDONLY | O_CLOEXEC);
-		if (log->dir < 0) err(EX_CANTCREAT, "%s", name);
-
-		free(name);
-	}
-
-	log->year = time->tm_year;
-	log->month = time->tm_mon;
-	log->day = time->tm_mday;
-
-	char path[sizeof("YYYY-MM-DD.log")];
-	strftime(path, sizeof(path), "%F.log", time);
-	int fd = openat(
-		log->dir, path, O_RDWR | O_APPEND | O_CREAT | O_CLOEXEC, 0600
-	);
-	if (fd < 0) err(EX_CANTCREAT, "%s/%s", tag.name, path);
-
-	log->file = fdopen(fd, "a+");
-	if (!log->file) err(EX_CANTCREAT, "%s/%s", tag.name, path);
-	setlinebuf(log->file);
-
-	return log->file;
-}
-
-enum { StampLen = sizeof("YYYY-MM-DDThh:mm:ss+hhmm") - 1 };
-
-void logFmt(struct Tag tag, const time_t *ts, const char *format, ...) {
-	if (logRoot < 0) return;
-
-	time_t t;
-	if (!ts) {
-		t = time(NULL);
-		ts = &t;
-	}
-
-	struct tm *time = localtime(ts);
-	if (!time) err(EX_SOFTWARE, "localtime");
-
-	FILE *file = logFile(tag, time);
-
-	char stamp[StampLen + 1];
-	strftime(stamp, sizeof(stamp), "%FT%T%z", time);
-	fprintf(file, "[%s] ", stamp);
-	if (ferror(file)) err(EX_IOERR, "%s", tag.name);
-
-	va_list ap;
-	va_start(ap, format);
-	vfprintf(file, format, ap);
-	va_end(ap);
-	if (ferror(file)) err(EX_IOERR, "%s", tag.name);
-
-	fprintf(file, "\n");
-	if (ferror(file)) err(EX_IOERR, "%s", tag.name);
-}
-
-static void logRead(struct Tag tag, bool replay) {
-	if (logRoot < 0) return;
-
-	time_t t = time(NULL);
-	struct tm *time = localtime(&t);
-	if (!time) err(EX_SOFTWARE, "localtime");
-
-	FILE *file = logFile(tag, time);
-	rewind(file);
-
-	char *line = NULL;
-	size_t cap = 0;
-	ssize_t len;
-	while (0 < (len = getline(&line, &cap, file))) {
-		if (replay) {
-			if (len < 1 + StampLen + 2 + 1) continue;
-			line[len - 1] = '\0';
-			uiFmt(tag, UICold, "\3%d%s", IRCGray, &line[1 + StampLen + 2]);
-		} else {
-			printf("%s", line);
-		}
-	}
-	if (ferror(file)) err(EX_IOERR, "%s", tag.name);
-	free(line);
-}
-
-void logList(struct Tag tag) {
-	logRead(tag, false);
-}
-
-void logReplay(struct Tag tag) {
-	logRead(tag, true);
-}
diff --git a/man.sh b/man.sh
deleted file mode 100644
index 9d686f9..0000000
--- a/man.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-exec mandoc /usr/share/man/man1/catgirl.1 | LESSSECURE=1 less
diff --git a/pls.c b/pls.c
deleted file mode 100644
index b724033..0000000
--- a/pls.c
+++ /dev/null
@@ -1,186 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <errno.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <wchar.h>
-
-#include "chat.h"
-
-wchar_t *wcsnchr(const wchar_t *wcs, size_t len, wchar_t chr) {
-	len = wcsnlen(wcs, len);
-	for (size_t i = 0; i < len; ++i) {
-		if (wcs[i] == chr) return (wchar_t *)&wcs[i];
-	}
-	return NULL;
-}
-
-wchar_t *wcsnrchr(const wchar_t *wcs, size_t len, wchar_t chr) {
-	len = wcsnlen(wcs, len);
-	for (size_t i = len - 1; i < len; --i) {
-		if (wcs[i] == chr) return (wchar_t *)&wcs[i];
-	}
-	return NULL;
-}
-
-wchar_t *ambstowcs(const char *src) {
-	size_t len = mbsrtowcs(NULL, &src, 0, NULL);
-	if (len == (size_t)-1) return NULL;
-
-	wchar_t *dst = malloc(sizeof(*dst) * (1 + len));
-	if (!dst) return NULL;
-
-	len = mbsrtowcs(dst, &src, len, NULL);
-	if (len == (size_t)-1) {
-		free(dst);
-		return NULL;
-	}
-
-	dst[len] = L'\0';
-	return dst;
-}
-
-char *awcstombs(const wchar_t *src) {
-	size_t len = wcsrtombs(NULL, &src, 0, NULL);
-	if (len == (size_t)-1) return NULL;
-
-	char *dst = malloc(sizeof(*dst) * (1 + len));
-	if (!dst) return NULL;
-
-	len = wcsrtombs(dst, &src, len, NULL);
-	if (len == (size_t)-1) {
-		free(dst);
-		return NULL;
-	}
-
-	dst[len] = '\0';
-	return dst;
-}
-
-char *awcsntombs(const wchar_t *src, size_t nwc) {
-	size_t len = wcsnrtombs(NULL, &src, nwc, 0, NULL);
-	if (len == (size_t)-1) return NULL;
-
-	char *dst = malloc(sizeof(*dst) * (1 + len));
-	if (!dst) return NULL;
-
-	len = wcsnrtombs(dst, &src, nwc, len, NULL);
-	if (len == (size_t)-1) {
-		free(dst);
-		return NULL;
-	}
-
-	dst[len] = '\0';
-	return dst;
-}
-
-// From <https://en.cppreference.com/w/c/io/fwprintf#Notes>:
-//
-// While narrow strings provide snprintf, which makes it possible to determine
-// the required output buffer size, there is no equivalent for wide strings
-// (until C11's snwprintf_s), and in order to determine the buffer size, the
-// program may need to call swprintf, check the result value, and reallocate a
-// larger buffer, trying again until successful.
-//
-// snwprintf_s, unlike swprintf_s, will truncate the result to fit within the
-// array pointed to by buffer, even though truncation is treated as an error by
-// most bounds-checked functions.
-int vaswprintf(wchar_t **ret, const wchar_t *format, va_list ap) {
-	*ret = NULL;
-
-	for (size_t cap = 2 * wcslen(format);; cap *= 2) {
-		wchar_t *buf = realloc(*ret, sizeof(*buf) * (1 + cap));
-		if (!buf) goto fail;
-		*ret = buf;
-
-		va_list _ap;
-		va_copy(_ap, ap);
-		errno = EOVERFLOW; // vswprintf may not set errno.
-		int len = vswprintf(*ret, 1 + cap, format, _ap);
-		va_end(_ap);
-
-		if (len >= 0) return len;
-		if (errno != EOVERFLOW) goto fail;
-	}
-
-fail:
-	free(*ret);
-	*ret = NULL;
-	return -1;
-}
-
-int aswprintf(wchar_t **ret, const wchar_t *format, ...) {
-	va_list ap;
-	va_start(ap, format);
-	int n = vaswprintf(ret, format, ap);
-	va_end(ap);
-	return n;
-}
-
-static const char Base64[64] = {
-	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
-};
-
-size_t base64Size(size_t len) {
-	return 1 + (len + 2) / 3 * 4;
-}
-
-void base64(char *dst, const byte *src, size_t len) {
-	size_t i = 0;
-	while (len > 2) {
-		dst[i++] = Base64[0x3F & (src[0] >> 2)];
-		dst[i++] = Base64[0x3F & (src[0] << 4 | src[1] >> 4)];
-		dst[i++] = Base64[0x3F & (src[1] << 2 | src[2] >> 6)];
-		dst[i++] = Base64[0x3F & src[2]];
-		src += 3;
-		len -= 3;
-	}
-	if (len) {
-		dst[i++] = Base64[0x3F & (src[0] >> 2)];
-		if (len > 1) {
-			dst[i++] = Base64[0x3F & (src[0] << 4 | src[1] >> 4)];
-			dst[i++] = Base64[0x3F & (src[1] << 2)];
-		} else {
-			dst[i++] = Base64[0x3F & (src[0] << 4)];
-			dst[i++] = '=';
-		}
-		dst[i++] = '=';
-	}
-	dst[i] = '\0';
-}
-
-#ifdef TEST
-#include <assert.h>
-#include <string.h>
-
-int main() {
-	assert(5 == base64Size(1));
-	assert(5 == base64Size(2));
-	assert(5 == base64Size(3));
-	assert(9 == base64Size(4));
-
-	char b64[base64Size(3)];
-	assert((base64(b64, (byte *)"cat", 3), !strcmp("Y2F0", b64)));
-	assert((base64(b64, (byte *)"ca", 2), !strcmp("Y2E=", b64)));
-	assert((base64(b64, (byte *)"c", 1), !strcmp("Yw==", b64)));
-
-	assert((base64(b64, (byte *)"\xFF\x00\xFF", 3), !strcmp("/wD/", b64)));
-	assert((base64(b64, (byte *)"\x00\xFF\x00", 3), !strcmp("AP8A", b64)));
-}
-
-#endif
diff --git a/sandman.1 b/sandman.1
deleted file mode 100644
index bd68874..0000000
--- a/sandman.1
+++ /dev/null
@@ -1,30 +0,0 @@
-.Dd July 2, 2019
-.Dt SANDMAN 1
-.Os
-.
-.Sh NAME
-.Nm sandman
-.Nd signal sleep
-.
-.Sh SYNOPSIS
-.Nm
-.Ar command ...
-.
-.Sh DESCRIPTION
-.Nm
-is a utility for Darwin systems.
-It runs the
-.Ar command
-as a child process.
-When the system goes to sleep,
-the process is sent
-.Dv SIGHUP .
-When the system wakes up,
-the process is restarted.
-.
-.Sh EXIT STATUS
-.Nm
-exits with the exit status of the child process.
-.
-.Sh SEE ALSO
-.Xr signal 3
diff --git a/sandman.m b/sandman.m
deleted file mode 100644
index 94c7d1a..0000000
--- a/sandman.m
+++ /dev/null
@@ -1,88 +0,0 @@
-/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#import <Cocoa/Cocoa.h>
-#import <err.h>
-#import <signal.h>
-#import <stdlib.h>
-#import <sysexits.h>
-#import <unistd.h>
-
-static volatile sig_atomic_t sleeping;
-
-static void sigchld(int sig) {
-	(void)sig;
-	int status;
-	pid_t pid = wait(&status);
-	if (pid < 0) _exit(EX_OSERR);
-	if (WIFSIGNALED(status) && WTERMSIG(status) != SIGHUP) {
-		_exit(128 + WTERMSIG(status));
-	} else if (!sleeping) {
-		_exit(WEXITSTATUS(status));
-	}
-}
-
-static pid_t spawn(char *argv[]) {
-	pid_t pid = fork();
-	if (pid < 0) err(EX_OSERR, "fork");
-	if (pid) return pid;
-	execvp(argv[0], argv);
-	err(EX_NOINPUT, "%s", argv[0]);
-}
-
-static pid_t pid;
-
-int main(int argc, char *argv[]) {
-	if (argc < 2) return EX_USAGE;
-
-	sigset_t mask;
-	sigemptyset(&mask);
-	struct sigaction action = {
-		.sa_handler = sigchld,
-		.sa_mask = mask,
-		.sa_flags = SA_NOCLDSTOP | SA_RESTART,
-	};
-	sigaction(SIGCHLD, &action, NULL);
-
-	pid = spawn(&argv[1]);
-
-	[
-		[[NSWorkspace sharedWorkspace] notificationCenter]
-		addObserverForName: NSWorkspaceWillSleepNotification
-		object: nil
-		queue: nil
-		usingBlock: ^(NSNotification *note) {
-			(void)note;
-			sleeping = 1;
-			int error = kill(pid, SIGHUP);
-			if (error) err(EX_UNAVAILABLE, "kill %d", pid);
-		}
-	];
-
-	[
-		[[NSWorkspace sharedWorkspace] notificationCenter]
-		addObserverForName: NSWorkspaceDidWakeNotification
-		object: nil
-		queue: nil
-		usingBlock: ^(NSNotification *note) {
-			(void)note;
-			sleeping = 0;
-			pid = spawn(&argv[1]);
-		}
-	];
-
-	[[NSApplication sharedApplication] run];
-}
diff --git a/sshd_config b/sshd_config
deleted file mode 100644
index 47b5a33..0000000
--- a/sshd_config
+++ /dev/null
@@ -1,13 +0,0 @@
-UsePAM no
-
-Match User chat
-	PasswordAuthentication yes
-	PermitEmptyPasswords yes
-	ChrootDirectory /home/chat
-	ForceCommand catgirl
-
-	AllowAgentForwarding no
-	AllowTcpForwarding no
-	AllowStreamLocalForwarding no
-	MaxSessions 1
-	X11Forwarding no
diff --git a/tab.c b/tab.c
deleted file mode 100644
index a17218d..0000000
--- a/tab.c
+++ /dev/null
@@ -1,148 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <err.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sysexits.h>
-
-#include "chat.h"
-
-static struct Entry {
-	struct Tag tag;
-	char *word;
-	struct Entry *prev;
-	struct Entry *next;
-} *head;
-
-static void prepend(struct Entry *entry) {
-	entry->prev = NULL;
-	entry->next = head;
-	if (head) head->prev = entry;
-	head = entry;
-}
-
-static void unlink(struct Entry *entry) {
-	if (entry->prev) entry->prev->next = entry->next;
-	if (entry->next) entry->next->prev = entry->prev;
-	if (head == entry) head = entry->next;
-}
-
-static void touch(struct Entry *entry) {
-	if (head == entry) return;
-	unlink(entry);
-	prepend(entry);
-}
-
-static struct Entry *find(struct Tag tag, const char *word) {
-	for (struct Entry *entry = head; entry; entry = entry->next) {
-		if (entry->tag.id != tag.id) continue;
-		if (strcmp(entry->word, word)) continue;
-		return entry;
-	}
-	return NULL;
-}
-
-static void add(struct Tag tag, const char *word) {
-	struct Entry *entry = malloc(sizeof(*entry));
-	if (!entry) err(EX_OSERR, "malloc");
-
-	entry->tag = tag;
-	entry->word = strdup(word);
-	if (!entry->word) err(EX_OSERR, "strdup");
-
-	prepend(entry);
-}
-
-void tabTouch(struct Tag tag, const char *word) {
-	struct Entry *entry = find(tag, word);
-	if (entry) {
-		touch(entry);
-	} else {
-		add(tag, word);
-	}
-}
-
-void tabAdd(struct Tag tag, const char *word) {
-	if (!find(tag, word)) add(tag, word);
-}
-
-void tabReplace(struct Tag tag, const char *prev, const char *next) {
-	struct Entry *entry = find(tag, prev);
-	if (!entry) return;
-	touch(entry);
-	free(entry->word);
-	entry->word = strdup(next);
-	if (!entry->word) err(EX_OSERR, "strdup");
-}
-
-static struct Entry *iter;
-
-void tabRemove(struct Tag tag, const char *word) {
-	for (struct Entry *entry = head; entry; entry = entry->next) {
-		if (entry->tag.id != tag.id) continue;
-		if (strcmp(entry->word, word)) continue;
-		if (iter == entry) iter = entry->prev;
-		unlink(entry);
-		free(entry->word);
-		free(entry);
-		return;
-	}
-}
-
-void tabClear(struct Tag tag) {
-	for (struct Entry *entry = head; entry; entry = entry->next) {
-		if (entry->tag.id != tag.id) continue;
-		if (iter == entry) iter = entry->prev;
-		unlink(entry);
-		free(entry->word);
-		free(entry);
-	}
-}
-
-struct Tag tabTag(const char *word) {
-	struct Entry *start = (iter ? iter->next : head);
-	for (struct Entry *entry = start; entry; entry = entry->next) {
-		if (strcmp(entry->word, word)) continue;
-		iter = entry;
-		return entry->tag;
-	}
-	iter = NULL;
-	return TagNone;
-}
-
-const char *tabNext(struct Tag tag, const char *prefix) {
-	size_t len = strlen(prefix);
-	struct Entry *start = (iter ? iter->next : head);
-	for (struct Entry *entry = start; entry; entry = entry->next) {
-		if (entry->tag.id != TagNone.id && entry->tag.id != tag.id) continue;
-		if (strncasecmp(entry->word, prefix, len)) continue;
-		iter = entry;
-		return entry->word;
-	}
-	if (!iter) return NULL;
-	iter = NULL;
-	return tabNext(tag, prefix);
-}
-
-void tabAccept(void) {
-	if (iter) touch(iter);
-	iter = NULL;
-}
-
-void tabReject(void) {
-	iter = NULL;
-}
diff --git a/tag.c b/tag.c
deleted file mode 100644
index 5b4232e..0000000
--- a/tag.c
+++ /dev/null
@@ -1,53 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <err.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sysexits.h>
-
-#include "chat.h"
-
-static struct {
-	char *name[TagsLen];
-	size_t len;
-} tags = {
-	.name = { "<none>", "<status>", "<raw>" },
-	.len = 3,
-};
-
-const struct Tag TagNone   = { 0, "<none>" };
-const struct Tag TagStatus = { 1, "<status>" };
-const struct Tag TagRaw    = { 2, "<raw>" };
-
-struct Tag tagFind(const char *name) {
-	for (size_t id = 0; id < tags.len; ++id) {
-		if (strcmp(tags.name[id], name)) continue;
-		return (struct Tag) { id, tags.name[id] };
-	}
-	return TagNone;
-}
-
-struct Tag tagFor(const char *name) {
-	struct Tag tag = tagFind(name);
-	if (tag.id != TagNone.id) return tag;
-	if (tags.len == TagsLen) return TagStatus;
-
-	size_t id = tags.len++;
-	tags.name[id] = strdup(name);
-	if (!tags.name[id]) err(EX_OSERR, "strdup");
-	return (struct Tag) { id, tags.name[id] };
-}
diff --git a/term.c b/term.c
deleted file mode 100644
index 4b583ae..0000000
--- a/term.c
+++ /dev/null
@@ -1,100 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <termios.h>
-#include <unistd.h>
-
-#include "chat.h"
-
-static bool xterm;
-
-void termInit(void) {
-	char *term = getenv("TERM");
-	xterm = term && !strncmp(term, "xterm", 5);
-}
-
-void termNoFlow(void) {
-	struct termios attr;
-	int error = tcgetattr(STDIN_FILENO, &attr);
-	if (error) return;
-	attr.c_iflag &= ~IXON;
-	attr.c_cc[VDISCARD] = _POSIX_VDISABLE;
-	tcsetattr(STDIN_FILENO, TCSANOW, &attr);
-}
-
-void termTitle(const char *title) {
-	if (!xterm) return;
-	printf("\33]0;%s\33\\", title);
-	fflush(stdout);
-}
-
-static void privateMode(const char *mode, bool set) {
-	printf("\33[?%s%c", mode, (set ? 'h' : 'l'));
-	fflush(stdout);
-}
-
-void termMode(enum TermMode mode, bool set) {
-	switch (mode) {
-		break; case TermFocus: privateMode("1004", set);
-		break; case TermPaste: privateMode("2004", set);
-	}
-}
-
-#define T(s, i) ((s) << 8 | (i))
-
-enum { Esc = '\33' };
-
-enum TermEvent termEvent(char ch) {
-	static uint state = 0;
-	switch (T(state, ch)) {
-		case T(0, Esc): state = 1; return 0;
-		case T(1, '['): state = 2; return 0;
-		case T(2, 'I'): state = 0; return TermFocusIn;
-		case T(2, 'O'): state = 0; return TermFocusOut;
-		case T(2, '2'): state = 3; return 0;
-		case T(3, '0'): state = 4; return 0;
-		case T(4, '0'): state = 5; return 0;
-		case T(5, '~'): state = 0; return TermPasteStart;
-		case T(4, '1'): state = 6; return 0;
-		case T(6, '~'): state = 0; return TermPasteEnd;
-		default:        state = 0; return 0;
-	}
-}
-
-#ifdef TEST
-#include <assert.h>
-
-static bool testEvent(const char *str, enum TermEvent event) {
-	enum TermEvent e = TermNone;
-	for (size_t i = 0; i < strlen(str); ++i) {
-		if (e) return false;
-		e = termEvent(str[i]);
-	}
-	return (e == event);
-}
-
-int main() {
-	assert(testEvent("\33[I", TermFocusIn));
-	assert(testEvent("\33[O", TermFocusOut));
-	assert(testEvent("\33[200~", TermPasteStart));
-	assert(testEvent("\33[201~", TermPasteEnd));
-}
-
-#endif
diff --git a/ui.c b/ui.c
deleted file mode 100644
index 9cf21d3..0000000
--- a/ui.c
+++ /dev/null
@@ -1,615 +0,0 @@
-/* Copyright (C) 2018, 2019  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#define _XOPEN_SOURCE_EXTENDED
-
-#include <curses.h>
-#include <err.h>
-#include <stdarg.h>
-#include <stdbool.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sysexits.h>
-#include <wchar.h>
-#include <wctype.h>
-
-#ifndef A_ITALIC
-#define A_ITALIC A_UNDERLINE
-#endif
-
-#include "chat.h"
-#undef uiFmt
-
-#define CTRL(ch) ((ch) & 037)
-enum { Esc = L'\33', Del = L'\177' };
-
-static const int LogLines = 512;
-
-static int lastLine(void) {
-	return LINES - 1;
-}
-static int lastCol(void) {
-	return COLS - 1;
-}
-static int logHeight(void) {
-	return LINES - 2;
-}
-
-struct Window {
-	struct Tag tag;
-	WINDOW *log;
-	bool hot;
-	bool mark;
-	int scroll;
-	uint unread;
-	struct Window *prev;
-	struct Window *next;
-};
-
-static struct {
-	struct Window *active;
-	struct Window *other;
-	struct Window *head;
-	struct Window *tail;
-	struct Window *tag[TagsLen];
-} windows;
-
-static void windowAppend(struct Window *win) {
-	if (windows.tail) windows.tail->next = win;
-	win->prev = windows.tail;
-	win->next = NULL;
-	windows.tail = win;
-	if (!windows.head) windows.head = win;
-	windows.tag[win->tag.id] = win;
-}
-
-static void windowInsert(struct Window *win, struct Window *next) {
-	win->prev = next->prev;
-	win->next = next;
-	if (win->prev) win->prev->next = win;
-	win->next->prev = win;
-	if (!win->prev) windows.head = win;
-	windows.tag[win->tag.id] = win;
-}
-
-static void windowRemove(struct Window *win) {
-	windows.tag[win->tag.id] = NULL;
-	if (win->prev) win->prev->next = win->next;
-	if (win->next) win->next->prev = win->prev;
-	if (windows.head == win) windows.head = win->next;
-	if (windows.tail == win) windows.tail = win->prev;
-}
-
-static struct Window *windowFor(struct Tag tag) {
-	struct Window *win = windows.tag[tag.id];
-	if (win) {
-		win->tag = tag;
-		return win;
-	}
-
-	win = calloc(1, sizeof(*win));
-	if (!win) err(EX_OSERR, "calloc");
-
-	win->tag = tag;
-	win->mark = true;
-	win->scroll = LogLines;
-
-	win->log = newpad(LogLines, COLS);
-	wsetscrreg(win->log, 0, LogLines - 1);
-	scrollok(win->log, true);
-	wmove(win->log, LogLines - 1, 0);
-
-	windowAppend(win);
-	return win;
-}
-
-static void windowResize(struct Window *win) {
-	wresize(win->log, LogLines, COLS);
-	wmove(win->log, LogLines - 1, lastCol());
-}
-
-static void windowMark(struct Window *win) {
-	win->mark = true;
-}
-static void windowUnmark(struct Window *win) {
-	win->mark = false;
-	win->unread = 0;
-	win->hot = false;
-}
-
-static void windowShow(struct Window *win) {
-	if (windows.active) windowMark(windows.active);
-	if (win) {
-		touchwin(win->log);
-		windowUnmark(win);
-	}
-	windows.other = windows.active;
-	windows.active = win;
-}
-
-static void windowClose(struct Window *win) {
-	if (windows.active == win) windowShow(win->next ? win->next : win->prev);
-	if (windows.other == win) windows.other = NULL;
-	windowRemove(win);
-	delwin(win->log);
-	free(win);
-}
-
-static void windowScroll(struct Window *win, int lines) {
-	if (lines < 0) {
-		if (win->scroll == logHeight()) return;
-		if (win->scroll == LogLines) windowMark(win);
-		win->scroll = MAX(win->scroll + lines, logHeight());
-	} else {
-		if (win->scroll == LogLines) return;
-		win->scroll = MIN(win->scroll + lines, LogLines);
-		if (win->scroll == LogLines) windowUnmark(win);
-	}
-}
-
-static void colorInit(void) {
-	start_color();
-	use_default_colors();
-	if (COLORS < 16) {
-		for (short pair = 0; pair < 0100; ++pair) {
-			if (pair < 010) {
-				init_pair(1 + pair, pair, -1);
-			} else {
-				init_pair(1 + pair, pair & 007, (pair & 070) >> 3);
-			}
-		}
-	} else {
-		for (short pair = 0; pair < 0x100; ++pair) {
-			if (pair < 0x10) {
-				init_pair(1 + pair, pair, -1);
-			} else {
-				init_pair(1 + pair, pair & 0x0F, (pair & 0xF0) >> 4);
-			}
-		}
-	}
-}
-
-static attr_t colorAttr(short color) {
-	if (color < 0) return A_NORMAL;
-	if (COLORS < 16 && (color & 0x08)) return A_BOLD;
-	return A_NORMAL;
-}
-static short colorPair(short color) {
-	if (color < 0) return 0;
-	if (COLORS < 16) return 1 + ((color & 0x70) >> 1 | (color & 0x07));
-	return 1 + color;
-}
-
-static struct {
-	bool hide;
-	WINDOW *status;
-	WINDOW *input;
-} ui;
-
-void uiInit(void) {
-	initscr();
-	cbreak();
-	noecho();
-	termInit();
-	termNoFlow();
-	def_prog_mode();
-	colorInit();
-	ui.status = newwin(1, COLS, 0, 0);
-	ui.input = newpad(1, 512);
-	keypad(ui.input, true);
-	nodelay(ui.input, true);
-	uiShow();
-}
-
-static void uiResize(void) {
-	wresize(ui.status, 1, COLS);
-	for (struct Window *win = windows.head; win; win = win->next) {
-		windowResize(win);
-	}
-}
-
-void uiShow(void) {
-	ui.hide = false;
-	termMode(TermFocus, true);
-	uiDraw();
-}
-void uiHide(void) {
-	ui.hide = true;
-	termMode(TermFocus, false);
-	endwin();
-}
-
-void uiExit(int status) {
-	uiHide();
-	printf(
-		"This program is AGPLv3 Free Software!\n"
-		"Code is available from <" SOURCE_URL ">.\n"
-	);
-	exit(status);
-}
-
-static int _;
-void uiDraw(void) {
-	if (ui.hide) return;
-	wnoutrefresh(ui.status);
-	if (windows.active) {
-		pnoutrefresh(
-			windows.active->log,
-			windows.active->scroll - logHeight(), 0,
-			1, 0,
-			lastLine() - 1, lastCol()
-		);
-	}
-	int x;
-	getyx(ui.input, _, x);
-	pnoutrefresh(
-		ui.input,
-		0, MAX(0, x - lastCol() + 3),
-		lastLine(), 0,
-		lastLine(), lastCol()
-	);
-	doupdate();
-}
-
-static const short Colors[] = {
-	[IRCWhite]      = 8 + COLOR_WHITE,
-	[IRCBlack]      = 0 + COLOR_BLACK,
-	[IRCBlue]       = 0 + COLOR_BLUE,
-	[IRCGreen]      = 0 + COLOR_GREEN,
-	[IRCRed]        = 8 + COLOR_RED,
-	[IRCBrown]      = 0 + COLOR_RED,
-	[IRCMagenta]    = 0 + COLOR_MAGENTA,
-	[IRCOrange]     = 0 + COLOR_YELLOW,
-	[IRCYellow]     = 8 + COLOR_YELLOW,
-	[IRCLightGreen] = 8 + COLOR_GREEN,
-	[IRCCyan]       = 0 + COLOR_CYAN,
-	[IRCLightCyan]  = 8 + COLOR_CYAN,
-	[IRCLightBlue]  = 8 + COLOR_BLUE,
-	[IRCPink]       = 8 + COLOR_MAGENTA,
-	[IRCGray]       = 8 + COLOR_BLACK,
-	[IRCLightGray]  = 0 + COLOR_WHITE,
-};
-
-static void addFormat(WINDOW *win, const struct Format *format) {
-	attr_t attr = A_NORMAL;
-	if (format->bold)      attr |= A_BOLD;
-	if (format->italic)    attr |= A_ITALIC;
-	if (format->underline) attr |= A_UNDERLINE;
-	if (format->reverse)   attr |= A_REVERSE;
-
-	short color = -1;
-	if (format->fg != IRCDefault) color = Colors[format->fg];
-	if (format->bg != IRCDefault) color |= Colors[format->bg] << 4;
-
-	wattr_set(win, attr | colorAttr(color), colorPair(color), NULL);
-	waddnwstr(win, format->str, format->len);
-}
-
-static int printWidth(const wchar_t *str, size_t len) {
-	int width = 0;
-	for (size_t i = 0; i < len; ++i) {
-		if (iswprint(str[i])) width += wcwidth(str[i]);
-	}
-	return width;
-}
-
-static int addWrap(WINDOW *win, const wchar_t *str) {
-	int lines = 0;
-	struct Format format = { .str = str };
-	formatReset(&format);
-
-	while (formatParse(&format, NULL)) {
-		size_t word = 1 + wcscspn(&format.str[1], L" ");
-		if (word < format.len) format.len = word;
-
-		int x, xMax;
-		getyx(win, _, x);
-		getmaxyx(win, _, xMax);
-		if (xMax - x - 1 < printWidth(format.str, word)) {
-			if (format.str[0] == L' ') {
-				format.str++;
-				format.len--;
-			}
-			waddch(win, '\n');
-			lines++;
-		}
-		addFormat(win, &format);
-	}
-	return lines;
-}
-
-static void title(const struct Window *win) {
-	int unread;
-	char *str;
-	int len = asprintf(&str, "%s%n (%u)", win->tag.name, &unread, win->unread);
-	if (len < 0) err(EX_OSERR, "asprintf");
-	if (!win->unread) str[unread] = '\0';
-	termTitle(str);
-	free(str);
-}
-
-static void uiStatus(void) {
-	wmove(ui.status, 0, 0);
-	int num = 0;
-	for (const struct Window *win = windows.head; win; win = win->next, ++num) {
-		if (!win->unread && windows.active != win) continue;
-		if (windows.active == win) title(win);
-		int unread;
-		wchar_t *str;
-		int len = aswprintf(
-			&str, L"%c\3%d %d %s %n(\3%02d%u\3%d) ",
-			(windows.active == win ? IRCReverse : IRCReset), colorFor(win->tag),
-			num, win->tag.name,
-			&unread, (win->hot ? IRCWhite : colorFor(win->tag)), win->unread,
-			colorFor(win->tag)
-		);
-		if (len < 0) err(EX_OSERR, "aswprintf");
-		if (!win->unread) str[unread] = L'\0';
-		addWrap(ui.status, str);
-		free(str);
-	}
-	wclrtoeol(ui.status);
-}
-
-static void uiShowWindow(struct Window *win) {
-	windowShow(win);
-	uiStatus();
-	uiPrompt(false);
-}
-
-void uiShowTag(struct Tag tag) {
-	uiShowWindow(windowFor(tag));
-}
-
-static void uiShowAuto(void) {
-	struct Window *unread = NULL;
-	struct Window *hot;
-	for (hot = windows.head; hot; hot = hot->next) {
-		if (hot->hot) break;
-		if (!unread && hot->unread) unread = hot;
-	}
-	if (!hot && !unread) return;
-	uiShowWindow(hot ? hot : unread);
-}
-
-void uiShowNum(int num, bool relative) {
-	struct Window *win = (relative ? windows.active : windows.head);
-	if (num < 0) {
-		for (; win; win = win->prev) if (!num++) break;
-	} else {
-		for (; win; win = win->next) if (!num--) break;
-	}
-	if (win) uiShowWindow(win);
-}
-
-void uiMoveTag(struct Tag tag, int num, bool relative) {
-	struct Window *win = windowFor(tag);
-	windowRemove(win);
-	struct Window *ins = (relative ? win : windows.head);
-	if (num < 0) {
-		for (; ins; ins = ins->prev) if (!num++) break;
-	} else {
-		if (relative) ins = ins->next;
-		for (; ins; ins = ins->next) if (!num--) break;
-	}
-	ins ? windowInsert(win, ins) : windowAppend(win);
-	uiStatus();
-}
-
-void uiCloseTag(struct Tag tag) {
-	windowClose(windowFor(tag));
-	uiStatus();
-	uiPrompt(false);
-}
-
-static void notify(struct Tag tag, const wchar_t *str) {
-	beep();
-	if (!self.notify) return;
-
-	size_t len = 0;
-	char buf[256];
-	struct Format format = { .str = str };
-	formatReset(&format);
-	while (formatParse(&format, NULL)) {
-		int n = snprintf(
-			&buf[len], sizeof(buf) - len,
-			"%.*ls", (int)format.len, format.str
-		);
-		if (n < 0) err(EX_OSERR, "snprintf");
-		len += n;
-		if (len >= sizeof(buf)) break;
-	}
-	eventPipe((const char *[]) { "notify-send", tag.name, buf, NULL });
-}
-
-void uiLog(struct Tag tag, enum UIHeat heat, const wchar_t *str) {
-	struct Window *win = windowFor(tag);
-	int lines = 1;
-	waddch(win->log, '\n');
-	if (win->mark && heat > UICold) {
-		if (!win->unread++) {
-			lines++;
-			waddch(win->log, '\n');
-		}
-		if (heat > UIWarm) {
-			win->hot = true;
-			notify(tag, str);
-		}
-		uiStatus();
-	}
-	lines += addWrap(win->log, str);
-	if (win->scroll != LogLines) win->scroll -= lines;
-}
-
-void uiFmt(struct Tag tag, enum UIHeat heat, const wchar_t *format, ...) {
-	wchar_t *str;
-	va_list ap;
-	va_start(ap, format);
-	vaswprintf(&str, format, ap);
-	va_end(ap);
-	if (!str) err(EX_OSERR, "vaswprintf");
-	uiLog(tag, heat, str);
-	free(str);
-}
-
-static void keyCode(wchar_t code) {
-	if (code == KEY_RESIZE) uiResize();
-	struct Window *win = windows.active;
-	if (!win) return;
-	switch (code) {
-		break; case KEY_UP:        windowScroll(win, -1);
-		break; case KEY_DOWN:      windowScroll(win, +1);
-		break; case KEY_PPAGE:     windowScroll(win, -(logHeight() - 1));
-		break; case KEY_NPAGE:     windowScroll(win, +(logHeight() - 1));
-		break; case KEY_LEFT:      edit(win->tag, EditLeft, 0);
-		break; case KEY_RIGHT:     edit(win->tag, EditRight, 0);
-		break; case KEY_HOME:      edit(win->tag, EditHome, 0);
-		break; case KEY_END:       edit(win->tag, EditEnd, 0);
-		break; case KEY_DC:        edit(win->tag, EditDelete, 0);
-		break; case KEY_BACKSPACE: edit(win->tag, EditBackspace, 0);
-		break; case KEY_ENTER:     edit(win->tag, EditEnter, 0);
-		break; default: return;
-	}
-	uiStatus();
-}
-
-static void keyMeta(wchar_t ch) {
-	struct Window *win = windows.active;
-	if (ch >= L'0' && ch <= L'9') uiShowNum(ch - L'0', false);
-	if (ch == L'a') uiShowAuto();
-	if (ch == L'/' && windows.other) uiShowWindow(windows.other);
-	if (!win) return;
-	switch (ch) {
-		break; case L'b':  edit(win->tag, EditBackWord, 0);
-		break; case L'f':  edit(win->tag, EditForeWord, 0);
-		break; case L'\b': edit(win->tag, EditKillBackWord, 0);
-		break; case L'd':  edit(win->tag, EditKillForeWord, 0);
-		break; case L'l':  uiHide(); logList(win->tag);
-		break; case L'm':  uiLog(win->tag, UICold, L"");
-	}
-}
-
-static void keyChar(wchar_t ch) {
-	struct Window *win = windows.active;
-	if (ch == CTRL(L'L')) clearok(curscr, true);
-	if (!win) return;
-	switch (ch) {
-		break; case CTRL(L'N'): uiShowNum(+1, true);
-		break; case CTRL(L'P'): uiShowNum(-1, true);
-
-		break; case CTRL(L'A'): edit(win->tag, EditHome, 0);
-		break; case CTRL(L'B'): edit(win->tag, EditLeft, 0);
-		break; case CTRL(L'D'): edit(win->tag, EditDelete, 0);
-		break; case CTRL(L'E'): edit(win->tag, EditEnd, 0);
-		break; case CTRL(L'F'): edit(win->tag, EditRight, 0);
-		break; case CTRL(L'K'): edit(win->tag, EditKillEnd, 0);
-		break; case CTRL(L'U'): edit(win->tag, EditKill, 0);
-		break; case CTRL(L'W'): edit(win->tag, EditKillBackWord, 0);
-
-		break; case CTRL(L'C'): edit(win->tag, EditInsert, IRCColor);
-		break; case CTRL(L'O'): edit(win->tag, EditInsert, IRCBold);
-		break; case CTRL(L'R'): edit(win->tag, EditInsert, IRCColor);
-		break; case CTRL(L'S'): edit(win->tag, EditInsert, IRCReset);
-		break; case CTRL(L'T'): edit(win->tag, EditInsert, IRCItalic);
-		break; case CTRL(L'V'): edit(win->tag, EditInsert, IRCReverse);
-		break; case CTRL(L'_'): edit(win->tag, EditInsert, IRCUnderline);
-
-		break; case L'\b': edit(win->tag, EditBackspace, 0);
-		break; case L'\t': edit(win->tag, EditComplete, 0);
-		break; case L'\n': edit(win->tag, EditEnter, 0);
-
-		break; default: if (iswprint(ch)) edit(win->tag, EditInsert, ch);
-	}
-}
-
-void uiRead(void) {
-	if (ui.hide) uiShow();
-	static bool meta;
-	int ret;
-	wint_t ch;
-	enum TermEvent event;
-	while (ERR != (ret = wget_wch(ui.input, &ch))) {
-		if (ret == KEY_CODE_YES) {
-			keyCode(ch);
-		} else if (ch < 0200 && (event = termEvent((char)ch))) {
-			struct Window *win = windows.active;
-			switch (event) {
-				break; case TermFocusIn:  if (win) windowUnmark(win);
-				break; case TermFocusOut: if (win) windowMark(win);
-				break; default: {}
-			}
-			uiStatus();
-		} else if (ch == Esc) {
-			meta = true;
-			continue;
-		} else if (meta) {
-			keyMeta(ch == Del ? '\b' : ch);
-		} else {
-			keyChar(ch == Del ? '\b' : ch);
-		}
-		meta = false;
-	}
-	uiPrompt(false);
-}
-
-static bool isAction(struct Tag tag, const wchar_t *input) {
-	if (tag.id == TagStatus.id || tag.id == TagRaw.id) return false;
-	return !wcsncasecmp(input, L"/me ", 4);
-}
-
-static bool isCommand(struct Tag tag, const wchar_t *input) {
-	if (tag.id == TagStatus.id || tag.id == TagRaw.id) return true;
-	if (input[0] != L'/') return false;
-	const wchar_t *space = wcschr(&input[1], L' ');
-	const wchar_t *extra = wcschr(&input[1], L'/');
-	return !extra || (space && extra > space);
-}
-
-void uiPrompt(bool nickChanged) {
-	static wchar_t *promptMesg;
-	static wchar_t *promptAction;
-	if (nickChanged || !promptMesg || !promptAction) {
-		free(promptMesg);
-		free(promptAction);
-		enum IRCColor color = colorGen(self.user);
-		int len = aswprintf(&promptMesg, L"\3%d<%s>\3 ", color, self.nick);
-		if (len < 0) err(EX_OSERR, "aswprintf");
-		len = aswprintf(&promptAction, L"\3%d* %s\3 ", color, self.nick);
-		if (len < 0) err(EX_OSERR, "aswprintf");
-	}
-
-	const wchar_t *input = editHead();
-
-	wmove(ui.input, 0, 0);
-	if (windows.active) {
-		if (isAction(windows.active->tag, input) && editTail() >= &input[4]) {
-			input = &input[4];
-			addWrap(ui.input, promptAction);
-		} else if (!isCommand(windows.active->tag, input)) {
-			addWrap(ui.input, promptMesg);
-		}
-	}
-
-	int x = 0;
-	struct Format format = { .str = input };
-	formatReset(&format);
-	while (formatParse(&format, editTail())) {
-		if (format.split) getyx(ui.input, _, x);
-		addFormat(ui.input, &format);
-	}
-	wclrtoeol(ui.input);
-	wmove(ui.input, 0, x);
-}
diff --git a/url.c b/url.c
deleted file mode 100644
index 21d93e6..0000000
--- a/url.c
+++ /dev/null
@@ -1,111 +0,0 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <assert.h>
-#include <err.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sysexits.h>
-#include <unistd.h>
-
-#include "chat.h"
-
-static const char *Schemes[] = {
-	"cvs:",
-	"ftp:",
-	"git:",
-	"http:",
-	"https:",
-	"irc:",
-	"ircs:",
-	"magnet:",
-	"sftp:",
-	"ssh:",
-	"svn:",
-	"telnet:",
-	"vnc:",
-};
-static const size_t SchemesLen = sizeof(Schemes) / sizeof(Schemes[0]);
-
-struct Entry {
-	size_t tag;
-	char *url;
-};
-
-enum { RingLen = 32 };
-static_assert(!(RingLen & (RingLen - 1)), "power of two RingLen");
-
-static struct {
-	struct Entry buf[RingLen];
-	size_t end;
-} ring;
-
-static void ringPush(struct Tag tag, const char *url, size_t len) {
-	free(ring.buf[ring.end].url);
-	ring.buf[ring.end].tag = tag.id;
-	ring.buf[ring.end].url = strndup(url, len);
-	if (!ring.buf[ring.end].url) err(EX_OSERR, "strndup");
-	ring.end = (ring.end + 1) & (RingLen - 1);
-}
-
-static struct Entry ringEntry(size_t i) {
-	return ring.buf[(ring.end + i) & (RingLen - 1)];
-}
-
-void urlScan(struct Tag tag, const char *str) {
-	while (str[0]) {
-		size_t len = 1;
-		for (size_t i = 0; i < SchemesLen; ++i) {
-			if (strncmp(str, Schemes[i], strlen(Schemes[i]))) continue;
-			len = strcspn(str, " >\"");
-			ringPush(tag, str, len);
-		}
-		str = &str[len];
-	}
-}
-
-void urlList(struct Tag tag) {
-	uiHide();
-	for (size_t i = 0; i < RingLen; ++i) {
-		struct Entry entry = ringEntry(i);
-		if (!entry.url || entry.tag != tag.id) continue;
-		printf("%s\n", entry.url);
-	}
-}
-
-void urlOpenMatch(struct Tag tag, const char *substr) {
-	for (size_t i = RingLen - 1; i < RingLen; --i) {
-		struct Entry entry = ringEntry(i);
-		if (!entry.url || entry.tag != tag.id) continue;
-		if (!strstr(entry.url, substr)) continue;
-		eventPipe((const char *[]) { "open", entry.url, NULL });
-		break;
-	}
-}
-
-void urlOpenRange(struct Tag tag, size_t at, size_t to) {
-	size_t argc = 1;
-	const char *argv[2 + RingLen] = { "open" };
-	size_t tagIndex = 0;
-	for (size_t i = RingLen - 1; i < RingLen; --i) {
-		struct Entry entry = ringEntry(i);
-		if (!entry.url || entry.tag != tag.id) continue;
-		if (tagIndex >= at && tagIndex < to) argv[argc++] = entry.url;
-		tagIndex++;
-	}
-	argv[argc] = NULL;
-	if (argc > 1) eventPipe(argv);
-}