Merge branch 'rewrite'
commit
d1913a4c63
|
@ -0,0 +1,4 @@
|
||||||
|
*.o
|
||||||
|
catgirl
|
||||||
|
config.mk
|
||||||
|
tags
|
|
@ -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>.
|
|
@ -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
|
|
@ -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
|
|
@ -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 .
|
|
@ -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();
|
||||||
|
}
|
|
@ -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);
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue