diff options
Diffstat (limited to '')
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | LICENSE | 674 | ||||
-rw-r--r-- | Makefile | 41 | ||||
-rw-r--r-- | README.7 | 126 | ||||
-rw-r--r-- | catgirl.1 | 497 | ||||
-rw-r--r-- | chat.c | 259 | ||||
-rw-r--r-- | chat.h | 241 | ||||
-rw-r--r-- | command.c | 280 | ||||
-rw-r--r-- | complete.c | 162 | ||||
-rw-r--r-- | config.c | 137 | ||||
-rwxr-xr-x | configure | 11 | ||||
-rw-r--r-- | edit.c | 207 | ||||
-rw-r--r-- | handle.c | 637 | ||||
-rw-r--r-- | irc.c | 250 | ||||
-rw-r--r-- | ui.c | 974 | ||||
-rw-r--r-- | url.c | 202 | ||||
-rw-r--r-- | xdg.c | 134 |
17 files changed, 4836 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4cc4220 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.o +catgirl +config.mk +tags diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b1ffede --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +PREFIX = /usr/local +MANDIR = ${PREFIX}/share/man + +CFLAGS += -std=c11 -Wall -Wextra -Wpedantic +LDLIBS = -lcrypto -ltls -lncursesw + +-include config.mk + +OBJS += chat.o +OBJS += command.o +OBJS += complete.o +OBJS += config.o +OBJS += edit.o +OBJS += handle.o +OBJS += irc.o +OBJS += ui.o +OBJS += url.o +OBJS += xdg.o + +dev: tags all + +all: catgirl + +catgirl: ${OBJS} + ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ + +${OBJS}: chat.h + +tags: *.h *.c + ctags -w *.h *.c + +clean: + rm -f tags catgirl ${OBJS} + +install: catgirl catgirl.1 + install -d ${PREFIX}/bin ${MANDIR}/man1 + install catgirl ${PREFIX}/bin + gzip -c catgirl.1 > ${MANDIR}/man1/catgirl.1.gz + +uninstall: + rm -f ${PREFIX}/bin/catgirl ${MANDIR}/man1/catgirl.1.gz diff --git a/README.7 b/README.7 new file mode 100644 index 0000000..1478722 --- /dev/null +++ b/README.7 @@ -0,0 +1,126 @@ +.Dd February 11, 2020 +.Dt README 7 +.Os "Causal Agency" +. +.Sh NAME +.Nm catgirl +.Nd IRC client +. +.Sh DESCRIPTION +.Xr catgirl 1 +is a TLS-only terminal IRC client. +. +.Ss Notable Features +.Bl -bullet +.It +Tab complete: +most recently seen or mentioned nicks +are completed first. +Commas are inserted between multple nicks. +.It +Indicators: +the prompt clearly shows whether input +will be interpreted as a command +or sent as a message. +An indicator appears when scrolled up +in the chat history. +.It +Nick coloring: +color generation based on usernames +remains stable across nick changes. +Mentions of users in messages are colored. +.It +URL detection: +recent URLs from a particular user +or matching a substring +can be opened or copied. +.It +History: +window contents can be saved +and restored on startup. +.El +. +.Ss Non-features +.Bl -bullet +.It +Dynamic configuration: +all configuration happens +in a simple text file +or on the command line. +.It +Multi-network: +a terminal multiplexer such as +.Xr screen 1 +or +.Xr tmux 1 +(or just your regular terminal emulator tabs) +can be used to connect +.Nm +to multiple networks. +.It +Reconnection: +when the connection to the server is lost, +.Nm +exits. +It can be run in a loop +or connected to a bouncer, +such as +.Lk https://git.causal.agency/pounce "pounce" . +.It +Cleartext IRC: +TLS is now ubiquitous +and certificates are easy to obtain. +.El +. +.Sh INSTALLING +.Nm +requires LibreSSL +.Pq Fl ltls +and ncurses +.Pq Fl lncursesw . +It primarily targets +.Fx +and macOS, +as well as Linux. +.Bd -literal -offset indent +\&./configure +make all +sudo make install PREFIX=/usr/local +.Ed +. +.Sh FILES +.Bl -tag -width "complete.c" -compact +.It Pa chat.h +global state and declarations +.It Pa chat.c +startup and event loop +.It Pa irc.c +IRC connection and parsing +.It Pa ui.c +curses interface +.It Pa handle.c +IRC message handling +.It Pa command.c +input command handling +.It Pa edit.c +line editing +.It Pa complete.c +tab complete +.It Pa url.c +URL detection +.It Pa config.c +configuration parsing +.It Pa xdg.c +XDG base directories +.El +. +.Sh CONTRIBUTING +The upstream URL of this project is +.Aq Lk https://git.causal.agency/catgirl . +I'm happy to receive contributions in any form at +.Aq Mt june@causal.agency . +For sending patches by email, see +.Aq Lk https://git-send-email.io . +. +.Sh SEE ALSO +.Xr catgirl 1 diff --git a/catgirl.1 b/catgirl.1 new file mode 100644 index 0000000..7c51b08 --- /dev/null +++ b/catgirl.1 @@ -0,0 +1,497 @@ +.Dd February 10, 2020 +.Dt CATGIRL 1 +.Os +. +.Sh NAME +.Nm catgirl +.Nd IRC client +. +.Sh SYNOPSIS +.Nm +.Op Fl ev +.Op Fl C Ar copy +.Op Fl H Ar hash +.Op Fl O Ar open +.Op Fl a Ar auth +.Op Fl c Ar cert +.Op Fl h Ar host +.Op Fl j Ar join +.Op Fl k Ar priv +.Op Fl n Ar nick +.Op Fl p Ar port +.Op Fl r Ar real +.Op Fl s Ar save +.Op Fl u Ar user +.Op Fl w Ar pass +.Op Ar config ... +. +.Sh DESCRIPTION +The +.Nm +program is a TLS-only +curses IRC client. +. +.Pp +Options can be loaded from files +listed on the command line. +Files are searched for in +.Pa $XDG_CONFIG_DIRS/catgirl +unless the path starts with +.Ql / +or +.Ql \&. . +Each option is placed on a line, +and lines beginning with +.Ql # +are ignored. +The options are listed below +following their corresponding flags. +. +.Pp +The arguments are as follows: +.Bl -tag -width Ds +.It Fl C Ar util , Cm copy = Ar util +Set the utility used by +.Ic /copy . +The default is the first available of +.Xr pbcopy 1 , +.Xr wl-copy 1 , +.Xr xclip 1 , +.Xr xsel 1 . +. +.It Fl H Ar hash , Cm hash = Ar hash +Set the initial value of +the nick color hash function. +. +.It Fl O Ar util , Cm open = Ar util +Set the utility used by +.Ic /open . +The default is the first available of +.Xr open 1 , +.Xr xdg-open 1 . +. +.It Fl a Ar user Ns : Ns Ar pass , Cm sasl-plain = Ar user Ns : Ns Ar pass +Authenticate as +.Ar user +with +.Ar pass +using SASL PLAIN. +Since this requires the account password +in plain text, +it is recommended to use SASL EXTERNAL instead with +.Fl e . +. +.It Fl c Ar path , Cm cert = Ar path +Load the TLS client certificate from +.Ar path . +If the private key is in a separate file, +it is loaded with +.Fl k . +With +.Fl e , +authenticate using SASL EXTERNAL. +. +.It Fl e , Cm sasl-external +Authenticate using SASL EXTERNAL, +also known as CertFP. +The TLS client certificate is loaded with +.Fl c . +. +.It Fl h Ar host , Cm host = Ar host +Connect to +.Ar host . +. +.It Fl j Ar join , Cm join = Ar join +Join the comma-separated list of channels +.Ar join . +. +.It Fl k Ar path , Cm priv = Ar priv +Load the TLS client private key from +.Ar path . +. +.It Fl n Ar nick , Cm nick = Ar nick +Set nickname to +.Ar nick . +The default nickname is the user's name. +. +.It Fl p Ar port , Cm port = Ar port +Connect to +.Ar port . +The default port is 6697. +. +.It Fl r Ar real , Cm real = Ar real +Set realname to +.Ar real . +The default realname is the same as the nickname. +. +.It Fl s Ar name , Cm save = Ar name +Load and save the contents of windows from +.Ar name +in +.Pa $XDG_DATA_DIRS/catgirl , +or an absolute or relative path if +.Ar name +starts with +.Ql / +or +.Ql \&. . +. +.It Fl u Ar user , Cm user = Ar user +Set username to +.Ar user . +The default username is the same as the nickname. +. +.It Fl v , Cm debug +Log raw IRC messages to the +.Sy <debug> +window +as well as standard error +if it is not a terminal. +. +.It Fl w Ar pass , Cm pass = Ar pass +Log in with the server password +.Ar pass . +.El +. +.Sh COMMANDS +Any unique prefix can be used to abbreviate a command. +For example, +.Ic /join +can be typed +.Ic /j . +. +.Ss Chat Commands +.Bl -tag -width Ds +.It Ic /join Ar channel +Join a channel. +.It Ic /me Op Ar action +Send an action message. +.It Ic /msg Ar nick message +Send a private message. +.It Ic /names +List users in the channel. +.It Ic /nick Ar nick +Change nicknames. +.It Ic /notice Ar message +Send a notice. +.It Ic /part Op Ar message +Leave the channel. +.It Ic /query Ar nick +Start a private conversation. +.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 channel. +.It Ic /whois Ar nick +Query information about a user. +.El +. +.Ss UI Commands +.Bl -tag -width Ds +.It Ic /close Op Ar name | num +Close the named, numbered or current window. +.It Ic /copy Op Ar nick | substring +Copy the most recent URL from +.Ar nick +or matching +.Ar substring . +.It Ic /debug +Toggle logging in the +.Sy <debug> +window. +.It Ic /help Op Ar search +View this manual. +Type +.Ic q +to return to +.Nm . +.It Ic /open Op Ar count +Open each of +.Ar count +most recent URLs. +.It Ic /open Ar nick | substring +Open the most recent URL from +.Ar nick +or matching +.Ar substring . +.It Ic /window Ar name +Switch to window by name. +.It Ic /window Ar num , Ic / Ns Ar num +Switch to window by number. +.El +. +.Sh KEY BINDINGS +The +.Nm +interface provides +.Xr emacs 1 Ns -like +line editing +as well as keys for IRC formatting. +The prefixes +.Ic C- +and +.Ic M- +represent the control and meta (alt) +modifiers, respectively. +. +.Ss Line Editing +.Bl -tag -width Ds -compact +.It Ic C-a +Move to beginning of line. +.It Ic C-b +Move left. +.It Ic C-d +Delete next character. +.It Ic C-e +Move to end of line. +.It Ic C-f +Move right. +.It Ic C-k +Delete to end of line. +.It Ic C-u +Delete to beginning of line. +.It Ic C-w +Delete previous word. +.It Ic C-y +Paste previously deleted text. +.It Ic M-b +Move to previous word. +.It Ic M-d +Delete next word. +.It Ic M-f +Move to next word. +.It Ic Tab +Complete nick, channel or command. +.El +. +.Ss Window Keys +.Bl -tag -width Ds -compact +.It Ic C-l +Redraw the UI. +.It Ic C-n +Switch to next window. +.It Ic C-o +Switch to previously selected window. +.It Ic C-p +Switch to previous window. +.It Ic M-/ +Switch to previously selected window. +.It Ic M-a +Cycle through unread windows. +.It Ic M-l +List the contents of the window +without word-wrapping. +Press +.Ic Enter +to return to +.Nm . +.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 M-u +Scroll to first unread line. +.El +. +.Ss IRC Formatting +.Bl -tag -width Ds -compact +.It Ic C-z b +Toggle bold. +.It Ic C-z c +Set or reset color. +.It Ic C-z i +Toggle italics. +.It Ic C-z o +Reset formatting. +.It Ic C-z r +Toggle reverse color. +.It Ic C-z u +Toggle underline. +.El +. +.Pp +To set colors, follow +.Ic C-z c +by one or two digits for the foreground color, +optionally followed by a comma +and one or two digits for the background color. +To reset color, follow +.Ic C-z c +by a non-digit. +. +.Pp +The color numbers are as follows: +.Pp +.Bl -column "99" "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 +.It 99 Ta default +.El +. +.Sh FILES +.Bl -tag -width Ds +.It Pa $XDG_CONFIG_DIRS/catgirl +Configuration files are searched for first in +.Ev $XDG_CONFIG_HOME , +usually +.Pa ~/.config , +followed by the colon-separated list of paths +.Ev $XDG_CONFIG_DIRS , +usually +.Pa /etc/xdg . +.It Pa ~/.config/catgirl +The most likely location of configuration files. +. +.It Pa $XDG_DATA_DIRS/catgirl +Save files are searched for first in +.Ev $XDG_DATA_HOME , +usually +.Pa ~/.local/share , +followed by the colon-separated list of paths +.Ev $XDG_DATA_DIRS , +usually +.Pa /usr/local/share:/usr/share . +.It Pa ~/.local/share/catgirl +The most likely location of save files. +.El +. +.Sh EXAMPLES +Command line: +.Bd -literal -offset indent +catgirl -h chat.freenode.net -j '#ascii.town' +.Ed +.Pp +Configuration file: +.Bd -literal -offset indent +host = chat.freenode.net +join = #ascii.town +.Ed +. +.Sh STANDARDS +.Bl -item +.It +.Rs +.%A Kiyoshi Aman +.%T IRCv3.1 extended-join Extension +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/extended-join-3.1 +.Re +. +.It +.Rs +.%A Waldo Bastian +.%A Ryan Lortie +.%A Lennart Poettering +.%T XDG Base Directory Specification +.%D November 24, 2010 +.%U https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html +.Re +. +.It +.Rs +.%A Kyle Fuller +.%A St\('ephan Kochen +.%A Alexey Sokolov +.%A James Wheare +.%T IRCv3.2 server-time Extension +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/server-time-3.2 +.Re +. +.It +.Rs +.%A Lee Hardy +.%A Perry Lorier +.%A Kevin L. Mitchell +.%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 S. Josefsson +.%T The Base16, Base32, and Base64 Data Encodings +.%I IETF +.%N RFC 4648 +.%D October 2006 +.%U https://tools.ietf.org/html/rfc4648 +.Re +. +.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 Mantas Mikul\[u0117]nas +.%T IRCv3.2 userhost-in-names Extension +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/userhost-in-names-3.2 +.Re +. +.It +.Rs +.%A Daniel Oaks +.%T IRC Formatting +.%I ircdocs +.%U https://modern.ircdocs.horse/formatting.html +.Re +. +.It +.Rs +.%A William Pitcock +.%A Jilles Tjoelker +.%T IRCv3.1 SASL Authentication +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/sasl-3.1.html +.Re +. +.It +.Rs +.%A Alexey Sokolov +.%A St\('ephan Kochen +.%A Kyle Fuller +.%A Kiyoshi Aman +.%A James Wheare +.%T IRCv3 Message Tags +.%I IRCv3 Working Group +.%U https://ircv3.net/specs/extensions/message-tags +.Re +. +.It +.Rs +.%A K. Zeilenga, Ed. +.%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 +.El +. +.Sh AUTHORS +.An June Bug Aq Mt june@causal.agency +. +.Sh BUGS +Send mail to +.Aq Mt june@causal.agency +or join +.Li #ascii.town +on +.Li chat.freenode.net . diff --git a/chat.c b/chat.c new file mode 100644 index 0000000..f854a33 --- /dev/null +++ b/chat.c @@ -0,0 +1,259 @@ +/* Copyright (C) 2020 C. McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <locale.h> +#include <poll.h> +#include <signal.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/wait.h> +#include <sysexits.h> +#include <unistd.h> + +#include "chat.h" + +char *idNames[IDCap] = { + [None] = "<none>", + [Debug] = "<debug>", + [Network] = "<network>", +}; + +enum Color idColors[IDCap] = { + [None] = Black, + [Debug] = Green, + [Network] = Gray, +}; + +size_t idNext = Network + 1; + +struct Self self = { .color = Default }; + +static const char *save; +static void exitSave(void) { + int error = uiSave(save); + if (error) { + warn("%s", save); + _exit(EX_IOERR); + } +} + +uint32_t hashInit; + +int procPipe[2] = { -1, -1 }; + +static void pipeRead(void) { + char buf[1024]; + ssize_t len = read(procPipe[0], buf, sizeof(buf) - 1); + if (len < 0) err(EX_IOERR, "read"); + if (!len) return; + buf[len - 1] = '\0'; + char *ptr = buf; + while (ptr) { + char *line = strsep(&ptr, "\n"); + uiFormat(Network, Warm, NULL, "%s", line); + } +} + +static volatile sig_atomic_t signals[NSIG]; +static void signalHandler(int signal) { + signals[signal] = 1; +} + +int main(int argc, char *argv[]) { + setlocale(LC_CTYPE, ""); + + bool insecure = false; + const char *host = NULL; + const char *port = "6697"; + const char *cert = NULL; + const char *priv = NULL; + + bool sasl = false; + const char *pass = NULL; + const char *nick = NULL; + const char *user = NULL; + const char *real = NULL; + + const char *Opts = "!C:H:O:a:c:eh:j:k:n:p:r:s:u:vw:"; + const struct option LongOpts[] = { + { "insecure", no_argument, NULL, '!' }, + { "copy", required_argument, NULL, 'C' }, + { "hash", required_argument, NULL, 'H' }, + { "open", required_argument, NULL, 'O' }, + { "sasl-plain", required_argument, NULL, 'a' }, + { "cert", required_argument, NULL, 'c' }, + { "sasl-external", no_argument, NULL, 'e' }, + { "host", required_argument, NULL, 'h' }, + { "join", required_argument, NULL, 'j' }, + { "priv", required_argument, NULL, 'k' }, + { "nick", required_argument, NULL, 'n' }, + { "port", required_argument, NULL, 'p' }, + { "real", required_argument, NULL, 'r' }, + { "save", required_argument, NULL, 's' }, + { "user", required_argument, NULL, 'u' }, + { "debug", no_argument, NULL, 'v' }, + { "pass", required_argument, NULL, 'w' }, + {0}, + }; + + int opt; + while (0 < (opt = getopt_config(argc, argv, Opts, LongOpts, NULL))) { + switch (opt) { + break; case '!': insecure = true; + break; case 'C': urlCopyUtil = optarg; + break; case 'H': hashInit = strtoul(optarg, NULL, 0); + break; case 'O': urlOpenUtil = optarg; + break; case 'a': sasl = true; self.plain = optarg; + break; case 'c': cert = optarg; + break; case 'e': sasl = true; + break; case 'h': host = optarg; + break; case 'j': self.join = optarg; + break; case 'k': priv = optarg; + break; case 'n': nick = optarg; + break; case 'p': port = optarg; + break; case 'r': real = optarg; + break; case 's': save = optarg; + break; case 'u': user = optarg; + break; case 'v': self.debug = true; + break; case 'w': pass = optarg; + break; default: return EX_USAGE; + } + } + if (!host) errx(EX_USAGE, "host required"); + + if (!nick) nick = getenv("USER"); + if (!nick) errx(EX_CONFIG, "USER unset"); + if (!user) user = nick; + if (!real) real = nick; + + set(&self.network, host); + set(&self.chanTypes, "#&"); + set(&self.prefixes, "@+"); + commandComplete(); + + FILE *certFile = NULL; + FILE *privFile = NULL; + if (cert) { + certFile = configOpen(cert, "r"); + if (!certFile) return EX_NOINPUT; + } + if (priv) { + privFile = configOpen(priv, "r"); + if (!privFile) return EX_NOINPUT; + } + ircConfig(insecure, certFile, privFile); + if (certFile) fclose(certFile); + if (privFile) fclose(privFile); + + uiInit(); + if (save) { + uiLoad(save); + atexit(exitSave); + } + uiShowID(Network); + uiFormat(Network, Cold, NULL, "Traveling..."); + uiDraw(); + + int irc = ircConnect(host, port); + if (pass) ircFormat("PASS :%s\r\n", pass); + if (sasl) ircFormat("CAP REQ :sasl\r\n"); + ircFormat("CAP LS\r\n"); + ircFormat("NICK :%s\r\n", nick); + ircFormat("USER %s 0 * :%s\r\n", user, real); + + signal(SIGHUP, signalHandler); + signal(SIGINT, signalHandler); + signal(SIGTERM, signalHandler); + signal(SIGCHLD, signalHandler); + sig_t cursesWinch = signal(SIGWINCH, signalHandler); + + int error = pipe(procPipe); + if (error) err(EX_OSERR, "pipe"); + + fcntl(irc, F_SETFD, FD_CLOEXEC); + fcntl(procPipe[0], F_SETFD, FD_CLOEXEC); + fcntl(procPipe[1], F_SETFD, FD_CLOEXEC); + + struct pollfd fds[3] = { + { .events = POLLIN, .fd = STDIN_FILENO }, + { .events = POLLIN, .fd = irc }, + { .events = POLLIN, .fd = procPipe[0] }, + }; + while (!self.quit) { + int nfds = poll(fds, ARRAY_LEN(fds), -1); + if (nfds < 0 && errno != EINTR) err(EX_IOERR, "poll"); + if (nfds > 0) { + if (fds[0].revents) uiRead(); + if (fds[1].revents) ircRecv(); + if (fds[2].revents) pipeRead(); + } + + if (signals[SIGHUP]) self.quit = "zzz"; + if (signals[SIGINT] || signals[SIGTERM]) break; + + if (signals[SIGCHLD]) { + signals[SIGCHLD] = 0; + int status; + while (0 < waitpid(-1, &status, WNOHANG)) { + if (WIFEXITED(status) && WEXITSTATUS(status)) { + uiFormat( + Network, Warm, NULL, + "Process exits with status %d", WEXITSTATUS(status) + ); + } else if (WIFSIGNALED(status)) { + uiFormat( + Network, Warm, NULL, + "Process terminates from %s", + strsignal(WTERMSIG(status)) + ); + } + } + uiShow(); + } + + if (signals[SIGWINCH]) { + signals[SIGWINCH] = 0; + cursesWinch(SIGWINCH); + // XXX: For some reason, calling uiDraw() here is the only way to + // get uiRead() to properly receive KEY_RESIZE. + uiDraw(); + uiRead(); + } + + uiDraw(); + } + + if (self.quit) { + ircFormat("QUIT :%s\r\n", self.quit); + } else { + ircFormat("QUIT\r\n"); + } + struct Message msg = { + .nick = self.nick, + .user = self.user, + .cmd = "QUIT", + .params[0] = self.quit, + }; + handle(msg); + + uiHide(); +} diff --git a/chat.h b/chat.h new file mode 100644 index 0000000..f47b244 --- /dev/null +++ b/chat.h @@ -0,0 +1,241 @@ +/* Copyright (C) 2020 C. McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <err.h> +#include <getopt.h> +#include <stdbool.h> +#include <stdint.h> +#include <string.h> +#include <sysexits.h> +#include <time.h> +#include <wchar.h> + +#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) +#define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit + +#define XDG_SUBDIR "catgirl" + +typedef unsigned char byte; + +int procPipe[2]; + +enum Color { + White, Black, Blue, Green, Red, Brown, Magenta, Orange, + Yellow, LightGreen, Cyan, LightCyan, LightBlue, Pink, Gray, LightGray, + Default = 99, +}; + +enum { None, Debug, Network, IDCap = 256 }; +extern char *idNames[IDCap]; +extern enum Color idColors[IDCap]; +extern size_t idNext; + +static inline size_t idFind(const char *name) { + for (size_t id = 0; id < idNext; ++id) { + if (!strcmp(idNames[id], name)) return id; + } + return None; +} + +static inline size_t idFor(const char *name) { + size_t id = idFind(name); + if (id) return id; + if (idNext == IDCap) return Network; + idNames[idNext] = strdup(name); + if (!idNames[idNext]) err(EX_OSERR, "strdup"); + idColors[idNext] = Default; + return idNext++; +} + +#define ENUM_CAP \ + X("extended-join", CapExtendedJoin) \ + X("sasl", CapSASL) \ + X("server-time", CapServerTime) \ + X("userhost-in-names", CapUserhostInNames) + +enum Cap { +#define X(name, id) BIT(id), + ENUM_CAP +#undef X +}; + +extern struct Self { + bool debug; + char *plain; + const char *join; + enum Cap caps; + char *network; + char *chanTypes; + char *prefixes; + char *nick; + char *user; + enum Color color; + char *quit; +} self; + +static inline void set(char **field, const char *value) { + free(*field); + *field = strdup(value); + if (!*field) err(EX_OSERR, "strdup"); +} + +#define ENUM_TAG \ + X("time", TagTime) + +enum Tag { +#define X(name, id) id, + ENUM_TAG +#undef X + TagCap, +}; + +enum { ParamCap = 15 }; +struct Message { + char *tags[TagCap]; + char *nick; + char *user; + char *host; + char *cmd; + char *params[ParamCap]; +}; + +void ircConfig(bool insecure, FILE *cert, FILE *priv); +int ircConnect(const char *host, const char *port); +void ircRecv(void); +void ircSend(const char *ptr, size_t len); +void ircFormat(const char *format, ...) + __attribute__((format(printf, 1, 2))); + +extern struct Replies { + size_t join; + size_t topic; + size_t names; + size_t whois; +} replies; + +void handle(struct Message msg); +void command(size_t id, char *input); +const char *commandIsPrivmsg(size_t id, const char *input); +const char *commandIsNotice(size_t id, const char *input); +const char *commandIsAction(size_t id, const char *input); +void commandComplete(void); + +enum Heat { Cold, Warm, Hot }; +void uiInit(void); +void uiShow(void); +void uiHide(void); +void uiDraw(void); +void uiShowID(size_t id); +void uiShowNum(size_t num); +void uiCloseID(size_t id); +void uiCloseNum(size_t id); +void uiRead(void); +void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str); +void uiFormat( + size_t id, enum Heat heat, const time_t *time, const char *format, ... +) __attribute__((format(printf, 4, 5))); +void uiLoad(const char *name); +int uiSave(const char *name); + +enum Edit { + EditHead, + EditTail, + EditPrev, + EditNext, + EditPrevWord, + EditNextWord, + EditDeleteHead, + EditDeleteTail, + EditDeletePrev, + EditDeleteNext, + EditDeletePrevWord, + EditDeleteNextWord, + EditPaste, + EditInsert, + EditComplete, + EditEnter, +}; +void edit(size_t id, enum Edit op, wchar_t ch); +char *editBuffer(size_t *pos); + +const char *complete(size_t id, const char *prefix); +void completeAccept(void); +void completeReject(void); +void completeAdd(size_t id, const char *str, enum Color color); +void completeTouch(size_t id, const char *str, enum Color color); +void completeReplace(size_t id, const char *old, const char *new); +void completeRemove(size_t id, const char *str); +void completeClear(size_t id); +size_t completeID(const char *str); +enum Color completeColor(size_t id, const char *str); + +extern const char *urlOpenUtil; +extern const char *urlCopyUtil; +void urlScan(size_t id, const char *nick, const char *mesg); +void urlOpenCount(size_t id, size_t count); +void urlOpenMatch(size_t id, const char *str); +void urlCopyMatch(size_t id, const char *str); + +FILE *configOpen(const char *path, const char *mode); +FILE *dataOpen(const char *path, const char *mode); + +int getopt_config( + int argc, char *const *argv, + const char *optstring, const struct option *longopts, int *longindex +); + +extern uint32_t hashInit; +static inline enum Color hash(const char *str) { + if (*str == '~') str++; + uint32_t hash = hashInit; + for (; *str; ++str) { + hash = (hash << 5) | (hash >> 27); + hash ^= *str; + hash *= 0x27220A95; + } + return 2 + hash % 74; +} + +#define BASE64_SIZE(len) (1 + ((len) + 2) / 3 * 4) +static const char Base64[64] = { + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +}; +static inline 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'; +} + +// Defined in libcrypto if missing from libc: +void explicit_bzero(void *b, size_t len); diff --git a/command.c b/command.c new file mode 100644 index 0000000..5cb43cf --- /dev/null +++ b/command.c @@ -0,0 +1,280 @@ +/* Copyright (C) 2020 C. McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "chat.h" + +typedef void Command(size_t id, char *params); + +static void commandDebug(size_t id, char *params) { + (void)id; + (void)params; + self.debug ^= true; + uiFormat( + Debug, Warm, NULL, + "\3%dDebug is %s", Gray, (self.debug ? "on" : "off") + ); +} + +static void commandQuote(size_t id, char *params) { + (void)id; + if (params) ircFormat("%s\r\n", params); +} + +static void commandPrivmsg(size_t id, char *params) { + if (!params || !params[0]) return; + ircFormat("PRIVMSG %s :%s\r\n", idNames[id], params); + struct Message msg = { + .nick = self.nick, + .user = self.user, + .cmd = "PRIVMSG", + .params[0] = idNames[id], + .params[1] = params, + }; + handle(msg); +} + +static void commandNotice(size_t id, char *params) { + if (!params || !params[0]) return; + ircFormat("NOTICE %s :%s\r\n", idNames[id], params); + struct Message msg = { + .nick = self.nick, + .user = self.user, + .cmd = "NOTICE", + .params[0] = idNames[id], + .params[1] = params, + }; + handle(msg); +} + +static void commandMe(size_t id, char *params) { + char buf[512]; + snprintf(buf, sizeof(buf), "\1ACTION %s\1", (params ? params : "")); + commandPrivmsg(id, buf); +} + +static void commandMsg(size_t id, char *params) { + (void)id; + char *nick = strsep(¶ms, " "); + if (!params) return; + commandPrivmsg(idFor(nick), params); +} + +static void commandJoin(size_t id, char *params) { + size_t count = 1; + if (params) { + for (char *ch = params; *ch && *ch != ' '; ++ch) { + if (*ch == ',') count++; + } + } + ircFormat("JOIN %s\r\n", (params ? params : idNames[id])); + replies.join += count; + replies.topic += count; + replies.names += count; +} + +static void commandPart(size_t id, char *params) { + if (params) { + ircFormat("PART %s :%s\r\n", idNames[id], params); + } else { + ircFormat("PART %s\r\n", idNames[id]); + } +} + +static void commandQuit(size_t id, char *params) { + (void)id; + set(&self.quit, (params ? params : "Goodbye")); +} + +static void commandNick(size_t id, char *params) { + (void)id; + if (!params) return; + ircFormat("NICK :%s\r\n", params); +} + +static void commandTopic(size_t id, char *params) { + if (params) { + ircFormat("TOPIC %s :%s\r\n", idNames[id], params); + } else { + ircFormat("TOPIC %s\r\n", idNames[id]); + replies.topic++; + } +} + +static void commandNames(size_t id, char *params) { + (void)params; + ircFormat("NAMES :%s\r\n", idNames[id]); + replies.names++; +} + +static void commandWhois(size_t id, char *params) { + (void)id; + if (!params) return; + ircFormat("WHOIS :%s\r\n", params); + replies.whois++; +} + +static void commandQuery(size_t id, char *params) { + if (!params) return; + size_t query = idFor(params); + idColors[query] = completeColor(id, params); + uiShowID(query); +} + +static void commandWindow(size_t id, char *params) { + if (!params) return; + if (isdigit(params[0])) { + uiShowNum(strtoul(params, NULL, 10)); + } else { + id = idFind(params); + if (id) uiShowID(id); + } +} + +static void commandClose(size_t id, char *params) { + if (!params) { + uiCloseID(id); + } else if (isdigit(params[0])) { + uiCloseNum(strtoul(params, NULL, 10)); + } else { + id = idFind(params); + if (id) uiCloseID(id); + } +} + +static void commandOpen(size_t id, char *params) { + if (!params) { + urlOpenCount(id, 1); + } else if (isdigit(params[0])) { + urlOpenCount(id, strtoul(params, NULL, 10)); + } else { + urlOpenMatch(id, params); + } +} + +static void commandCopy(size_t id, char *params) { + urlCopyMatch(id, params); +} + +static void commandHelp(size_t id, char *params) { + (void)id; + uiHide(); + + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + if (pid) return; + + char buf[256]; + snprintf(buf, sizeof(buf), "ip%s$", (params ? params : "COMMANDS")); + setenv("LESS", buf, 1); + execlp("man", "man", "1", "catgirl", NULL); + dup2(procPipe[1], STDERR_FILENO); + warn("man"); + _exit(EX_UNAVAILABLE); +} + +static const struct Handler { + const char *cmd; + Command *fn; +} Commands[] = { + { "/close", commandClose }, + { "/copy", commandCopy }, + { "/debug", commandDebug }, + { "/help", commandHelp }, + { "/join", commandJoin }, + { "/me", commandMe }, + { "/msg", commandMsg }, + { "/names", commandNames }, + { "/nick", commandNick }, + { "/notice", commandNotice }, + { "/open", commandOpen }, + { "/part", commandPart }, + { "/query", commandQuery }, + { "/quit", commandQuit }, + { "/quote", commandQuote }, + { "/topic", commandTopic }, + { "/whois", commandWhois }, + { "/window", commandWindow }, +}; + +static int compar(const void *cmd, const void *_handler) { + const struct Handler *handler = _handler; + return strcmp(cmd, handler->cmd); +} + +const char *commandIsPrivmsg(size_t id, const char *input) { + if (id == Network || id == Debug) return NULL; + if (input[0] != '/') return input; + const char *space = strchr(&input[1], ' '); + const char *slash = strchr(&input[1], '/'); + if (slash && (!space || slash < space)) return input; + return NULL; +} + +const char *commandIsNotice(size_t id, const char *input) { + if (id == Network || id == Debug) return NULL; + if (strncmp(input, "/notice ", 8)) return NULL; + return &input[8]; +} + +const char *commandIsAction(size_t id, const char *input) { + if (id == Network || id == Debug) return NULL; + if (strncmp(input, "/me ", 4)) return NULL; + return &input[4]; +} + +void command(size_t id, char *input) { + if (id == Debug && input[0] != '/') { + commandQuote(id, input); + } else if (commandIsPrivmsg(id, input)) { + commandPrivmsg(id, input); + } else if (input[0] == '/' && isdigit(input[1])) { + commandWindow(id, &input[1]); + } else { + const char *cmd = strsep(&input, " "); + const char *unique = complete(None, cmd); + if (unique && !complete(None, cmd)) { + cmd = unique; + completeReject(); + } + const struct Handler *handler = bsearch( + cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar + ); + if (handler) { + if (input) { + input += strspn(input, " "); + size_t len = strlen(input); + while (input[len - 1] == ' ') input[--len] = '\0'; + if (!input[0]) input = NULL; + } + if (input && !input[0]) input = NULL; + handler->fn(id, input); + } else { + uiFormat(id, Hot, NULL, "No such command %s", cmd); + } + } +} + +void commandComplete(void) { + for (size_t i = 0; i < ARRAY_LEN(Commands); ++i) { + completeAdd(None, Commands[i].cmd, Default); + } +} diff --git a/complete.c b/complete.c new file mode 100644 index 0000000..2f5275f --- /dev/null +++ b/complete.c @@ -0,0 +1,162 @@ +/* Copyright (C) 2020 C. McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <err.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> + +#include "chat.h" + +struct Node { + size_t id; + char *str; + enum Color color; + struct Node *prev; + struct Node *next; +}; + +static struct Node *alloc(size_t id, const char *str, enum Color color) { + struct Node *node = malloc(sizeof(*node)); + if (!node) err(EX_OSERR, "malloc"); + node->id = id; + node->str = strdup(str); + if (!node->str) err(EX_OSERR, "strdup"); + node->color = color; + node->prev = NULL; + node->next = NULL; + return node; +} + +static struct Node *head; +static struct Node *tail; + +static struct Node *detach(struct Node *node) { + if (node->prev) node->prev->next = node->next; + if (node->next) node->next->prev = node->prev; + if (head == node) head = node->next; + if (tail == node) tail = node->prev; + node->prev = NULL; + node->next = NULL; + return node; +} + +static struct Node *prepend(struct Node *node) { + node->prev = NULL; + node->next = head; + if (head) head->prev = node; + head = node; + if (!tail) tail = node; + return node; +} + +static struct Node *append(struct Node *node) { + node->next = NULL; + node->prev = tail; + if (tail) tail->next = node; + tail = node; + if (!head) head = node; + return node; +} + +static struct Node *find(size_t id, const char *str) { + for (struct Node *node = head; node; node = node->next) { + if (node->id == id && !strcmp(node->str, str)) return node; + } + return NULL; +} + +void completeAdd(size_t id, const char *str, enum Color color) { + if (!find(id, str)) append(alloc(id, str, color)); +} + +void completeTouch(size_t id, const char *str, enum Color color) { + struct Node *node = find(id, str); + if (node && node->color != color) node->color = color; + prepend(node ? detach(node) : alloc(id, str, color)); +} + +enum Color completeColor(size_t id, const char *str) { + struct Node *node = find(id, str); + return (node ? node->color : Default); +} + +static struct Node *match; + +const char *complete(size_t id, const char *prefix) { + for (match = (match ? match->next : head); match; match = match->next) { + if (match->id && match->id != id) continue; + if (strncasecmp(match->str, prefix, strlen(prefix))) continue; + return match->str; + } + return NULL; +} + +void completeAccept(void) { + if (match) prepend(detach(match)); + match = NULL; +} + +void completeReject(void) { + match = NULL; +} + +size_t completeID(const char *str) { + for (match = (match ? match->next : head); match; match = match->next) { + if (match->id && !strcmp(match->str, str)) return match->id; + } + return None; +} + +void completeReplace(size_t id, const char *old, const char *new) { + struct Node *next = NULL; + for (struct Node *node = head; node; node = node->next) { + next = node->next; + if (id && node->id != id) continue; + if (strcmp(node->str, old)) continue; + if (match == node) match = NULL; + free(node->str); + node->str = strdup(new); + if (!node->str) err(EX_OSERR, "strdup"); + prepend(detach(node)); + } +} + +void completeRemove(size_t id, const char *str) { + struct Node *next = NULL; + for (struct Node *node = head; node; node = next) { + next = node->next; + if (id && node->id != id) continue; + if (strcmp(node->str, str)) continue; + if (match == node) match = NULL; + detach(node); + free(node->str); + free(node); + } +} + +void completeClear(size_t id) { + struct Node *next = NULL; + for (struct Node *node = head; node; node = next) { + next = node->next; + if (node->id != id) continue; + if (match == node) match = NULL; + detach(node); + free(node->str); + free(node); + } +} diff --git a/config.c b/config.c new file mode 100644 index 0000000..3a87948 --- /dev/null +++ b/config.c @@ -0,0 +1,137 @@ +/* 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 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <err.h> +#include <errno.h> +#include <getopt.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "chat.h" + +#define WS "\t " + +static const char *path; +static FILE *file; +static size_t num; +static char *line; +static size_t cap; + +static int clean(int opt) { + if (file) fclose(file); + free(line); + line = NULL; + cap = 0; + return opt; +} + +int getopt_config( + int argc, char *const *argv, + const char *optstring, const struct option *longopts, int *longindex +) { + static int opt; + if (opt >= 0) { + opt = getopt_long(argc, argv, optstring, longopts, longindex); + } + if (opt >= 0) return opt; + + for (;;) { + if (!file) { + if (optind < argc) { + num = 0; + path = argv[optind++]; + file = configOpen(path, "r"); + if (!file) return clean('?'); + } else { + return clean(-1); + } + } + + for (;;) { + ssize_t llen = getline(&line, &cap, file); + if (ferror(file)) { + warn("%s", path); + return clean('?'); + } + if (llen <= 0) break; + if (line[llen - 1] == '\n') line[llen - 1] = '\0'; + num++; + + char *name = line + strspn(line, WS); + size_t len = strcspn(name, WS "="); + if (!name[0] || name[0] == '#') continue; + + const struct option *option; + for (option = longopts; option->name; ++option) { + if (strlen(option->name) != len) continue; + if (!strncmp(option->name, name, len)) break; + } + if (!option->name) { + warnx( + "%s:%zu: unrecognized option `%.*s'", + path, num, (int)len, name + ); + return clean('?'); + } + + char *equal = &name[len] + strspn(&name[len], WS); + if (*equal && *equal != '=') { + warnx( + "%s:%zu: option `%s' missing equals sign", + path, num, option->name + ); + return clean('?'); + } + if (option->has_arg == no_argument && *equal) { + warnx( + "%s:%zu: option `%s' doesn't allow an argument", + path, num, option->name + ); + return clean('?'); + } + if (option->has_arg == required_argument && !*equal) { + warnx( + "%s:%zu: option `%s' requires an argument", + path, num, option->name + ); + return clean(':'); + } + + optarg = NULL; + if (*equal) { + char *arg = &equal[1] + strspn(&equal[1], WS); + optarg = strdup(arg); + if (!optarg) { + warn("getopt_config"); + return clean('?'); + } + } + + if (longindex) *longindex = option - longopts; + if (option->flag) { + *option->flag = option->val; + return 0; + } else { + return option->val; + } + } + + fclose(file); + file = NULL; + } +} diff --git a/configure b/configure new file mode 100755 index 0000000..90e1173 --- /dev/null +++ b/configure @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu + +libs='libcrypto libtls ncursesw' +pkg-config --print-errors $libs + +cat >config.mk <<EOF +CFLAGS += $(pkg-config --cflags $libs) +LDFLAGS += $(pkg-config --libs-only-L $libs) +LDLIBS = $(pkg-config --libs-only-l $libs) +EOF diff --git a/edit.c b/edit.c new file mode 100644 index 0000000..d90d558 --- /dev/null +++ b/edit.c @@ -0,0 +1,207 @@ +/* Copyright (C) 2020 C. McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <limits.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <wchar.h> +#include <wctype.h> + +#include "chat.h" + +enum { Cap = 512 }; +static wchar_t buf[Cap]; +static size_t len; +static size_t pos; + +char *editBuffer(size_t *mbsPos) { + static char mbs[MB_LEN_MAX * Cap]; + + const wchar_t *ptr = buf; + size_t mbsLen = wcsnrtombs(mbs, &ptr, pos, sizeof(mbs) - 1, NULL); + assert(mbsLen != (size_t)-1); + if (mbsPos) *mbsPos = mbsLen; + + ptr = &buf[pos]; + size_t n = wcsnrtombs( + &mbs[mbsLen], &ptr, len - pos, sizeof(mbs) - mbsLen - 1, NULL + ); + assert(n != (size_t)-1); + mbsLen += n; + + mbs[mbsLen] = '\0'; + return mbs; +} + +static struct { + wchar_t buf[Cap]; + size_t len; +} cut; + +static bool reserve(size_t index, size_t count) { + if (len + count > Cap) return false; + memmove(&buf[index + count], &buf[index], sizeof(*buf) * (len - index)); + len += count; + return true; +} + +static void delete(size_t index, size_t count) { + if (index + count > len) return; + if (count > 1) { + memcpy(cut.buf, &buf[index], sizeof(*buf) * count); + cut.len = count; + } + memmove( + &buf[index], &buf[index + count], sizeof(*buf) * (len - index - count) + ); + len -= count; +} + +static struct { + size_t pos; + size_t pre; + size_t len; +} tab; + +static void tabComplete(size_t id) { + if (!tab.len) { + tab.pos = pos; + while (tab.pos && buf[tab.pos - 1] != L' ') tab.pos--; + if (tab.pos == pos) return; + tab.pre = pos - tab.pos; + tab.len = tab.pre; + } + + char mbs[MB_LEN_MAX * Cap]; + const wchar_t *ptr = &buf[tab.pos]; + size_t n = wcsnrtombs(mbs, &ptr, tab.pre, sizeof(mbs) - 1, NULL); + assert(n != (size_t)-1); + mbs[n] = '\0'; + + const char *comp = complete(id, mbs); + if (!comp) comp = complete(id, mbs); + if (!comp) { + tab.len = 0; + return; + } + + wchar_t wcs[Cap]; + n = mbstowcs(wcs, comp, sizeof(wcs)); + assert(n != (size_t)-1); + if (tab.pos + n + 2 > Cap) { + completeReject(); + tab.len = 0; + return; + } + + delete(tab.pos, tab.len); + if (wcs[0] != L'/' && !tab.pos) { + tab.len = n + 2; + reserve(tab.pos, tab.len); + buf[tab.pos + n + 0] = L':'; + buf[tab.pos + n + 1] = L' '; + } else if ( + tab.pos >= 2 && (buf[tab.pos - 2] == L':' || buf[tab.pos - 2] == L',') + ) { + tab.len = n + 2; + reserve(tab.pos, tab.len); + buf[tab.pos - 2] = L','; + buf[tab.pos + n + 0] = L':'; + buf[tab.pos + n + 1] = L' '; + } else { + tab.len = n + 1; + reserve(tab.pos, tab.len); + buf[tab.pos + n] = L' '; + } + memcpy(&buf[tab.pos], wcs, sizeof(*wcs) * n); + pos = tab.pos + tab.len; +} + +static void tabAccept(void) { + completeAccept(); + tab.len = 0; +} + +static void tabReject(void) { + completeReject(); + tab.len = 0; +} + +void edit(size_t id, enum Edit op, wchar_t ch) { + size_t init = pos; + switch (op) { + break; case EditHead: pos = 0; + break; case EditTail: pos = len; + break; case EditPrev: if (pos) pos--; + break; case EditNext: if (pos < len) pos++; + break; case EditPrevWord: { + if (pos) pos--; + while (pos && !iswspace(buf[pos - 1])) pos--; + } + break; case EditNextWord: { + if (pos < len) pos++; + while (pos < len && !iswspace(buf[pos])) pos++; + } + + break; case EditDeleteHead: delete(0, pos); pos = 0; + break; case EditDeleteTail: delete(pos, len - pos); + break; case EditDeletePrev: if (pos) delete(--pos, 1); + break; case EditDeleteNext: delete(pos, 1); + break; case EditDeletePrevWord: { + if (!pos) break; + size_t word = pos - 1; + while (word && !iswspace(buf[word - 1])) word--; + delete(word, pos - word); + pos = word; + } + break; case EditDeleteNextWord: { + if (pos == len) break; + size_t word = pos + 1; + while (word < len && !iswspace(buf[word])) word++; + delete(pos, word - pos); + } + break; case EditPaste: { + if (reserve(pos, cut.len)) { + memcpy(&buf[pos], cut.buf, sizeof(*buf) * cut.len); + pos += cut.len; + } + } + + break; case EditInsert: { + if (reserve(pos, 1)) { + buf[pos++] = ch; + } + } + break; case EditComplete: { + tabComplete(id); + return; + } + break; case EditEnter: { + tabAccept(); + command(id, editBuffer(NULL)); + len = pos = 0; + return; + } + } + + if (pos < init) { + tabReject(); + } else { + tabAccept(); + } +} diff --git a/handle.c b/handle.c new file mode 100644 index 0000000..ce56a51 --- /dev/null +++ b/handle.c @@ -0,0 +1,637 @@ +/* Copyright (C) 2020 C. McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <ctype.h> +#include <err.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> + +#include "chat.h" + +struct Replies replies; + +static const char *CapNames[] = { +#define X(name, id) [id##Bit] = name, + ENUM_CAP +#undef X +}; + +static enum Cap capParse(const char *list) { + enum Cap caps = 0; + while (*list) { + enum Cap cap = 0; + size_t len = strcspn(list, " "); + for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) { + if (len != strlen(CapNames[i])) continue; + if (strncmp(list, CapNames[i], len)) continue; + cap = 1 << i; + break; + } + caps |= cap; + list += len; + if (*list) list++; + } + return caps; +} + +static const char *capList(enum Cap caps) { + static char buf[1024]; + buf[0] = '\0'; + for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) { + if (caps & (1 << i)) { + if (buf[0]) strlcat(buf, " ", sizeof(buf)); + strlcat(buf, CapNames[i], sizeof(buf)); + } + } + return buf; +} + +static void require(struct Message *msg, bool origin, size_t len) { + if (origin) { + if (!msg->nick) errx(EX_PROTOCOL, "%s missing origin", msg->cmd); + if (!msg->user) msg->user = msg->nick; + if (!msg->host) msg->host = msg->user; + } + for (size_t i = 0; i < len; ++i) { + if (msg->params[i]) continue; + errx(EX_PROTOCOL, "%s missing parameter %zu", msg->cmd, 1 + i); + } +} + +static const time_t *tagTime(const struct Message *msg) { + static time_t time; + struct tm tm; + if (!msg->tags[TagTime]) return NULL; + if (!strptime(msg->tags[TagTime], "%FT%T", &tm)) return NULL; + time = timegm(&tm); + return &time; +} + +typedef void Handler(struct Message *msg); + +static void handleErrorNicknameInUse(struct Message *msg) { + if (self.nick) return; + require(msg, false, 2); + ircFormat("NICK :%s_\r\n", msg->params[1]); +} + +static void handleErrorErroneousNickname(struct Message *msg) { + require(msg, false, 3); + errx(EX_CONFIG, "%s: %s", msg->params[1], msg->params[2]); +} + +static void handleCap(struct Message *msg) { + require(msg, false, 3); + enum Cap caps = capParse(msg->params[2]); + if (!strcmp(msg->params[1], "LS")) { + caps &= ~CapSASL; + if (caps) { + ircFormat("CAP REQ :%s\r\n", capList(caps)); + } else { + if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n"); + } + } else if (!strcmp(msg->params[1], "ACK")) { + self.caps |= caps; + if (caps & CapSASL) { + ircFormat("AUTHENTICATE %s\r\n", (self.plain ? "PLAIN" : "EXTERNAL")); + } + if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n"); + } else if (!strcmp(msg->params[1], "NAK")) { + errx(EX_CONFIG, "server does not support %s", msg->params[2]); + } +} + +static void handleAuthenticate(struct Message *msg) { + (void)msg; + if (!self.plain) { + ircFormat("AUTHENTICATE +\r\n"); + return; + } + + byte buf[299]; + size_t len = 1 + strlen(self.plain); + if (sizeof(buf) < len) errx(EX_CONFIG, "SASL PLAIN is too long"); + buf[0] = 0; + for (size_t i = 0; self.plain[i]; ++i) { + buf[1 + i] = (self.plain[i] == ':' ? 0 : self.plain[i]); + } + + char b64[BASE64_SIZE(sizeof(buf))]; + base64(b64, buf, len); + ircFormat("AUTHENTICATE "); + ircSend(b64, BASE64_SIZE(len)); + ircFormat("\r\n"); + + explicit_bzero(b64, sizeof(b64)); + explicit_bzero(buf, sizeof(buf)); + explicit_bzero(self.plain, strlen(self.plain)); +} + +static void handleReplyLoggedIn(struct Message *msg) { + (void)msg; + ircFormat("CAP END\r\n"); +} + +static void handleErrorSASLFail(struct Message *msg) { + require(msg, false, 2); + errx(EX_CONFIG, "%s", msg->params[1]); +} + +static void handleReplyWelcome(struct Message *msg) { + require(msg, false, 1); + set(&self.nick, msg->params[0]); + completeTouch(Network, self.nick, Default); + if (self.join) { + size_t count = 1; + for (const char *ch = self.join; *ch && *ch != ' '; ++ch) { + if (*ch == ',') count++; + } + ircFormat("JOIN %s\r\n", self.join); + replies.join += count; + replies.topic += count; + replies.names += count; + } +} + +static void handleReplyISupport(struct Message *msg) { + for (size_t i = 1; i < ParamCap; ++i) { + if (!msg->params[i]) break; + char *key = strsep(&msg->params[i], "="); + if (!msg->params[i]) continue; + if (!strcmp(key, "NETWORK")) { + set(&self.network, msg->params[i]); + uiFormat( + Network, Cold, tagTime(msg), + "You arrive in %s", msg->params[i] + ); + } else if (!strcmp(key, "CHANTYPES")) { + set(&self.chanTypes, msg->params[i]); + } else if (!strcmp(key, "PREFIX")) { + strsep(&msg->params[i], ")"); + if (!msg->params[i]) continue; + set(&self.prefixes, msg->params[i]); + } + } +} + +static void handleReplyMOTD(struct Message *msg) { + require(msg, false, 2); + char *line = msg->params[1]; + urlScan(Network, msg->nick, line); + if (!strncmp(line, "- ", 2)) { + uiFormat(Network, Cold, tagTime(msg), "\3%d-\3\t%s", Gray, &line[2]); + } else { + uiFormat(Network, Cold, tagTime(msg), "%s", line); + } +} + +static void handleJoin(struct Message *msg) { + require(msg, true, 1); + size_t id = idFor(msg->params[0]); + if (self.nick && !strcmp(msg->nick, self.nick)) { + if (!self.user) { + set(&self.user, msg->user); + self.color = hash(msg->user); + } + idColors[id] = hash(msg->params[0]); + completeTouch(None, msg->params[0], idColors[id]); + if (replies.join) { + uiShowID(id); + replies.join--; + } + } + completeTouch(id, msg->nick, hash(msg->user)); + if (msg->params[2] && !strcasecmp(msg->params[2], msg->nick)) { + msg->params[2] = NULL; + } + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\t%s%s%sarrives in \3%02d%s\3", + hash(msg->user), msg->nick, + (msg->params[2] ? "(" : ""), + (msg->params[2] ? msg->params[2] : ""), + (msg->params[2] ? ") " : ""), + hash(msg->params[0]), msg->params[0] + ); +} + +static void handlePart(struct Message *msg) { + require(msg, true, 1); + size_t id = idFor(msg->params[0]); + if (self.nick && !strcmp(msg->nick, self.nick)) { + completeClear(id); + } + completeRemove(id, msg->nick); + urlScan(id, msg->nick, msg->params[1]); + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\tleaves \3%02d%s\3%s%s", + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0], + (msg->params[1] ? ": " : ""), + (msg->params[1] ? msg->params[1] : "") + ); +} + +static void handleKick(struct Message *msg) { + require(msg, true, 2); + size_t id = idFor(msg->params[0]); + bool kicked = self.nick && !strcmp(msg->params[1], self.nick); + completeTouch(id, msg->nick, hash(msg->user)); + urlScan(id, msg->nick, msg->params[2]); + uiFormat( + id, (kicked ? Hot : Cold), tagTime(msg), + "%s\3%02d%s\17\tkicks \3%02d%s\3 out of \3%02d%s\3%s%s", + (kicked ? "\26" : ""), + hash(msg->user), msg->nick, + completeColor(id, msg->params[1]), msg->params[1], + hash(msg->params[0]), msg->params[0], + (msg->params[2] ? ": " : ""), + (msg->params[2] ? msg->params[2] : "") + ); + completeRemove(id, msg->params[1]); + if (kicked) completeClear(id); +} + +static void handleNick(struct Message *msg) { + require(msg, true, 1); + if (self.nick && !strcmp(msg->nick, self.nick)) { + set(&self.nick, msg->params[0]); + uiRead(); // Update prompt. + } + size_t id; + while (None != (id = completeID(msg->nick))) { + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\tis now known as \3%02d%s\3", + hash(msg->user), msg->nick, hash(msg->user), msg->params[0] + ); + } + completeReplace(None, msg->nick, msg->params[0]); +} + +static void handleQuit(struct Message *msg) { + require(msg, true, 0); + size_t id; + while (None != (id = completeID(msg->nick))) { + urlScan(id, msg->nick, msg->params[0]); + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\tleaves%s%s", + hash(msg->user), msg->nick, + (msg->params[0] ? ": " : ""), + (msg->params[0] ? msg->params[0] : "") + ); + } + completeRemove(None, msg->nick); +} + +static void handleReplyNames(struct Message *msg) { + require(msg, false, 4); + size_t id = idFor(msg->params[2]); + char buf[1024]; + size_t len = 0; + while (msg->params[3]) { + char *name = strsep(&msg->params[3], " "); + name += strspn(name, self.prefixes); + char *nick = strsep(&name, "!"); + char *user = strsep(&name, "@"); + enum Color color = (user ? hash(user) : Default); + completeAdd(id, nick, color); + if (!replies.names) continue; + int n = snprintf( + &buf[len], sizeof(buf) - len, + "%s\3%02d%s\3", (len ? ", " : ""), color, nick + ); + assert(n > 0 && len + n < sizeof(buf)); + len += n; + } + if (!replies.names) return; + uiFormat( + id, Cold, tagTime(msg), + "In \3%02d%s\3 are %s", + hash(msg->params[2]), msg->params[2], buf + ); +} + +static void handleReplyEndOfNames(struct Message *msg) { + (void)msg; + if (replies.names) replies.names--; +} + +static void handleReplyNoTopic(struct Message *msg) { + require(msg, false, 2); + if (!replies.topic) return; + replies.topic--; + uiFormat( + idFor(msg->params[1]), Cold, tagTime(msg), + "There is no sign in \3%02d%s\3", + hash(msg->params[1]), msg->params[1] + ); +} + +static void handleReplyTopic(struct Message *msg) { + require(msg, false, 3); + if (!replies.topic) return; + replies.topic--; + size_t id = idFor(msg->params[1]); + urlScan(id, NULL, msg->params[2]); + uiFormat( + id, Cold, tagTime(msg), + "The sign in \3%02d%s\3 reads: %s", + hash(msg->params[1]), msg->params[1], msg->params[2] + ); +} + +static void handleTopic(struct Message *msg) { + require(msg, true, 2); + size_t id = idFor(msg->params[0]); + if (msg->params[1][0]) { + urlScan(id, msg->nick, msg->params[1]); + uiFormat( + id, Warm, tagTime(msg), + "\3%02d%s\3\tplaces a new sign in \3%02d%s\3: %s", + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0], + msg->params[1] + ); + } else { + uiFormat( + id, Warm, tagTime(msg), + "\3%02d%s\3\tremoves the sign in \3%02d%s\3", + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0] + ); + } +} + +static void handleReplyWhoisUser(struct Message *msg) { + require(msg, false, 6); + if (!replies.whois) return; + completeTouch(Network, msg->params[1], hash(msg->params[2])); + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis %s!%s@%s (%s)", + hash(msg->params[2]), msg->params[1], + msg->params[1], msg->params[2], msg->params[3], msg->params[5] + ); +} + +static void handleReplyWhoisServer(struct Message *msg) { + require(msg, false, 4); + if (!replies.whois) return; + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis connected to %s (%s)", + completeColor(Network, msg->params[1]), msg->params[1], + msg->params[2], msg->params[3] + ); +} + +static void handleReplyWhoisIdle(struct Message *msg) { + require(msg, false, 3); + if (!replies.whois) return; + unsigned long idle = strtoul(msg->params[2], NULL, 10); + const char *unit = "second"; + if (idle / 60) { idle /= 60; unit = "minute"; } + if (idle / 60) { idle /= 60; unit = "hour"; } + if (idle / 24) { idle /= 24; unit = "day"; } + time_t signon = (msg->params[3] ? strtoul(msg->params[3], NULL, 10) : 0); + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis idle for %lu %s%s%s%.*s", + completeColor(Network, msg->params[1]), msg->params[1], + idle, unit, (idle != 1 ? "s" : ""), + (signon ? ", signed on " : ""), + 24, (signon ? ctime(&signon) : "") + ); +} + +static void handleReplyWhoisChannels(struct Message *msg) { + require(msg, false, 3); + if (!replies.whois) return; + char buf[1024]; + size_t len = 0; + while (msg->params[2]) { + char *channel = strsep(&msg->params[2], " "); + channel += strspn(channel, self.prefixes); + int n = snprintf( + &buf[len], sizeof(buf) - len, + "%s\3%02d%s\3", (len ? ", " : ""), hash(channel), channel + ); + assert(n > 0 && len + n < sizeof(buf)); + len += n; + } + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis in %s", + completeColor(Network, msg->params[1]), msg->params[1], buf + ); +} + +static void handleReplyWhoisGeneric(struct Message *msg) { + require(msg, false, 3); + if (!replies.whois) return; + if (msg->params[3]) { + msg->params[0] = msg->params[2]; + msg->params[2] = msg->params[3]; + msg->params[3] = msg->params[0]; + } + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\t%s%s%s", + completeColor(Network, msg->params[1]), msg->params[1], + msg->params[2], + (msg->params[3] ? " " : ""), + (msg->params[3] ? msg->params[3] : "") + ); +} + +static void handleReplyEndOfWhois(struct Message *msg) { + require(msg, false, 2); + if (!replies.whois) return; + if (!self.nick || strcmp(msg->params[1], self.nick)) { + completeRemove(Network, msg->params[1]); + } + replies.whois--; +} + +static bool isAction(struct Message *msg) { + if (strncmp(msg->params[1], "\1ACTION ", 8)) return false; + msg->params[1] += 8; + size_t len = strlen(msg->params[1]); + if (msg->params[1][len - 1] == '\1') msg->params[1][len - 1] = '\0'; + return true; +} + +static bool isMention(const struct Message *msg) { + if (!self.nick) return false; + size_t len = strlen(self.nick); + const char *match = msg->params[1]; + while (NULL != (match = strcasestr(match, self.nick))) { + char a = (match > msg->params[1] ? match[-1] : ' '); + char b = (match[len] ? match[len] : ' '); + if ((isspace(a) || ispunct(a)) && (isspace(b) || ispunct(b))) { + return true; + } + match = &match[len]; + } + return false; +} + +static const char *colorMentions(size_t id, struct Message *msg) { + char *split = strchr(msg->params[1], ':'); + if (!split) split = strchr(msg->params[1], ' '); + if (!split) split = &msg->params[1][strlen(msg->params[1])]; + for (char *ch = msg->params[1]; ch < split; ++ch) { + if (iscntrl(*ch)) return ""; + } + char delimit = *split; + char *mention = msg->params[1]; + msg->params[1] = (delimit ? &split[1] : split); + *split = '\0'; + + static char buf[1024]; + FILE *str = fmemopen(buf, sizeof(buf), "w"); + if (!str) err(EX_OSERR, "fmemopen"); + + while (*mention) { + size_t skip = strspn(mention, ",<> "); + fwrite(mention, skip, 1, str); + mention += skip; + + size_t len = strcspn(mention, ",<> "); + char punct = mention[len]; + mention[len] = '\0'; + fprintf(str, "\3%02d%s\3", completeColor(id, mention), mention); + mention[len] = punct; + mention += len; + } + fputc(delimit, str); + + fclose(str); + buf[sizeof(buf) - 1] = '\0'; + return buf; +} + +static void handlePrivmsg(struct Message *msg) { + require(msg, true, 2); + bool query = !strchr(self.chanTypes, msg->params[0][0]); + bool network = strchr(msg->nick, '.'); + bool mine = self.nick && !strcmp(msg->nick, self.nick); + size_t id; + if (query && network) { + id = Network; + } else if (query && !mine) { + id = idFor(msg->nick); + idColors[id] = hash(msg->user); + } else { + id = idFor(msg->params[0]); + } + + bool notice = (msg->cmd[0] == 'N'); + bool action = isAction(msg); + bool mention = !mine && isMention(msg); + if (!notice && !mine) completeTouch(id, msg->nick, hash(msg->user)); + urlScan(id, msg->nick, msg->params[1]); + if (notice) { + uiFormat( + id, Warm, tagTime(msg), + "%s\3%d-%s-\17\3%d\t%s", + (mention ? "\26" : ""), hash(msg->user), msg->nick, + LightGray, msg->params[1] + ); + } else if (action) { + uiFormat( + id, (mention || query ? Hot : Warm), tagTime(msg), + "%s\35\3%d* %s\17\35\t%s", + (mention ? "\26" : ""), hash(msg->user), msg->nick, msg->params[1] + ); + } else { + const char *mentions = colorMentions(id, msg); + uiFormat( + id, (mention || query ? Hot : Warm), tagTime(msg), + "%s\3%d<%s>\17\t%s%s", + (mention ? "\26" : ""), hash(msg->user), msg->nick, + mentions, msg->params[1] + ); + } +} + +static void handlePing(struct Message *msg) { + require(msg, false, 1); + ircFormat("PONG :%s\r\n", msg->params[0]); +} + +static void handleError(struct Message *msg) { + require(msg, false, 1); + errx(EX_UNAVAILABLE, "%s", msg->params[0]); +} + +static const struct Handler { + const char *cmd; + Handler *fn; +} Handlers[] = { + { "001", handleReplyWelcome }, + { "005", handleReplyISupport }, + { "276", handleReplyWhoisGeneric }, + { "307", handleReplyWhoisGeneric }, + { "311", handleReplyWhoisUser }, + { "312", handleReplyWhoisServer }, + { "313", handleReplyWhoisGeneric }, + { "317", handleReplyWhoisIdle }, + { "318", handleReplyEndOfWhois }, + { "319", handleReplyWhoisChannels }, + { "330", handleReplyWhoisGeneric }, + { "331", handleReplyNoTopic }, + { "332", handleReplyTopic }, + { "353", handleReplyNames }, + { "366", handleReplyEndOfNames }, + { "372", handleReplyMOTD }, + { "432", handleErrorErroneousNickname }, + { "433", handleErrorNicknameInUse }, + { "671", handleReplyWhoisGeneric }, + { "900", handleReplyLoggedIn }, + { "904", handleErrorSASLFail }, + { "905", handleErrorSASLFail }, + { "906", handleErrorSASLFail }, + { "AUTHENTICATE", handleAuthenticate }, + { "CAP", handleCap }, + { "ERROR", handleError }, + { "JOIN", handleJoin }, + { "KICK", handleKick }, + { "NICK", handleNick }, + { "NOTICE", handlePrivmsg }, + { "PART", handlePart }, + { "PING", handlePing }, + { "PRIVMSG", handlePrivmsg }, + { "QUIT", handleQuit }, + { "TOPIC", handleTopic }, +}; + +static int compar(const void *cmd, const void *_handler) { + const struct Handler *handler = _handler; + return strcmp(cmd, handler->cmd); +} + +void handle(struct Message msg) { + if (!msg.cmd) return; + const struct Handler *handler = bsearch( + msg.cmd, Handlers, ARRAY_LEN(Handlers), sizeof(*handler), compar + ); + if (handler) handler->fn(&msg); +} diff --git a/irc.c b/irc.c new file mode 100644 index 0000000..05f8f9d --- /dev/null +++ b/irc.c @@ -0,0 +1,250 @@ +/* Copyright (C) 2020 C. McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <err.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 <sys/stat.h> +#include <sysexits.h> +#include <tls.h> +#include <unistd.h> + +#include "chat.h" + +struct tls *client; + +static byte *readFile(size_t *len, FILE *file) { + struct stat stat; + int error = fstat(fileno(file), &stat); + if (error) err(EX_IOERR, "fstat"); + + byte *buf = malloc(stat.st_size); + if (!buf) err(EX_OSERR, "malloc"); + + rewind(file); + *len = fread(buf, 1, stat.st_size, file); + if (ferror(file)) err(EX_IOERR, "fread"); + + return buf; +} + +void ircConfig(bool insecure, FILE *cert, FILE *priv) { + struct tls_config *config = tls_config_new(); + if (!config) errx(EX_SOFTWARE, "tls_config_new"); + + int error = tls_config_set_ciphers(config, "compat"); + if (error) { + errx( + EX_SOFTWARE, "tls_config_set_ciphers: %s", + tls_config_error(config) + ); + } + + if (insecure) { + tls_config_insecure_noverifycert(config); + tls_config_insecure_noverifyname(config); + } + + if (cert) { + size_t len; + byte *buf = readFile(&len, cert); + error = tls_config_set_cert_mem(config, buf, len); + if (error) { + errx( + EX_CONFIG, "tls_config_set_cert_mem: %s", + tls_config_error(config) + ); + } + if (priv) { + free(buf); + buf = readFile(&len, priv); + } + error = tls_config_set_key_mem(config, buf, len); + if (error) { + errx( + EX_CONFIG, "tls_config_set_key_mem: %s", + tls_config_error(config) + ); + } + explicit_bzero(buf, len); + free(buf); + } + + client = tls_client(); + if (!client) errx(EX_SOFTWARE, "tls_client"); + + error = tls_configure(client, config); + if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client)); + tls_config_free(config); +} + +int ircConnect(const char *host, const char *port) { + assert(client); + + struct addrinfo *head; + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + .ai_protocol = IPPROTO_TCP, + }; + int error = getaddrinfo(host, port, &hints, &head); + if (error) errx(EX_NOHOST, "%s:%s: %s", host, port, 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, "%s:%s", host, port); + freeaddrinfo(head); + + error = tls_connect_socket(client, sock, host); + if (error) errx(EX_PROTOCOL, "tls_connect: %s", tls_error(client)); + + error = tls_handshake(client); + if (error) errx(EX_PROTOCOL, "tls_handshake: %s", tls_error(client)); + + return sock; +} + +static void debug(char dir, const char *line) { + if (!self.debug) return; + size_t len = strcspn(line, "\r\n"); + uiFormat( + Debug, Cold, NULL, "\3%d%c%c\3\t%.*s", + Gray, dir, dir, (int)len, line + ); + if (!isatty(STDERR_FILENO)) { + fprintf(stderr, "%c%c %.*s\n", dir, dir, (int)len, line); + } +} + +void ircSend(const char *ptr, size_t len) { + assert(client); + 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 ircFormat(const char *format, ...) { + char buf[1024]; + va_list ap; + va_start(ap, format); + int len = vsnprintf(buf, sizeof(buf), format, ap); + va_end(ap); + assert((size_t)len < sizeof(buf)); + debug('<', buf); + ircSend(buf, len); +} + +static const char *TagNames[TagCap] = { +#define X(name, id) [id] = name, + ENUM_TAG +#undef X +}; + +static void unescape(char *tag) { + for (;;) { + tag = strchr(tag, '\\'); + if (!tag) break; + switch (tag[1]) { + break; case ':': tag[1] = ';'; + break; case 's': tag[1] = ' '; + break; case 'r': tag[1] = '\r'; + break; case 'n': tag[1] = '\n'; + } + memmove(tag, &tag[1], strlen(&tag[1]) + 1); + if (tag[0]) tag = &tag[1]; + } +} + +static struct Message parse(char *line) { + struct Message msg = { .cmd = NULL }; + + if (line[0] == '@') { + char *tags = 1 + strsep(&line, " "); + while (tags) { + char *tag = strsep(&tags, ";"); + char *key = strsep(&tag, "="); + for (size_t i = 0; i < TagCap; ++i) { + if (strcmp(key, TagNames[i])) continue; + unescape(tag); + msg.tags[i] = tag; + break; + } + } + } + + if (line[0] == ':') { + char *origin = 1 + strsep(&line, " "); + msg.nick = strsep(&origin, "!"); + msg.user = strsep(&origin, "@"); + msg.host = origin; + } + + msg.cmd = strsep(&line, " "); + for (size_t i = 0; line && i < ParamCap; ++i) { + if (line[0] == ':') { + msg.params[i] = &line[1]; + break; + } + msg.params[i] = strsep(&line, " "); + } + + return msg; +} + +void ircRecv(void) { + static char buf[8191 + 512]; + static size_t len = 0; + + assert(client); + ssize_t ret = tls_read(client, &buf[len], sizeof(buf) - len); + if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) return; + if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client)); + if (!ret) errx(EX_PROTOCOL, "server closed connection"); + len += ret; + + char *crlf; + char *line = buf; + for (;;) { + crlf = memmem(line, &buf[len] - line, "\r\n", 2); + if (!crlf) break; + *crlf = '\0'; + debug('>', line); + handle(parse(line)); + line = crlf + 2; + } + + len -= line - buf; + memmove(buf, line, len); +} diff --git a/ui.c b/ui.c new file mode 100644 index 0000000..6c9606d --- /dev/null +++ b/ui.c @@ -0,0 +1,974 @@ +/* Copyright (C) 2020 C. McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#define _XOPEN_SOURCE_EXTENDED + +#include <assert.h> +#include <ctype.h> +#include <curses.h> +#include <err.h> +#include <errno.h> +#include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <term.h> +#include <termios.h> +#include <time.h> +#include <unistd.h> +#include <wchar.h> +#include <wctype.h> + +#include "chat.h" + +// Annoying stuff from <term.h>: +#undef lines +#undef tab + +#ifndef A_ITALIC +#define A_ITALIC A_NORMAL +#endif + +#define BOTTOM (LINES - 1) +#define RIGHT (COLS - 1) +#define PAGE_LINES (LINES - 2) + +static WINDOW *status; +static WINDOW *marker; +static WINDOW *input; + +enum { BufferCap = 512 }; +struct Buffer { + time_t times[BufferCap]; + char *lines[BufferCap]; + size_t len; +}; +static_assert(!(BufferCap & (BufferCap - 1)), "BufferCap is power of two"); + +static void bufferPush(struct Buffer *buffer, time_t time, const char *line) { + size_t i = buffer->len++ % BufferCap; + free(buffer->lines[i]); + buffer->times[i] = time; + buffer->lines[i] = strdup(line); + if (!buffer->lines[i]) err(EX_OSERR, "strdup"); +} + +static time_t bufferTime(const struct Buffer *buffer, size_t i) { + return buffer->times[(buffer->len + i) % BufferCap]; +} +static const char *bufferLine(const struct Buffer *buffer, size_t i) { + return buffer->lines[(buffer->len + i) % BufferCap]; +} + +enum { WindowLines = BufferCap }; +struct Window { + size_t id; + struct Buffer buffer; + WINDOW *pad; + int scroll; + bool mark; + enum Heat heat; + int unreadCount; + int unreadLines; + struct Window *prev; + struct Window *next; +}; + +static struct { + struct Window *active; + struct Window *other; + struct Window *head; + struct Window *tail; +} windows; + +static void windowAdd(struct Window *window) { + if (windows.tail) windows.tail->next = window; + window->prev = windows.tail; + window->next = NULL; + windows.tail = window; + if (!windows.head) windows.head = window; +} + +static void windowRemove(struct Window *window) { + if (window->prev) window->prev->next = window->next; + if (window->next) window->next->prev = window->prev; + if (windows.head == window) windows.head = window->next; + if (windows.tail == window) windows.tail = window->prev; +} + +static struct Window *windowFor(size_t id) { + struct Window *window; + for (window = windows.head; window; window = window->next) { + if (window->id == id) return window; + } + window = calloc(1, sizeof(*window)); + if (!window) err(EX_OSERR, "malloc"); + + window->id = id; + window->pad = newpad(WindowLines, COLS); + if (!window->pad) err(EX_OSERR, "newpad"); + scrollok(window->pad, true); + wmove(window->pad, WindowLines - 1, 0); + window->mark = true; + + windowAdd(window); + return window; +} + +static short colorPairs; + +static void colorInit(void) { + start_color(); + use_default_colors(); + for (short pair = 0; pair < 16; ++pair) { + init_pair(1 + pair, pair % COLORS, -1); + } + colorPairs = 17; +} + +static attr_t colorAttr(short fg) { + if (fg != COLOR_BLACK && fg % COLORS == COLOR_BLACK) return A_BOLD; + if (COLORS > 8) return A_NORMAL; + return (fg / COLORS & 1 ? A_BOLD : A_NORMAL); +} + +static short colorPair(short fg, short bg) { + fg %= COLORS; + bg %= COLORS; + if (bg == -1 && fg < 16) return 1 + fg; + for (short pair = 17; pair < colorPairs; ++pair) { + short f, b; + pair_content(pair, &f, &b); + if (f == fg && b == bg) return pair; + } + init_pair(colorPairs, fg, bg); + return colorPairs++; +} + +// XXX: Assuming terminals will be fine with these even if they're unsupported, +// since they're "private" modes. +static const char *EnterFocusMode = "\33[?1004h"; +static const char *ExitFocusMode = "\33[?1004l"; +static const char *EnterPasteMode = "\33[?2004h"; +static const char *ExitPasteMode = "\33[?2004l"; + +// Gain use of C-q, C-s, C-c, C-z, C-y, C-o. +static void acquireKeys(void) { + struct termios term; + int error = tcgetattr(STDOUT_FILENO, &term); + if (error) err(EX_OSERR, "tcgetattr"); + term.c_iflag &= ~IXON; + term.c_cc[VINTR] = _POSIX_VDISABLE; + term.c_cc[VSUSP] = _POSIX_VDISABLE; + term.c_cc[VDSUSP] = _POSIX_VDISABLE; + term.c_cc[VDISCARD] = _POSIX_VDISABLE; + error = tcsetattr(STDOUT_FILENO, TCSADRAIN, &term); + if (error) err(EX_OSERR, "tcsetattr"); +} + +static void errExit(void) { + reset_shell_mode(); +} + +#define ENUM_KEY \ + X(KeyMeta0, "\0330") \ + X(KeyMeta1, "\0331") \ + X(KeyMeta2, "\0332") \ + X(KeyMeta3, "\0333") \ + X(KeyMeta4, "\0334") \ + X(KeyMeta5, "\0335") \ + X(KeyMeta6, "\0336") \ + X(KeyMeta7, "\0337") \ + X(KeyMeta8, "\0338") \ + X(KeyMeta9, "\0339") \ + X(KeyMetaA, "\33a") \ + X(KeyMetaB, "\33b") \ + X(KeyMetaD, "\33d") \ + X(KeyMetaF, "\33f") \ + X(KeyMetaL, "\33l") \ + X(KeyMetaM, "\33m") \ + X(KeyMetaU, "\33u") \ + X(KeyMetaSlash, "\33/") \ + X(KeyFocusIn, "\33[I") \ + X(KeyFocusOut, "\33[O") \ + X(KeyPasteOn, "\33[200~") \ + X(KeyPasteOff, "\33[201~") + +enum { + KeyMax = KEY_MAX, +#define X(id, seq) id, + ENUM_KEY +#undef X +}; + +void uiInit(void) { + initscr(); + cbreak(); + noecho(); + acquireKeys(); + def_prog_mode(); + atexit(errExit); + colorInit(); + + if (!to_status_line && !strncmp(termname(), "xterm", 5)) { + to_status_line = "\33]2;"; + from_status_line = "\7"; + } + +#define X(id, seq) define_key(seq, id); + ENUM_KEY +#undef X + + status = newwin(1, COLS, 0, 0); + if (!status) err(EX_OSERR, "newwin"); + + marker = newwin(1, COLS, LINES - 2, 0); + short fg = 8 + COLOR_BLACK; + wbkgd(marker, '~' | colorAttr(fg) | COLOR_PAIR(colorPair(fg, -1))); + + input = newpad(1, 512); + if (!input) err(EX_OSERR, "newpad"); + keypad(input, true); + nodelay(input, true); + + windows.active = windowFor(Network); + uiShow(); +} + +static bool hidden; +static bool waiting; + +static char title[256]; +static char prevTitle[sizeof(title)]; + +void uiDraw(void) { + if (hidden) return; + wnoutrefresh(status); + struct Window *window = windows.active; + pnoutrefresh( + window->pad, + WindowLines - window->scroll - PAGE_LINES + !!window->scroll, 0, + 1, 0, + BOTTOM - 1 - !!window->scroll, RIGHT + ); + if (window->scroll) { + touchwin(marker); + wnoutrefresh(marker); + } + int y, x; + getyx(input, y, x); + pnoutrefresh( + input, + 0, (x + 1 > RIGHT ? x + 1 - RIGHT : 0), + BOTTOM, 0, + BOTTOM, RIGHT + ); + doupdate(); + + if (!to_status_line) return; + if (!strcmp(title, prevTitle)) return; + strcpy(prevTitle, title); + putp(to_status_line); + putp(title); + putp(from_status_line); + fflush(stdout); +} + +void uiShow(void) { + prevTitle[0] = '\0'; + putp(EnterFocusMode); + putp(EnterPasteMode); + fflush(stdout); + hidden = false; +} + +void uiHide(void) { + hidden = true; + putp(ExitFocusMode); + putp(ExitPasteMode); + endwin(); +} + +struct Style { + attr_t attr; + enum Color fg, bg; +}; +static const struct Style Reset = { A_NORMAL, Default, Default }; + +static const short Colors[100] = { + [Default] = -1, + [White] = 8 + COLOR_WHITE, + [Black] = 0 + COLOR_BLACK, + [Blue] = 0 + COLOR_BLUE, + [Green] = 0 + COLOR_GREEN, + [Red] = 8 + COLOR_RED, + [Brown] = 0 + COLOR_RED, + [Magenta] = 0 + COLOR_MAGENTA, + [Orange] = 0 + COLOR_YELLOW, + [Yellow] = 8 + COLOR_YELLOW, + [LightGreen] = 8 + COLOR_GREEN, + [Cyan] = 0 + COLOR_CYAN, + [LightCyan] = 8 + COLOR_CYAN, + [LightBlue] = 8 + COLOR_BLUE, + [Pink] = 8 + COLOR_MAGENTA, + [Gray] = 8 + COLOR_BLACK, + [LightGray] = 0 + COLOR_WHITE, + 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89, + 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125, + 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161, + 196, 208, 226, 154, 46, 86, 51, 75, 21, 171, 201, 198, + 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205, + 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212, + 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, +}; + +enum { B = '\2', C = '\3', O = '\17', R = '\26', I = '\35', U = '\37' }; + +static void styleParse(struct Style *style, const char **str, size_t *len) { + switch (**str) { + break; case B: (*str)++; style->attr ^= A_BOLD; + break; case O: (*str)++; *style = Reset; + break; case R: (*str)++; style->attr ^= A_REVERSE; + break; case I: (*str)++; style->attr ^= A_ITALIC; + break; case U: (*str)++; style->attr ^= A_UNDERLINE; + break; case C: { + (*str)++; + if (!isdigit(**str)) { + style->fg = Default; + style->bg = Default; + break; + } + style->fg = *(*str)++ - '0'; + if (isdigit(**str)) style->fg = style->fg * 10 + *(*str)++ - '0'; + if ((*str)[0] != ',' || !isdigit((*str)[1])) break; + (*str)++; + style->bg = *(*str)++ - '0'; + if (isdigit(**str)) style->bg = style->bg * 10 + *(*str)++ - '0'; + } + } + *len = strcspn(*str, (const char[]) { B, C, O, R, I, U, '\0' }); +} + +static void statusAdd(const char *str) { + size_t len; + struct Style style = Reset; + while (*str) { + styleParse(&style, &str, &len); + wattr_set( + status, + style.attr | colorAttr(Colors[style.fg]), + colorPair(Colors[style.fg], Colors[style.bg]), + NULL + ); + waddnstr(status, str, len); + str += len; + } +} + +static void statusUpdate(void) { + int otherUnread = 0; + enum Heat otherHeat = Cold; + wmove(status, 0, 0); + + int num; + const struct Window *window; + for (num = 0, window = windows.head; window; ++num, window = window->next) { + if (!window->heat && window != windows.active) continue; + if (window != windows.active) { + otherUnread += window->unreadCount; + if (window->heat > otherHeat) otherHeat = window->heat; + } + int trunc; + char buf[256]; + snprintf( + buf, sizeof(buf), "\3%d%s %d %s %n(\3%02d%d\3%d) ", + idColors[window->id], (window == windows.active ? "\26" : ""), + num, idNames[window->id], + &trunc, (window->heat > Warm ? White : idColors[window->id]), + window->unreadCount, + idColors[window->id] + ); + if (!window->mark || !window->unreadCount) buf[trunc] = '\0'; + statusAdd(buf); + } + wclrtoeol(status); + + window = windows.active; + snprintf(title, sizeof(title), "%s %s", self.network, idNames[window->id]); + if (window->mark && window->unreadCount) { + snprintf( + &title[strlen(title)], sizeof(title) - strlen(title), + " (%d%s)", window->unreadCount, (window->heat > Warm ? "!" : "") + ); + } + if (otherUnread) { + snprintf( + &title[strlen(title)], sizeof(title) - strlen(title), + " (+%d%s)", otherUnread, (otherHeat > Warm ? "!" : "") + ); + } +} + +static void mark(struct Window *window) { + if (window->scroll) return; + window->mark = true; + window->unreadCount = 0; + window->unreadLines = 0; +} + +static void unmark(struct Window *window) { + if (!window->scroll) { + window->mark = false; + window->heat = Cold; + } + statusUpdate(); +} + +static void windowScroll(struct Window *window, int n) { + mark(window); + window->scroll += n; + if (window->scroll > WindowLines - PAGE_LINES) { + window->scroll = WindowLines - PAGE_LINES; + } + if (window->scroll < 0) window->scroll = 0; + unmark(window); +} + +static void windowScrollUnread(struct Window *window) { + window->scroll = 0; + windowScroll(window, window->unreadLines - PAGE_LINES + 1); +} + +static int wordWidth(const char *str) { + size_t len = strcspn(str, " "); + int width = 0; + while (len) { + wchar_t wc; + int n = mbtowc(&wc, str, len); + if (n < 1) return width + len; + width += (iswprint(wc) ? wcwidth(wc) : 0); + str += n; + len -= n; + } + return width; +} + +static int wordWrap(WINDOW *win, const char *str) { + int y, x, width; + getmaxyx(win, y, width); + + size_t len; + int lines = 0; + int align = 0; + struct Style style = Reset; + while (*str) { + if (*str == '\t') { + if (align) { + waddch(win, '\t'); + str++; + } else { + waddch(win, ' '); + getyx(win, y, align); + str++; + } + } else if (*str == ' ') { + getyx(win, y, x); + const char *word = &str[strspn(str, " ")]; + if (width - x - 1 <= wordWidth(word)) { + lines += 1 + (align + wordWidth(word)) / width; + waddch(win, '\n'); + getyx(win, y, x); + wmove(win, y, align); + str = word; + } else { + waddch(win, ' '); + str++; + } + } + + styleParse(&style, &str, &len); + size_t ws = strcspn(str, "\t "); + if (ws < len) len = ws; + + wattr_set( + win, + style.attr | colorAttr(Colors[style.fg]), + colorPair(Colors[style.fg], Colors[style.bg]), + NULL + ); + waddnstr(win, str, len); + str += len; + } + return lines; +} + +void uiWrite(size_t id, enum Heat heat, const time_t *src, const char *str) { + struct Window *window = windowFor(id); + time_t clock = (src ? *src : time(NULL)); + bufferPush(&window->buffer, clock, str); + + int lines = 1; + waddch(window->pad, '\n'); + if (window->mark && heat > Cold) { + if (!window->unreadCount++) { + lines++; + waddch(window->pad, '\n'); + } + if (window->heat < heat) window->heat = heat; + statusUpdate(); + } + lines += wordWrap(window->pad, str); + window->unreadLines += lines; + if (window->scroll) windowScroll(window, lines); + if (heat > Warm) beep(); +} + +void uiFormat( + size_t id, enum Heat heat, const time_t *time, const char *format, ... +) { + char buf[1024]; + va_list ap; + va_start(ap, format); + int len = vsnprintf(buf, sizeof(buf), format, ap); + va_end(ap); + assert((size_t)len < sizeof(buf)); + uiWrite(id, heat, time, buf); +} + +static void reflow(struct Window *window) { + werase(window->pad); + wmove(window->pad, WindowLines - 1, 0); + window->unreadLines = 0; + for (size_t i = 0; i < BufferCap; ++i) { + const char *line = bufferLine(&window->buffer, i); + if (!line) continue; + waddch(window->pad, '\n'); + if (i >= (size_t)(BufferCap - window->unreadCount)) { + window->unreadLines += 1 + wordWrap(window->pad, line); + } else { + wordWrap(window->pad, line); + } + } +} + +static void resize(void) { + mvwin(marker, LINES - 2, 0); + int height, width; + getmaxyx(windows.active->pad, height, width); + if (width == COLS) return; + for (struct Window *window = windows.head; window; window = window->next) { + wresize(window->pad, BufferCap, COLS); + reflow(window); + } + statusUpdate(); +} + +static void bufferList(const struct Buffer *buffer) { + uiHide(); + waiting = true; + for (size_t i = 0; i < BufferCap; ++i) { + time_t time = bufferTime(buffer, i); + const char *line = bufferLine(buffer, i); + if (!line) continue; + + struct tm *tm = localtime(&time); + if (!tm) continue; + char buf[sizeof("[00:00:00]")]; + strftime(buf, sizeof(buf), "[%T]", tm); + vid_attr(colorAttr(Colors[Gray]), colorPair(Colors[Gray], -1), NULL); + printf("%s ", buf); + + size_t len; + bool align = false; + struct Style style = Reset; + while (*line) { + if (*line == '\t') { + printf("%c", (align ? '\t' : ' ')); + align = true; + line++; + } + styleParse(&style, &line, &len); + size_t tab = strcspn(line, "\t"); + if (tab < len) len = tab; + vid_attr( + style.attr | colorAttr(Colors[style.fg]), + colorPair(Colors[style.fg], Colors[style.bg]), + NULL + ); + if (len) printf("%.*s", (int)len, line); + line += len; + } + printf("\n"); + } +} + +static void inputAdd(struct Style *style, const char *str) { + size_t len; + while (*str) { + const char *code = str; + styleParse(style, &str, &len); + wattr_set(input, A_BOLD | A_REVERSE, 0, NULL); + switch (*code) { + break; case B: waddch(input, 'B'); + break; case C: waddch(input, 'C'); + break; case O: waddch(input, 'O'); + break; case R: waddch(input, 'R'); + break; case I: waddch(input, 'I'); + break; case U: waddch(input, 'U'); + } + if (str - code > 1) waddnstr(input, &code[1], str - &code[1]); + wattr_set( + input, + style->attr | colorAttr(Colors[style->fg]), + colorPair(Colors[style->fg], Colors[style->bg]), + NULL + ); + waddnstr(input, str, len); + str += len; + } +} + +static void inputUpdate(void) { + size_t id = windows.active->id; + size_t pos; + char *buf = editBuffer(&pos); + + const char *skip = NULL; + struct Style init = { .fg = self.color, .bg = Default }; + struct Style rest = Reset; + const char *prefix = ""; + const char *prompt = (self.nick ? self.nick : ""); + const char *suffix = ""; + if (NULL != (skip = commandIsPrivmsg(id, buf))) { + prefix = "<"; suffix = "> "; + } else if (NULL != (skip = commandIsNotice(id, buf))) { + prefix = "-"; suffix = "- "; + rest.fg = LightGray; + } else if (NULL != (skip = commandIsAction(id, buf))) { + init.attr |= A_ITALIC; + prefix = "* "; suffix = " "; + rest.attr |= A_ITALIC; + } else if (id == Debug) { + skip = buf; + init.fg = Gray; + prompt = "<< "; + } else { + prompt = ""; + } + if (skip && skip > &buf[pos]) { + skip = NULL; + prefix = prompt = suffix = ""; + } + + int y, x; + wmove(input, 0, 0); + wattr_set( + input, + init.attr | colorAttr(Colors[init.fg]), + colorPair(Colors[init.fg], Colors[init.bg]), + NULL + ); + waddstr(input, prefix); + waddstr(input, prompt); + waddstr(input, suffix); + struct Style style = rest; + char p = buf[pos]; + buf[pos] = '\0'; + inputAdd(&style, (skip ? skip : buf)); + getyx(input, y, x); + buf[pos] = p; + inputAdd(&style, &buf[pos]); + wclrtoeol(input); + wmove(input, y, x); +} + +static void windowShow(struct Window *window) { + if (!window) return; + touchwin(window->pad); + windows.other = windows.active; + windows.active = window; + mark(windows.other); + unmark(windows.active); + inputUpdate(); +} + +void uiShowID(size_t id) { + windowShow(windowFor(id)); +} + +void uiShowNum(size_t num) { + struct Window *window = windows.head; + for (size_t i = 0; i < num; ++i) { + window = window->next; + if (!window) return; + } + windowShow(window); +} + +static void windowClose(struct Window *window) { + if (window->id == Network) return; + if (windows.active == window) { + if (windows.other && windows.other != window) { + windowShow(windows.other); + } else { + windowShow(window->prev ? window->prev : window->next); + } + } + if (windows.other == window) windows.other = NULL; + windowRemove(window); + for (size_t i = 0; i < BufferCap; ++i) { + free(window->buffer.lines[i]); + } + delwin(window->pad); + free(window); + statusUpdate(); +} + +void uiCloseID(size_t id) { + windowClose(windowFor(id)); +} + +void uiCloseNum(size_t num) { + struct Window *window = windows.head; + for (size_t i = 0; i < num; ++i) { + window = window->next; + if (!window) return; + } + windowClose(window); +} + +static void showAuto(void) { + static struct Window *other; + if (windows.other != other) { + other = windows.active; + } + for (struct Window *window = windows.head; window; window = window->next) { + if (window->heat < Hot) continue; + windowShow(window); + windows.other = other; + return; + } + for (struct Window *window = windows.head; window; window = window->next) { + if (window->heat < Warm) continue; + windowShow(window); + windows.other = other; + return; + } + windowShow(windows.other); +} + +static void keyCode(int code) { + struct Window *window = windows.active; + size_t id = window->id; + switch (code) { + break; case KEY_RESIZE: resize(); + break; case KeyFocusIn: unmark(window); + break; case KeyFocusOut: mark(window); + break; case KeyPasteOn:; // TODO + break; case KeyPasteOff:; // TODO + + break; case KeyMetaSlash: windowShow(windows.other); + + break; case KeyMetaA: showAuto(); + break; case KeyMetaB: edit(id, EditPrevWord, 0); + break; case KeyMetaD: edit(id, EditDeleteNextWord, 0); + break; case KeyMetaF: edit(id, EditNextWord, 0); + break; case KeyMetaL: bufferList(&window->buffer); + break; case KeyMetaM: waddch(window->pad, '\n'); + break; case KeyMetaU: windowScrollUnread(window); + + break; case KEY_BACKSPACE: edit(id, EditDeletePrev, 0); + break; case KEY_DC: edit(id, EditDeleteNext, 0); + break; case KEY_DOWN: windowScroll(window, -1); + break; case KEY_END: edit(id, EditTail, 0); + break; case KEY_ENTER: edit(id, EditEnter, 0); + break; case KEY_HOME: edit(id, EditHead, 0); + break; case KEY_LEFT: edit(id, EditPrev, 0); + break; case KEY_NPAGE: windowScroll(window, -(PAGE_LINES - 2)); + break; case KEY_PPAGE: windowScroll(window, +(PAGE_LINES - 2)); + break; case KEY_RIGHT: edit(id, EditNext, 0); + break; case KEY_UP: windowScroll(window, +1); + + break; default: { + if (code >= KeyMeta0 && code <= KeyMeta9) { + uiShowNum(code - KeyMeta0); + } + } + } +} + +static void keyCtrl(wchar_t ch) { + size_t id = windows.active->id; + switch (ch ^ L'@') { + break; case L'?': edit(id, EditDeletePrev, 0); + break; case L'A': edit(id, EditHead, 0); + break; case L'B': edit(id, EditPrev, 0); + break; case L'C': raise(SIGINT); + break; case L'D': edit(id, EditDeleteNext, 0); + break; case L'E': edit(id, EditTail, 0); + break; case L'F': edit(id, EditNext, 0); + break; case L'H': edit(id, EditDeletePrev, 0); + break; case L'I': edit(id, EditComplete, 0); + break; case L'J': edit(id, EditEnter, 0); + break; case L'K': edit(id, EditDeleteTail, 0); + break; case L'L': clearok(curscr, true); + break; case L'N': windowShow(windows.active->next); + break; case L'O': windowShow(windows.other); + break; case L'P': windowShow(windows.active->prev); + break; case L'U': edit(id, EditDeleteHead, 0); + break; case L'W': edit(id, EditDeletePrevWord, 0); + break; case L'Y': edit(id, EditPaste, 0); + } +} + +static void keyStyle(wchar_t ch) { + size_t id = windows.active->id; + switch (iswcntrl(ch) ? ch ^ L'@' : towupper(ch)) { + break; case L'B': edit(id, EditInsert, B); + break; case L'C': edit(id, EditInsert, C); + break; case L'I': edit(id, EditInsert, I); + break; case L'O': edit(id, EditInsert, O); + break; case L'R': edit(id, EditInsert, R); + break; case L'U': edit(id, EditInsert, U); + } +} + +void uiRead(void) { + if (hidden) { + if (waiting) { + uiShow(); + flushinp(); + waiting = false; + } else { + return; + } + } + + int ret; + wint_t ch; + static bool style; + while (ERR != (ret = wget_wch(input, &ch))) { + if (ret == KEY_CODE_YES) { + keyCode(ch); + } else if (ch == (L'Z' ^ L'@')) { + style = true; + continue; + } else if (style) { + keyStyle(ch); + } else if (iswcntrl(ch)) { + keyCtrl(ch); + } else { + edit(windows.active->id, EditInsert, ch); + } + style = false; + } + inputUpdate(); +} + +static const size_t Signatures[] = { + 0x6C72696774616301, +}; + +static size_t signatureVersion(size_t signature) { + for (size_t i = 0; i < ARRAY_LEN(Signatures); ++i) { + if (signature == Signatures[i]) return i; + } + err(EX_DATAERR, "unknown file signature %zX", signature); +} + +static int writeSize(FILE *file, size_t value) { + return (fwrite(&value, sizeof(value), 1, file) ? 0 : -1); +} +static int writeTime(FILE *file, time_t time) { + return (fwrite(&time, sizeof(time), 1, file) ? 0 : -1); +} +static int writeString(FILE *file, const char *str) { + return (fwrite(str, strlen(str) + 1, 1, file) ? 0 : -1); +} + +int uiSave(const char *name) { + FILE *file = dataOpen(name, "w"); + if (!file) return -1; + + if (writeSize(file, Signatures[0])) return -1; + const struct Window *window; + for (window = windows.head; window; window = window->next) { + if (writeString(file, idNames[window->id])) return -1; + for (size_t i = 0; i < BufferCap; ++i) { + time_t time = bufferTime(&window->buffer, i); + const char *line = bufferLine(&window->buffer, i); + if (!line) continue; + if (writeTime(file, time)) return -1; + if (writeString(file, line)) return -1; + } + if (writeTime(file, 0)) return -1; + } + return fclose(file); +} + +static size_t readSize(FILE *file) { + size_t value; + fread(&value, sizeof(value), 1, file); + if (ferror(file)) err(EX_IOERR, "fread"); + if (feof(file)) errx(EX_DATAERR, "unexpected eof"); + return value; +} +static time_t readTime(FILE *file) { + time_t time; + fread(&time, sizeof(time), 1, file); + if (ferror(file)) err(EX_IOERR, "fread"); + if (feof(file)) errx(EX_DATAERR, "unexpected eof"); + return time; +} +static ssize_t readString(FILE *file, char **buf, size_t *cap) { + ssize_t len = getdelim(buf, cap, '\0', file); + if (len < 0 && !feof(file)) err(EX_IOERR, "getdelim"); + return len; +} + +void uiLoad(const char *name) { + FILE *file = dataOpen(name, "r"); + if (!file) { + if (errno != ENOENT) exit(EX_NOINPUT); + file = dataOpen(name, "w"); + if (!file) exit(EX_CANTCREAT); + fclose(file); + return; + } + + size_t signature = readSize(file); + signatureVersion(signature); + + char *buf = NULL; + size_t cap = 0; + while (0 < readString(file, &buf, &cap)) { + struct Window *window = windowFor(idFor(buf)); + for (;;) { + time_t time = readTime(file); + if (!time) break; + readString(file, &buf, &cap); + bufferPush(&window->buffer, time, buf); + } + reflow(window); + waddch(window->pad, '\n'); + } + + free(buf); + fclose(file); +} diff --git a/url.c b/url.c new file mode 100644 index 0000000..1ccc206 --- /dev/null +++ b/url.c @@ -0,0 +1,202 @@ +/* Copyright (C) 2020 C. McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <err.h> +#include <errno.h> +#include <regex.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <unistd.h> + +#include "chat.h" + +static const char *Pattern = { + "(" + "cvs|" + "ftp|" + "git|" + "gopher|" + "http|" + "https|" + "irc|" + "ircs|" + "magnet|" + "sftp|" + "ssh|" + "svn|" + "telnet|" + "vnc" + ")" + ":([^[:space:]>\"()]|[(][^)]*[)])+" +}; +static regex_t Regex; + +static void compile(void) { + static bool compiled; + if (compiled) return; + compiled = true; + int error = regcomp(&Regex, Pattern, REG_EXTENDED); + if (!error) return; + char buf[256]; + regerror(error, &Regex, buf, sizeof(buf)); + errx(EX_SOFTWARE, "regcomp: %s: %s", buf, Pattern); +} + +struct URL { + size_t id; + char *nick; + char *url; +}; + +enum { Cap = 32 }; +static struct { + struct URL urls[Cap]; + size_t len; +} ring; +static_assert(!(Cap & (Cap - 1)), "Cap is power of two"); + +static void push(size_t id, const char *nick, const char *str, size_t len) { + struct URL *url = &ring.urls[ring.len++ % Cap]; + free(url->nick); + free(url->url); + url->id = id; + url->nick = NULL; + if (nick) { + url->nick = strdup(nick); + if (!url->nick) err(EX_OSERR, "strdup"); + } + url->url = strndup(str, len); + if (!url->url) err(EX_OSERR, "strndup"); +} + +void urlScan(size_t id, const char *nick, const char *mesg) { + if (!mesg) return; + compile(); + regmatch_t match = {0}; + for (const char *ptr = mesg; *ptr; ptr += match.rm_eo) { + if (regexec(&Regex, ptr, 1, &match, 0)) break; + push(id, nick, &ptr[match.rm_so], match.rm_eo - match.rm_so); + } +} + +const char *urlOpenUtil; +static const char *OpenUtils[] = { "open", "xdg-open" }; + +static void urlOpen(const char *url) { + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + if (pid) return; + + close(STDIN_FILENO); + dup2(procPipe[1], STDOUT_FILENO); + dup2(procPipe[1], STDERR_FILENO); + if (urlOpenUtil) { + execlp(urlOpenUtil, urlOpenUtil, url, NULL); + warn("%s", urlOpenUtil); + _exit(EX_CONFIG); + } + for (size_t i = 0; i < ARRAY_LEN(OpenUtils); ++i) { + execlp(OpenUtils[i], OpenUtils[i], url, NULL); + if (errno != ENOENT) { + warn("%s", OpenUtils[i]); + _exit(EX_CONFIG); + } + } + warnx("no open utility found"); + _exit(EX_CONFIG); +} + +const char *urlCopyUtil; +static const char *CopyUtils[] = { "pbcopy", "wl-copy", "xclip", "xsel" }; + +static void urlCopy(const char *url) { + int rw[2]; + int error = pipe(rw); + if (error) err(EX_OSERR, "pipe"); + + ssize_t len = write(rw[1], url, strlen(url)); + if (len < 0) err(EX_IOERR, "write"); + + error = close(rw[1]); + if (error) err(EX_IOERR, "close"); + + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + if (pid) { + close(rw[0]); + return; + } + + dup2(rw[0], STDIN_FILENO); + dup2(procPipe[1], STDOUT_FILENO); + dup2(procPipe[1], STDERR_FILENO); + close(rw[0]); + if (urlCopyUtil) { + execlp(urlCopyUtil, urlCopyUtil, NULL); + warn("%s", urlCopyUtil); + _exit(EX_CONFIG); + } + for (size_t i = 0; i < ARRAY_LEN(CopyUtils); ++i) { + execlp(CopyUtils[i], CopyUtils[i], NULL); + if (errno != ENOENT) { + warn("%s", CopyUtils[i]); + _exit(EX_CONFIG); + } + } + warnx("no copy utility found"); + _exit(EX_CONFIG); +} + +void urlOpenCount(size_t id, size_t count) { + for (size_t i = 1; i <= Cap; ++i) { + const struct URL *url = &ring.urls[(ring.len - i) % Cap]; + if (!url->url) break; + if (url->id != id) continue; + urlOpen(url->url); + if (!--count) break; + } +} + +void urlOpenMatch(size_t id, const char *str) { + for (size_t i = 1; i <= Cap; ++i) { + const struct URL *url = &ring.urls[(ring.len - i) % Cap]; + if (!url->url) break; + if (url->id != id) continue; + if ((url->nick && !strcmp(url->nick, str)) || strstr(url->url, str)) { + urlOpen(url->url); + break; + } + } +} + +void urlCopyMatch(size_t id, const char *str) { + for (size_t i = 1; i <= Cap; ++i) { + const struct URL *url = &ring.urls[(ring.len - i) % Cap]; + if (!url->url) break; + if (url->id != id) continue; + if ( + !str + || (url->nick && !strcmp(url->nick, str)) + || strstr(url->url, str) + ) { + urlCopy(url->url); + break; + } + } +} diff --git a/xdg.c b/xdg.c new file mode 100644 index 0000000..6e33210 --- /dev/null +++ b/xdg.c @@ -0,0 +1,134 @@ +/* Copyright (C) 2019, 2020 C. McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <err.h> +#include <errno.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> + +#include "chat.h" + +FILE *configOpen(const char *path, const char *mode) { + if (path[0] == '/' || path[0] == '.') goto local; + + const char *home = getenv("HOME"); + const char *configHome = getenv("XDG_CONFIG_HOME"); + const char *configDirs = getenv("XDG_CONFIG_DIRS"); + + char buf[PATH_MAX]; + if (configHome) { + snprintf(buf, sizeof(buf), "%s/" XDG_SUBDIR "/%s", configHome, path); + } else { + if (!home) goto local; + snprintf(buf, sizeof(buf), "%s/.config/" XDG_SUBDIR "/%s", home, path); + } + FILE *file = fopen(buf, mode); + if (file) return file; + if (errno != ENOENT) { + warn("%s", buf); + return NULL; + } + + if (!configDirs) configDirs = "/etc/xdg"; + while (*configDirs) { + size_t len = strcspn(configDirs, ":"); + snprintf( + buf, sizeof(buf), "%.*s/" XDG_SUBDIR "/%s", + (int)len, configDirs, path + ); + file = fopen(buf, mode); + if (file) return file; + if (errno != ENOENT) { + warn("%s", buf); + return NULL; + } + configDirs += len; + if (*configDirs) configDirs++; + } + +local: + file = fopen(path, mode); + if (!file) warn("%s", path); + return file; +} + +FILE *dataOpen(const char *path, const char *mode) { + if (path[0] == '/' || path[0] == '.') goto local; + + const char *home = getenv("HOME"); + const char *dataHome = getenv("XDG_DATA_HOME"); + const char *dataDirs = getenv("XDG_DATA_DIRS"); + + char homePath[PATH_MAX]; + if (dataHome) { + snprintf( + homePath, sizeof(homePath), + "%s/" XDG_SUBDIR "/%s", dataHome, path + ); + } else { + if (!home) goto local; + snprintf( + homePath, sizeof(homePath), + "%s/.local/share/" XDG_SUBDIR "/%s", home, path + ); + } + FILE *file = fopen(homePath, mode); + if (file) return file; + if (errno != ENOENT) { + warn("%s", homePath); + return NULL; + } + + char buf[PATH_MAX]; + if (!dataDirs) dataDirs = "/usr/local/share:/usr/share"; + while (*dataDirs) { + size_t len = strcspn(dataDirs, ":"); + snprintf( + buf, sizeof(buf), "%.*s/" XDG_SUBDIR "/%s", + (int)len, dataDirs, path + ); + file = fopen(buf, mode); + if (file) return file; + if (errno != ENOENT) { + warn("%s", buf); + return NULL; + } + dataDirs += len; + if (*dataDirs) dataDirs++; + } + + if (mode[0] != 'r') { + char *base = strrchr(homePath, '/'); + *base = '\0'; + int error = mkdir(homePath, S_IRWXU); + if (error && errno != EEXIST) { + warn("%s", homePath); + return NULL; + } + *base = '/'; + file = fopen(homePath, mode); + if (!file) warn("%s", homePath); + return file; + } + +local: + file = fopen(path, mode); + if (!file) warn("%s", path); + return file; +} |