Add efinger: a Finger client and .plan feed reader
Efinger is a client for the Finger protocol (RFC 1288) with an Elfeed-inspired reader. Configure a list of accounts once and browse their .plan files from a two-pane interface: an account list on one side and the fingered content on the other. Move with n/p to preview each account, RET to read one in full, l to go back to the list and q to close both panes. M-x efinger-finger fingers a single account on demand. Connections are asynchronous (:nowait) with a configurable timeout, so Emacs never freezes when a host resolves but never answers on port 79. The finger core is forked from GNU Emacs' net-utils.el (by Peter Breton) and rewritten around an asynchronous connection.
This commit is contained in:
commit
324cef61d5
6 changed files with 1670 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.elc
|
||||||
|
.agent-shell/
|
||||||
674
LICENSE
Normal file
674
LICENSE
Normal file
|
|
@ -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>.
|
||||||
31
Makefile
Normal file
31
Makefile
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
EMACS ?= emacs
|
||||||
|
PACKAGE = efinger.el
|
||||||
|
TESTS = test/efinger-test.el
|
||||||
|
|
||||||
|
.PHONY: all check compile checkdoc package-lint test clean
|
||||||
|
|
||||||
|
all: check test
|
||||||
|
|
||||||
|
check: compile checkdoc package-lint
|
||||||
|
|
||||||
|
compile:
|
||||||
|
"$(EMACS)" --batch --eval "(setq byte-compile-error-on-warn t)" \
|
||||||
|
--eval "(byte-compile-file \"$(PACKAGE)\")"
|
||||||
|
|
||||||
|
checkdoc:
|
||||||
|
"$(EMACS)" --batch --eval "(checkdoc-file \"$(PACKAGE)\")"
|
||||||
|
|
||||||
|
package-lint:
|
||||||
|
"$(EMACS)" --batch \
|
||||||
|
--eval "(require 'package)" \
|
||||||
|
--eval "(add-to-list 'package-archives '(\"melpa\" . \"https://melpa.org/packages/\") t)" \
|
||||||
|
--eval "(package-initialize)" \
|
||||||
|
--eval "(unless (package-installed-p 'package-lint) (package-refresh-contents) (package-install 'package-lint))" \
|
||||||
|
--eval "(require 'package-lint)" \
|
||||||
|
-f package-lint-batch-and-exit $(PACKAGE)
|
||||||
|
|
||||||
|
test:
|
||||||
|
"$(EMACS)" --batch -L . -l $(TESTS) -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f *.elc
|
||||||
206
README.md
Normal file
206
README.md
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
# Efinger for Emacs
|
||||||
|
|
||||||
|
A client for the [Finger protocol](https://www.rfc-editor.org/rfc/rfc1288.html) (RFC 1288) with an [Elfeed](https://github.com/skeeto/elfeed)-inspired reader for Emacs.
|
||||||
|
|
||||||
|
Requires Emacs 28.1 or later.
|
||||||
|
|
||||||
|
Finger, born at Stanford in 1971, is arguably the first social network: your whole profile lives in a plain-text `.plan` file that anyone can read with `finger you@your-host`. John Carmack famously kept his DOOM/Quake development diary this way. Efinger lets you keep a list of accounts and browse their `.plan` files from a two-pane interface, the same way you follow feeds in Elfeed. For the story behind the protocol, see [Finger: la red social de 1971](https://andros.dev/blog/79c99190/finger-la-red-social-de-1971-que-nunca-llego-a-morir/).
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
```
|
||||||
|
Emacs Finger servers (TCP 79)
|
||||||
|
+---------------------+
|
||||||
|
| *efinger* | n / p +----------------------+
|
||||||
|
| ------------------ |------ move -->| happynetbox.com |
|
||||||
|
| >random@happynetbox | | thebackupbox.net |
|
||||||
|
| Carmack diary | RET / l | your own fingerd |
|
||||||
|
| ring@thebackupbox |<-- enter/back | |
|
||||||
|
+----------+----------+ +----------+-----------+
|
||||||
|
| |
|
||||||
|
v preview | async, :nowait
|
||||||
|
+---------------------+ |
|
||||||
|
| *efinger .plan* |<-------------------------+
|
||||||
|
| the fingered text |
|
||||||
|
+---------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
1. You keep a list of finger accounts in `efinger-accounts`.
|
||||||
|
2. `M-x efinger` opens the account list next to a content pane.
|
||||||
|
3. Moving with `n` / `p` fingers the account under point and previews its `.plan` in the other pane, asynchronously so Emacs never blocks.
|
||||||
|
4. `RET` jumps into the content, `l` goes back to the list, `q` closes both panes.
|
||||||
|
|
||||||
|
## Buffers
|
||||||
|
|
||||||
|
### Account list + content (`M-x efinger`)
|
||||||
|
|
||||||
|
```
|
||||||
|
Efinger — n/p move · RET open · g refresh · q quit
|
||||||
|
┌────────────────────────┬───────────────────────────────────────┐
|
||||||
|
│ random@happynetbox.com │ johnc@idsoftware.com │
|
||||||
|
│ Carmack diary │ ───────────────────────────────────── │
|
||||||
|
│ ring@thebackupbox.net │ │
|
||||||
|
│ me@happynetbox.com │ -------------------------------------- │
|
||||||
|
│ │ John Carmack's .plan for Aug 18, 1997 │
|
||||||
|
│ │ -------------------------------------- │
|
||||||
|
│ │ │
|
||||||
|
│ │ I get asked about the DOOM source code │
|
||||||
|
│ │ every once in a while, so here is a │
|
||||||
|
│ │ full status update: │
|
||||||
|
│ │ ... │
|
||||||
|
└────────────────────────┴───────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Single account (`M-x efinger-finger`)
|
||||||
|
|
||||||
|
```
|
||||||
|
.plan — l back · n/p next/prev · g refresh · q quit
|
||||||
|
random@happynetbox.com
|
||||||
|
──────────────────────
|
||||||
|
|
||||||
|
Emily Carter
|
||||||
|
Marine biologist. Coffee, kayaks and old maps.
|
||||||
|
|
||||||
|
--- 2026-08-14 ---
|
||||||
|
Back from sampling on the coast. Three jars of plankton to review.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Account list**: keep your finger accounts in `efinger-accounts` and browse them in a dedicated buffer, the way Elfeed lists feeds.
|
||||||
|
- **Live preview**: move with `n` / `p` and the `.plan` under point is fingered and shown in the content pane automatically.
|
||||||
|
- **Two-pane reader**: `RET` enters the content pane to read and scroll, `l` returns to the list, `q` closes both.
|
||||||
|
- **Asynchronous connections**: connections use `:nowait`, so Emacs never freezes even when a host resolves but never answers on port 79 (the common case today).
|
||||||
|
- **Connection timeout**: a stalled connection is abandoned after `efinger-connection-timeout` seconds with a clear message, instead of hanging.
|
||||||
|
- **Graceful errors**: refused or timed-out connections are reported in the content pane, never as a backtrace.
|
||||||
|
- **One-off finger**: `M-x efinger-finger` fingers a single `user@host` in its own buffer.
|
||||||
|
- **Configurable layout**: list on the left and content on the right (`horizontal`), or list on top and content below (`vertical`).
|
||||||
|
- **Ports and listing**: targets accept an optional `:port`; omitting the user (`host` or `@host`) asks the server to list every user.
|
||||||
|
- **UTF-8**: replies are decoded as UTF-8, so accents and box drawing render correctly.
|
||||||
|
|
||||||
|
## Keymap
|
||||||
|
|
||||||
|
### Account list (`efinger-list-mode`)
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
|-------|------------------------------------------|
|
||||||
|
| `n` | Move to next account and preview it |
|
||||||
|
| `p` | Move to previous account and preview it |
|
||||||
|
| `RET` | Enter the content pane for this account |
|
||||||
|
| `g` | Re-finger the account at point |
|
||||||
|
| `G` | Rebuild the list from `efinger-accounts`|
|
||||||
|
| `q` | Close both panes |
|
||||||
|
|
||||||
|
### Content pane (`efinger-plan-mode`)
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
|-------|------------------------------------------|
|
||||||
|
| `l` | Back to the account list |
|
||||||
|
| `n` | Preview next account (stay in content) |
|
||||||
|
| `p` | Preview previous account (stay in content)|
|
||||||
|
| `g` | Re-finger the current account |
|
||||||
|
| `q` | Close both panes |
|
||||||
|
| `SPC` / `DEL` | Scroll the content up / down |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### MELPA
|
||||||
|
|
||||||
|
```
|
||||||
|
M-x package-install RET efinger RET
|
||||||
|
```
|
||||||
|
|
||||||
|
### use-package with :vc (Emacs 29+)
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(use-package efinger
|
||||||
|
:vc (:url "https://git.andros.dev/andros/efinger.el"
|
||||||
|
:rev :newest)
|
||||||
|
:config
|
||||||
|
(setq efinger-accounts
|
||||||
|
'("random@happynetbox.com"
|
||||||
|
("Backup Box ring" "ring@thebackupbox.net"))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### use-package with :load-path
|
||||||
|
|
||||||
|
For manual installation or Emacs < 29:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(use-package efinger
|
||||||
|
:load-path "/path/to/efinger.el"
|
||||||
|
:config
|
||||||
|
(setq efinger-accounts
|
||||||
|
'("random@happynetbox.com"
|
||||||
|
("Backup Box ring" "ring@thebackupbox.net"))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
Clone the repository and place the file in a directory on your `load-path`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://git.andros.dev/andros/efinger.el.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add to your init file:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(add-to-list 'load-path "/path/to/efinger.el")
|
||||||
|
(require 'efinger)
|
||||||
|
(setq efinger-accounts
|
||||||
|
'("random@happynetbox.com"
|
||||||
|
("Backup Box ring" "ring@thebackupbox.net")))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Set `efinger-accounts` in your init file. Each entry is either a
|
||||||
|
`"[user@]host[:port]"` string or a `(LABEL "[user@]host[:port]")` list.
|
||||||
|
2. Run `M-x efinger` to open the reader.
|
||||||
|
3. Move with `n` / `p` to preview each account, `RET` to read one in full,
|
||||||
|
`l` to return to the list and `q` to quit.
|
||||||
|
4. Or run `M-x efinger-finger` to finger a single account on demand.
|
||||||
|
|
||||||
|
Public servers you can try today: `random@happynetbox.com`,
|
||||||
|
`benbrown@happynetbox.com` and `ring@thebackupbox.net`.
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
Run `M-x customize-group RET efinger RET` to list all available options.
|
||||||
|
|
||||||
|
Key options:
|
||||||
|
|
||||||
|
- `efinger-accounts`: the accounts shown in the reader.
|
||||||
|
- `efinger-port` (default `79`): default finger TCP port.
|
||||||
|
- `efinger-connection-timeout` (default `10`): seconds before a stalled
|
||||||
|
connection is abandoned; `nil` waits indefinitely.
|
||||||
|
- `efinger-forward-host` (default `nil`): send `user@host` instead of just
|
||||||
|
`user` (finger forwarding, for old daemons).
|
||||||
|
- `efinger-layout` (default `horizontal`): `horizontal` (list left, content
|
||||||
|
right) or `vertical` (list top, content bottom).
|
||||||
|
- `efinger-list-width` (default `40`): list width in columns (horizontal).
|
||||||
|
- `efinger-list-height` (default `12`): list height in lines (vertical).
|
||||||
|
|
||||||
|
## The Finger protocol
|
||||||
|
|
||||||
|
Finger is minimal: the client opens a TCP connection to port 79 of the server,
|
||||||
|
sends a single line (the user name, or an empty line to list everyone)
|
||||||
|
terminated by CRLF, and the server replies with plain text and closes the
|
||||||
|
connection. No encryption, no headers, no sessions. You can speak it by hand:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
echo "random" | nc happynetbox.com 79
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please see the [contribution guidelines](https://git.andros.dev/andros/contribute) for instructions on how to submit issues or pull requests.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
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/](https://www.gnu.org/licenses/).
|
||||||
536
efinger.el
Normal file
536
efinger.el
Normal file
|
|
@ -0,0 +1,536 @@
|
||||||
|
;;; efinger.el --- Finger client and .plan feed reader -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2026 Andros Fenollosa
|
||||||
|
|
||||||
|
;; Author: Andros Fenollosa <hi@andros.dev>
|
||||||
|
;; Maintainer: Andros Fenollosa <hi@andros.dev>
|
||||||
|
;; Version: 0.1.0
|
||||||
|
;; Package-Requires: ((emacs "28.1"))
|
||||||
|
;; Keywords: comm
|
||||||
|
;; URL: https://git.andros.dev/andros/efinger.el
|
||||||
|
|
||||||
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
;; This file is NOT part of GNU Emacs.
|
||||||
|
|
||||||
|
;; 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/>.
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; Efinger is a client for the Finger user information protocol
|
||||||
|
;; (RFC 1288) with an Elfeed-inspired reader. Configure a list of
|
||||||
|
;; accounts once and browse their `.plan' files from a two-pane
|
||||||
|
;; interface: a list of accounts on one side and the fingered content
|
||||||
|
;; on the other.
|
||||||
|
;;
|
||||||
|
;; Finger, born at Stanford in 1971, is arguably the first social
|
||||||
|
;; network: your whole profile lives in a plain-text `.plan' file that
|
||||||
|
;; anyone can read with `finger you@your-host'. John Carmack famously
|
||||||
|
;; kept his development diary this way.
|
||||||
|
;;
|
||||||
|
;; Usage:
|
||||||
|
;;
|
||||||
|
;; (setq efinger-accounts
|
||||||
|
;; '("random@happynetbox.com"
|
||||||
|
;; ("Backup Box ring" "ring@thebackupbox.net")))
|
||||||
|
;;
|
||||||
|
;; M-x efinger - open the two-pane reader
|
||||||
|
;; M-x efinger-finger - finger a single account
|
||||||
|
;;
|
||||||
|
;; In the account list, move with `n' and `p' to preview each `.plan'
|
||||||
|
;; in the other pane, press RET to jump into the content, `l' to go
|
||||||
|
;; back to the list and `q' to close both panes.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'subr-x)
|
||||||
|
|
||||||
|
;;;; Customization
|
||||||
|
|
||||||
|
(defgroup efinger nil
|
||||||
|
"Finger protocol client and .plan feed reader."
|
||||||
|
:group 'communication
|
||||||
|
:prefix "efinger-")
|
||||||
|
|
||||||
|
(defcustom efinger-accounts nil
|
||||||
|
"List of finger accounts shown in the `efinger' reader.
|
||||||
|
Each entry is either a string of the form \"[USER@]HOST[:PORT]\",
|
||||||
|
or a list (LABEL TARGET) where LABEL is the text shown in the
|
||||||
|
account list and TARGET is such a string.
|
||||||
|
|
||||||
|
If USER is omitted the server is asked to list every user. If
|
||||||
|
PORT is omitted `efinger-port' is used."
|
||||||
|
:type '(repeat (choice
|
||||||
|
(string :tag "user@host")
|
||||||
|
(list :tag "Labelled account"
|
||||||
|
(string :tag "Label")
|
||||||
|
(string :tag "user@host")))))
|
||||||
|
|
||||||
|
(defcustom efinger-port 79
|
||||||
|
"Default TCP port used to reach the finger service."
|
||||||
|
:type 'natnum)
|
||||||
|
|
||||||
|
(defcustom efinger-connection-timeout 10
|
||||||
|
"Seconds to wait for a finger connection before giving up.
|
||||||
|
If the connection is still being established after this many
|
||||||
|
seconds, it is aborted and the user is notified. A value of nil
|
||||||
|
means wait indefinitely."
|
||||||
|
:type '(choice (natnum :tag "Seconds")
|
||||||
|
(const :tag "No timeout" nil)))
|
||||||
|
|
||||||
|
(defcustom efinger-forward-host nil
|
||||||
|
"Non-nil means send \"USER@HOST\" as the finger query.
|
||||||
|
The default sends only USER, which is what modern finger hosts
|
||||||
|
such as happynetbox.com expect. Enable this for old daemons that
|
||||||
|
rely on finger forwarding."
|
||||||
|
:type 'boolean)
|
||||||
|
|
||||||
|
(defcustom efinger-layout 'horizontal
|
||||||
|
"How the account list and the .plan content are arranged.
|
||||||
|
`horizontal' places the list on the left and the content on the
|
||||||
|
right; `vertical' places the list on top and the content below."
|
||||||
|
:type '(choice (const :tag "List left, content right" horizontal)
|
||||||
|
(const :tag "List top, content bottom" vertical)))
|
||||||
|
|
||||||
|
(defcustom efinger-list-width 40
|
||||||
|
"Width in columns of the account list with a `horizontal' layout."
|
||||||
|
:type 'natnum)
|
||||||
|
|
||||||
|
(defcustom efinger-list-height 12
|
||||||
|
"Height in lines of the account list with a `vertical' layout."
|
||||||
|
:type 'natnum)
|
||||||
|
|
||||||
|
;;;; Faces
|
||||||
|
|
||||||
|
(defface efinger-account-face
|
||||||
|
'((t :inherit default))
|
||||||
|
"Face for account labels in the list.")
|
||||||
|
|
||||||
|
(defface efinger-header-face
|
||||||
|
'((t :inherit bold))
|
||||||
|
"Face for the header of a fingered .plan.")
|
||||||
|
|
||||||
|
(defface efinger-status-face
|
||||||
|
'((t :inherit shadow))
|
||||||
|
"Face for transient status messages in the content pane.")
|
||||||
|
|
||||||
|
(defface efinger-error-face
|
||||||
|
'((t :inherit error))
|
||||||
|
"Face for connection errors in the content pane.")
|
||||||
|
|
||||||
|
;;;; Internal state
|
||||||
|
|
||||||
|
(defconst efinger-list-buffer-name "*efinger*"
|
||||||
|
"Name of the buffer holding the account list.")
|
||||||
|
|
||||||
|
(defconst efinger-plan-buffer-name "*efinger .plan*"
|
||||||
|
"Name of the buffer holding the fingered .plan content.")
|
||||||
|
|
||||||
|
(defvar efinger--list-buffer nil
|
||||||
|
"The live account-list buffer, or nil.")
|
||||||
|
|
||||||
|
(defvar-local efinger--accounts nil
|
||||||
|
"Normalized accounts rendered in the current list buffer.")
|
||||||
|
|
||||||
|
(defvar-local efinger--owner nil
|
||||||
|
"Network process currently allowed to write to this buffer.")
|
||||||
|
|
||||||
|
(defvar-local efinger--received nil
|
||||||
|
"Non-nil once the current fetch has written its first output.")
|
||||||
|
|
||||||
|
(defvar-local efinger--body-start nil
|
||||||
|
"Marker where the fingered content begins in this buffer.")
|
||||||
|
|
||||||
|
;;;; Account parsing
|
||||||
|
|
||||||
|
(defun efinger--parse-target (target label)
|
||||||
|
"Parse TARGET, a \"[user@]host[:port]\" string, into an account plist.
|
||||||
|
LABEL is stored as the account's display label."
|
||||||
|
(let* ((at (string-search "@" target))
|
||||||
|
(user (if at (substring target 0 at) ""))
|
||||||
|
(hostport (if at (substring target (1+ at)) target))
|
||||||
|
(colon (string-search ":" hostport))
|
||||||
|
(host (if colon (substring hostport 0 colon) hostport))
|
||||||
|
(port (if colon
|
||||||
|
(string-to-number (substring hostport (1+ colon)))
|
||||||
|
efinger-port)))
|
||||||
|
(list :label label :user user :host host :port port)))
|
||||||
|
|
||||||
|
(defun efinger--parse-account (entry)
|
||||||
|
"Normalize ENTRY from `efinger-accounts' into an account plist."
|
||||||
|
(pcase entry
|
||||||
|
((pred stringp) (efinger--parse-target entry entry))
|
||||||
|
(`(,label ,target) (efinger--parse-target target label))
|
||||||
|
(_ (error "Invalid efinger account: %S" entry))))
|
||||||
|
|
||||||
|
(defun efinger--account-title (account)
|
||||||
|
"Return a human-readable \"user@host\" title for ACCOUNT."
|
||||||
|
(let ((user (plist-get account :user))
|
||||||
|
(host (plist-get account :host))
|
||||||
|
(port (plist-get account :port)))
|
||||||
|
(concat (if (string= user "") "" (concat user "@"))
|
||||||
|
host
|
||||||
|
(if (eql port efinger-port) "" (format ":%d" port)))))
|
||||||
|
|
||||||
|
(defun efinger--finger-query (account)
|
||||||
|
"Return the query line to send to the finger server for ACCOUNT."
|
||||||
|
(let ((user (plist-get account :user)))
|
||||||
|
(if (and efinger-forward-host (not (string= user "")))
|
||||||
|
(concat user "@" (plist-get account :host))
|
||||||
|
user)))
|
||||||
|
|
||||||
|
;;;; Networking
|
||||||
|
|
||||||
|
(defun efinger--strip-cr (string)
|
||||||
|
"Return STRING with every carriage return removed."
|
||||||
|
(replace-regexp-in-string "\r" "" string))
|
||||||
|
|
||||||
|
(defun efinger--cancel-timer (process)
|
||||||
|
"Cancel the connection-timeout timer stored on PROCESS, if any."
|
||||||
|
(when-let* ((timer (process-get process 'efinger-timer)))
|
||||||
|
(cancel-timer timer)
|
||||||
|
(process-put process 'efinger-timer nil)))
|
||||||
|
|
||||||
|
(defun efinger--owned-p (process)
|
||||||
|
"Return non-nil if PROCESS still owns its target buffer."
|
||||||
|
(let ((buffer (process-get process 'efinger-buffer)))
|
||||||
|
(and (buffer-live-p buffer)
|
||||||
|
(eq process (buffer-local-value 'efinger--owner buffer)))))
|
||||||
|
|
||||||
|
(defun efinger--report (process message)
|
||||||
|
"Display MESSAGE from PROCESS in its content buffer and the echo area."
|
||||||
|
(let ((buffer (process-get process 'efinger-buffer)))
|
||||||
|
(when (efinger--owned-p process)
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(let ((inhibit-read-only t))
|
||||||
|
(unless efinger--received
|
||||||
|
(setq efinger--received t)
|
||||||
|
(delete-region efinger--body-start (point-max)))
|
||||||
|
(goto-char (point-max))
|
||||||
|
(insert (propertize message 'face 'efinger-error-face) "\n")))))
|
||||||
|
(message "efinger: %s" message))
|
||||||
|
|
||||||
|
(defun efinger--filter (process string)
|
||||||
|
"Insert STRING from PROCESS into its content buffer, stripping CRs."
|
||||||
|
(when (efinger--owned-p process)
|
||||||
|
(with-current-buffer (process-get process 'efinger-buffer)
|
||||||
|
(let ((inhibit-read-only t))
|
||||||
|
(unless efinger--received
|
||||||
|
(setq efinger--received t)
|
||||||
|
(delete-region efinger--body-start (point-max)))
|
||||||
|
(save-excursion
|
||||||
|
(goto-char (point-max))
|
||||||
|
(insert (efinger--strip-cr string)))))))
|
||||||
|
|
||||||
|
(defun efinger--sentinel (process _event)
|
||||||
|
"Drive PROCESS through its connection state changes."
|
||||||
|
(pcase (process-status process)
|
||||||
|
('open
|
||||||
|
(efinger--cancel-timer process)
|
||||||
|
(process-send-string
|
||||||
|
process (concat (process-get process 'efinger-query) "\r\n")))
|
||||||
|
('failed
|
||||||
|
(efinger--cancel-timer process)
|
||||||
|
(efinger--report
|
||||||
|
process (format "Could not connect to %s"
|
||||||
|
(process-get process 'efinger-host))))
|
||||||
|
('closed
|
||||||
|
(efinger--cancel-timer process)
|
||||||
|
(when (and (efinger--owned-p process)
|
||||||
|
(not (buffer-local-value
|
||||||
|
'efinger--received
|
||||||
|
(process-get process 'efinger-buffer))))
|
||||||
|
(efinger--report process "(no reply)")))))
|
||||||
|
|
||||||
|
(defun efinger--timeout (process)
|
||||||
|
"Abort PROCESS if it is still trying to connect."
|
||||||
|
(when (and (process-live-p process)
|
||||||
|
(eq (process-status process) 'connect))
|
||||||
|
;; Silence the sentinel so the forced deletion is not reported twice.
|
||||||
|
(set-process-sentinel process #'ignore)
|
||||||
|
(delete-process process)
|
||||||
|
(efinger--report
|
||||||
|
process (format "Connection to %s timed out after %d seconds"
|
||||||
|
(process-get process 'efinger-host)
|
||||||
|
efinger-connection-timeout))))
|
||||||
|
|
||||||
|
(defun efinger--fetch (host port query buffer)
|
||||||
|
"Finger QUERY at HOST on PORT, streaming the reply into BUFFER.
|
||||||
|
Return the network process. The connection is asynchronous, so
|
||||||
|
Emacs never blocks while the handshake is retried."
|
||||||
|
(let ((process (make-network-process
|
||||||
|
:name (format "efinger %s@%s" query host)
|
||||||
|
:host host
|
||||||
|
:service port
|
||||||
|
:nowait t
|
||||||
|
:coding 'utf-8
|
||||||
|
:filter #'efinger--filter
|
||||||
|
:sentinel #'efinger--sentinel)))
|
||||||
|
(process-put process 'efinger-buffer buffer)
|
||||||
|
(process-put process 'efinger-query query)
|
||||||
|
(process-put process 'efinger-host host)
|
||||||
|
(when efinger-connection-timeout
|
||||||
|
(process-put process 'efinger-timer
|
||||||
|
(run-at-time efinger-connection-timeout nil
|
||||||
|
#'efinger--timeout process)))
|
||||||
|
process))
|
||||||
|
|
||||||
|
(defun efinger--start (buffer account)
|
||||||
|
"Finger ACCOUNT and render the reply into BUFFER."
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(when (process-live-p efinger--owner)
|
||||||
|
(efinger--cancel-timer efinger--owner)
|
||||||
|
(set-process-sentinel efinger--owner #'ignore)
|
||||||
|
(delete-process efinger--owner))
|
||||||
|
(let ((inhibit-read-only t)
|
||||||
|
(title (efinger--account-title account)))
|
||||||
|
(erase-buffer)
|
||||||
|
(insert (propertize title 'face 'efinger-header-face) "\n")
|
||||||
|
(insert (make-string (max 12 (length title)) ?─) "\n\n")
|
||||||
|
(setq efinger--received nil)
|
||||||
|
(setq efinger--body-start (copy-marker (point)))
|
||||||
|
(insert (propertize "Fingering…" 'face 'efinger-status-face))
|
||||||
|
(goto-char (point-min)))
|
||||||
|
(setq efinger--owner
|
||||||
|
(efinger--fetch (plist-get account :host)
|
||||||
|
(plist-get account :port)
|
||||||
|
(efinger--finger-query account)
|
||||||
|
buffer))))
|
||||||
|
|
||||||
|
;;;; Content buffer (.plan)
|
||||||
|
|
||||||
|
(defvar efinger-plan-mode-map
|
||||||
|
(let ((map (make-sparse-keymap)))
|
||||||
|
(define-key map "l" #'efinger-back-to-list)
|
||||||
|
(define-key map "n" #'efinger-plan-next)
|
||||||
|
(define-key map "p" #'efinger-plan-previous)
|
||||||
|
(define-key map "g" #'efinger-plan-refresh)
|
||||||
|
(define-key map "q" #'efinger-quit)
|
||||||
|
map)
|
||||||
|
"Keymap for `efinger-plan-mode'.")
|
||||||
|
|
||||||
|
(define-derived-mode efinger-plan-mode special-mode "Efinger-Plan"
|
||||||
|
"Major mode for reading a fingered .plan."
|
||||||
|
(setq-local truncate-lines nil)
|
||||||
|
(setq-local header-line-format
|
||||||
|
" .plan — l back · n/p next/prev · g refresh · q quit")
|
||||||
|
(buffer-disable-undo))
|
||||||
|
|
||||||
|
(defun efinger--plan-buffer ()
|
||||||
|
"Return the .plan content buffer, creating it if needed."
|
||||||
|
(let ((buffer (get-buffer-create efinger-plan-buffer-name)))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(unless (derived-mode-p 'efinger-plan-mode)
|
||||||
|
(efinger-plan-mode)))
|
||||||
|
buffer))
|
||||||
|
|
||||||
|
;;;; Account list
|
||||||
|
|
||||||
|
(defvar efinger-list-mode-map
|
||||||
|
(let ((map (make-sparse-keymap)))
|
||||||
|
(define-key map "n" #'efinger-next-account)
|
||||||
|
(define-key map "p" #'efinger-previous-account)
|
||||||
|
(define-key map (kbd "RET") #'efinger-select)
|
||||||
|
(define-key map "g" #'efinger-refresh)
|
||||||
|
(define-key map "G" #'efinger-reload)
|
||||||
|
(define-key map "q" #'efinger-quit)
|
||||||
|
map)
|
||||||
|
"Keymap for `efinger-list-mode'.")
|
||||||
|
|
||||||
|
(define-derived-mode efinger-list-mode special-mode "Efinger"
|
||||||
|
"Major mode for the Efinger account list."
|
||||||
|
(setq-local truncate-lines t)
|
||||||
|
(setq-local header-line-format
|
||||||
|
" Efinger — n/p move · RET open · g refresh · q quit")
|
||||||
|
(buffer-disable-undo)
|
||||||
|
(hl-line-mode))
|
||||||
|
|
||||||
|
(defun efinger--render-list ()
|
||||||
|
"Render `efinger--accounts' into the current list buffer."
|
||||||
|
(let ((inhibit-read-only t))
|
||||||
|
(erase-buffer)
|
||||||
|
(if (null efinger--accounts)
|
||||||
|
(insert (propertize
|
||||||
|
"No accounts configured. Customize `efinger-accounts'."
|
||||||
|
'face 'efinger-status-face))
|
||||||
|
(dolist (account efinger--accounts)
|
||||||
|
(insert (propertize (plist-get account :label)
|
||||||
|
'efinger-account account
|
||||||
|
'face 'efinger-account-face)
|
||||||
|
"\n")))
|
||||||
|
(goto-char (point-min))))
|
||||||
|
|
||||||
|
(defun efinger--account-at-point ()
|
||||||
|
"Return the account on the current list line, or nil."
|
||||||
|
(get-text-property (line-beginning-position) 'efinger-account))
|
||||||
|
|
||||||
|
(defun efinger--ensure-plan-window ()
|
||||||
|
"Ensure the .plan buffer is shown next to the account list."
|
||||||
|
(let* ((plan-buffer (efinger--plan-buffer))
|
||||||
|
(list-window (and (buffer-live-p efinger--list-buffer)
|
||||||
|
(get-buffer-window efinger--list-buffer))))
|
||||||
|
(or (get-buffer-window plan-buffer)
|
||||||
|
(if list-window
|
||||||
|
(with-selected-window list-window
|
||||||
|
(let ((window (if (eq efinger-layout 'vertical)
|
||||||
|
(split-window-below efinger-list-height)
|
||||||
|
(split-window-right efinger-list-width))))
|
||||||
|
(set-window-buffer window plan-buffer)
|
||||||
|
window))
|
||||||
|
(display-buffer plan-buffer)))))
|
||||||
|
|
||||||
|
(defun efinger--show-current ()
|
||||||
|
"Finger the account on the current list line into the .plan pane."
|
||||||
|
(let ((account (efinger--account-at-point)))
|
||||||
|
(when account
|
||||||
|
(efinger--ensure-plan-window)
|
||||||
|
(efinger--start (efinger--plan-buffer) account))))
|
||||||
|
|
||||||
|
;;;; Commands
|
||||||
|
|
||||||
|
(defun efinger-next-account ()
|
||||||
|
"Move to the next account and preview its .plan."
|
||||||
|
(interactive)
|
||||||
|
(forward-line 1)
|
||||||
|
(if (efinger--account-at-point)
|
||||||
|
(efinger--show-current)
|
||||||
|
(forward-line -1)))
|
||||||
|
|
||||||
|
(defun efinger-previous-account ()
|
||||||
|
"Move to the previous account and preview its .plan."
|
||||||
|
(interactive)
|
||||||
|
(when (> (line-number-at-pos) 1)
|
||||||
|
(forward-line -1)
|
||||||
|
(when (efinger--account-at-point)
|
||||||
|
(efinger--show-current))))
|
||||||
|
|
||||||
|
(defun efinger-select ()
|
||||||
|
"Finger the account at point and move into the .plan pane."
|
||||||
|
(interactive)
|
||||||
|
(when (efinger--account-at-point)
|
||||||
|
(efinger--show-current)
|
||||||
|
(let ((window (get-buffer-window (efinger--plan-buffer))))
|
||||||
|
(when window (select-window window)))))
|
||||||
|
|
||||||
|
(defun efinger-refresh ()
|
||||||
|
"Re-finger the account at point."
|
||||||
|
(interactive)
|
||||||
|
(efinger--show-current))
|
||||||
|
|
||||||
|
(defun efinger-reload ()
|
||||||
|
"Rebuild the account list from `efinger-accounts'."
|
||||||
|
(interactive)
|
||||||
|
(setq efinger--accounts (mapcar #'efinger--parse-account efinger-accounts))
|
||||||
|
(efinger--render-list)
|
||||||
|
(efinger--show-current))
|
||||||
|
|
||||||
|
(defun efinger-back-to-list ()
|
||||||
|
"Return focus to the account list."
|
||||||
|
(interactive)
|
||||||
|
(if (buffer-live-p efinger--list-buffer)
|
||||||
|
(let ((window (get-buffer-window efinger--list-buffer)))
|
||||||
|
(if window
|
||||||
|
(select-window window)
|
||||||
|
(switch-to-buffer efinger--list-buffer)))
|
||||||
|
(message "efinger: no account list")))
|
||||||
|
|
||||||
|
(defun efinger--plan-move (n)
|
||||||
|
"Move N accounts in the list window while staying in the .plan pane."
|
||||||
|
(let ((window (and (buffer-live-p efinger--list-buffer)
|
||||||
|
(get-buffer-window efinger--list-buffer))))
|
||||||
|
(if window
|
||||||
|
(with-selected-window window
|
||||||
|
(if (> n 0)
|
||||||
|
(efinger-next-account)
|
||||||
|
(efinger-previous-account)))
|
||||||
|
(message "efinger: no account list"))))
|
||||||
|
|
||||||
|
(defun efinger-plan-next ()
|
||||||
|
"Preview the next account without leaving the .plan pane."
|
||||||
|
(interactive)
|
||||||
|
(efinger--plan-move 1))
|
||||||
|
|
||||||
|
(defun efinger-plan-previous ()
|
||||||
|
"Preview the previous account without leaving the .plan pane."
|
||||||
|
(interactive)
|
||||||
|
(efinger--plan-move -1))
|
||||||
|
|
||||||
|
(defun efinger-plan-refresh ()
|
||||||
|
"Re-finger the account currently shown in the .plan pane."
|
||||||
|
(interactive)
|
||||||
|
(when (buffer-live-p efinger--list-buffer)
|
||||||
|
(with-current-buffer efinger--list-buffer
|
||||||
|
(efinger--show-current))))
|
||||||
|
|
||||||
|
(defun efinger-quit ()
|
||||||
|
"Close the account list and the .plan pane."
|
||||||
|
(interactive)
|
||||||
|
(let ((plan-buffer (get-buffer efinger-plan-buffer-name))
|
||||||
|
(list-buffer (get-buffer efinger-list-buffer-name)))
|
||||||
|
(when plan-buffer
|
||||||
|
(with-current-buffer plan-buffer
|
||||||
|
(when (process-live-p efinger--owner)
|
||||||
|
(efinger--cancel-timer efinger--owner)
|
||||||
|
(set-process-sentinel efinger--owner #'ignore)
|
||||||
|
(delete-process efinger--owner)))
|
||||||
|
(let ((window (get-buffer-window plan-buffer)))
|
||||||
|
(when (and window (not (one-window-p)))
|
||||||
|
(delete-window window)))
|
||||||
|
(kill-buffer plan-buffer))
|
||||||
|
(when list-buffer
|
||||||
|
(let ((window (get-buffer-window list-buffer)))
|
||||||
|
(when (and window (not (one-window-p)))
|
||||||
|
(delete-window window)))
|
||||||
|
(kill-buffer list-buffer)))
|
||||||
|
(setq efinger--list-buffer nil))
|
||||||
|
|
||||||
|
;;;; Entry points
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun efinger ()
|
||||||
|
"Open the Efinger reader over `efinger-accounts'."
|
||||||
|
(interactive)
|
||||||
|
(let ((list-buffer (get-buffer-create efinger-list-buffer-name)))
|
||||||
|
(setq efinger--list-buffer list-buffer)
|
||||||
|
(with-current-buffer list-buffer
|
||||||
|
(efinger-list-mode)
|
||||||
|
(setq efinger--accounts
|
||||||
|
(mapcar #'efinger--parse-account efinger-accounts))
|
||||||
|
(efinger--render-list))
|
||||||
|
(switch-to-buffer list-buffer)
|
||||||
|
(delete-other-windows)
|
||||||
|
(goto-char (point-min))
|
||||||
|
(efinger--show-current)))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun efinger-finger (target)
|
||||||
|
"Finger TARGET, a \"[user@]host[:port]\" string, in its own buffer."
|
||||||
|
(interactive
|
||||||
|
(list (read-string (format-prompt "Finger" nil))))
|
||||||
|
(when (string-empty-p (string-trim target))
|
||||||
|
(user-error "No account given"))
|
||||||
|
(let* ((account (efinger--parse-target (string-trim target)
|
||||||
|
(string-trim target)))
|
||||||
|
(buffer (get-buffer-create
|
||||||
|
(format "*finger %s*" (efinger--account-title account)))))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(unless (derived-mode-p 'efinger-plan-mode)
|
||||||
|
(efinger-plan-mode)))
|
||||||
|
(efinger--start buffer account)
|
||||||
|
(pop-to-buffer buffer)))
|
||||||
|
|
||||||
|
(provide 'efinger)
|
||||||
|
|
||||||
|
;;; efinger.el ends here
|
||||||
221
test/efinger-test.el
Normal file
221
test/efinger-test.el
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
;;; efinger-test.el --- Tests for efinger -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2026 Andros Fenollosa
|
||||||
|
|
||||||
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; ERT test suite for efinger.el.
|
||||||
|
;;
|
||||||
|
;; The integration tests spin up an ephemeral finger server *inside*
|
||||||
|
;; Emacs (via `make-network-process' with :server t) so no external
|
||||||
|
;; program or network access is required. The sample .plan payload is
|
||||||
|
;; John Carmack's real DOOM source-release announcement, quoted in
|
||||||
|
;; <https://andros.dev/blog/79c99190/>.
|
||||||
|
;;
|
||||||
|
;; Run with:
|
||||||
|
;; emacs --batch -L . -l test/efinger-test.el -f ert-run-tests-batch-and-exit
|
||||||
|
;;
|
||||||
|
;; The blackhole timeout test needs outbound routing and is skipped
|
||||||
|
;; unless EFINGER_NETWORK_TESTS is set in the environment.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'efinger)
|
||||||
|
|
||||||
|
(defconst efinger-test-plan
|
||||||
|
(concat "-----------------------------------------\r\n"
|
||||||
|
"John Carmack's .plan for Aug 18, 1997\r\n"
|
||||||
|
"-----------------------------------------\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"I get asked about the DOOM source code every once in a while,\r\n"
|
||||||
|
"so here is a full status update.\r\n")
|
||||||
|
"Sample .plan reply, in raw CRLF form, used by the integration tests.")
|
||||||
|
|
||||||
|
;;;; Helpers
|
||||||
|
|
||||||
|
(defun efinger-test--pump (seconds predicate)
|
||||||
|
"Run the event loop up to SECONDS until PREDICATE returns non-nil."
|
||||||
|
(let ((deadline (+ (float-time) seconds)))
|
||||||
|
(while (and (< (float-time) deadline)
|
||||||
|
(not (funcall predicate)))
|
||||||
|
(accept-process-output nil 0.05)
|
||||||
|
(sit-for 0.01))
|
||||||
|
(funcall predicate)))
|
||||||
|
|
||||||
|
(defun efinger-test--make-server (payload received)
|
||||||
|
"Start a one-shot finger server that replies with PAYLOAD.
|
||||||
|
RECEIVED is a one-element vector; the query line the client sends
|
||||||
|
is stored in its slot 0. Return the server process."
|
||||||
|
(make-network-process
|
||||||
|
:name "efinger-test-server"
|
||||||
|
:server t
|
||||||
|
:host 'local
|
||||||
|
:service t
|
||||||
|
:family 'ipv4
|
||||||
|
:coding 'utf-8
|
||||||
|
:filter (lambda (connection string)
|
||||||
|
(aset received 0 string)
|
||||||
|
(process-send-string connection payload)
|
||||||
|
(delete-process connection))))
|
||||||
|
|
||||||
|
(defun efinger-test--closed-port ()
|
||||||
|
"Return a loopback port number with nothing listening on it."
|
||||||
|
(let* ((server (make-network-process
|
||||||
|
:name "efinger-test-probe" :server t
|
||||||
|
:host 'local :service t :family 'ipv4))
|
||||||
|
(port (process-contact server :service)))
|
||||||
|
(delete-process server)
|
||||||
|
port))
|
||||||
|
|
||||||
|
;;;; Account parsing
|
||||||
|
|
||||||
|
(ert-deftest efinger-test-parse-user-host ()
|
||||||
|
(let ((account (efinger--parse-account "johnc@idsoftware.com")))
|
||||||
|
(should (equal (plist-get account :user) "johnc"))
|
||||||
|
(should (equal (plist-get account :host) "idsoftware.com"))
|
||||||
|
(should (eql (plist-get account :port) 79))
|
||||||
|
(should (equal (plist-get account :label) "johnc@idsoftware.com"))))
|
||||||
|
|
||||||
|
(ert-deftest efinger-test-parse-labelled-with-port ()
|
||||||
|
(let ((account (efinger--parse-account
|
||||||
|
'("Carmack" "johnc@example.com:1079"))))
|
||||||
|
(should (equal (plist-get account :label) "Carmack"))
|
||||||
|
(should (equal (plist-get account :user) "johnc"))
|
||||||
|
(should (equal (plist-get account :host) "example.com"))
|
||||||
|
(should (eql (plist-get account :port) 1079))))
|
||||||
|
|
||||||
|
(ert-deftest efinger-test-parse-host-only ()
|
||||||
|
(let ((account (efinger--parse-account "happynetbox.com")))
|
||||||
|
(should (equal (plist-get account :user) ""))
|
||||||
|
(should (equal (plist-get account :host) "happynetbox.com"))))
|
||||||
|
|
||||||
|
(ert-deftest efinger-test-parse-invalid ()
|
||||||
|
(should-error (efinger--parse-account 42)))
|
||||||
|
|
||||||
|
(ert-deftest efinger-test-account-title ()
|
||||||
|
(should (equal (efinger--account-title
|
||||||
|
(efinger--parse-account "johnc@idsoftware.com"))
|
||||||
|
"johnc@idsoftware.com"))
|
||||||
|
(should (equal (efinger--account-title
|
||||||
|
(efinger--parse-account "johnc@example.com:1079"))
|
||||||
|
"johnc@example.com:1079"))
|
||||||
|
(should (equal (efinger--account-title
|
||||||
|
(efinger--parse-account "happynetbox.com"))
|
||||||
|
"happynetbox.com")))
|
||||||
|
|
||||||
|
(ert-deftest efinger-test-finger-query ()
|
||||||
|
(let ((account (efinger--parse-account "johnc@idsoftware.com")))
|
||||||
|
(let ((efinger-forward-host nil))
|
||||||
|
(should (equal (efinger--finger-query account) "johnc")))
|
||||||
|
(let ((efinger-forward-host t))
|
||||||
|
(should (equal (efinger--finger-query account)
|
||||||
|
"johnc@idsoftware.com"))))
|
||||||
|
;; Host-only listing sends an empty query regardless of forwarding.
|
||||||
|
(let ((account (efinger--parse-account "happynetbox.com"))
|
||||||
|
(efinger-forward-host t))
|
||||||
|
(should (equal (efinger--finger-query account) ""))))
|
||||||
|
|
||||||
|
(ert-deftest efinger-test-strip-cr ()
|
||||||
|
(should (equal (efinger--strip-cr "a\r\nb\r\n") "a\nb\n")))
|
||||||
|
|
||||||
|
;;;; Integration: async fetch
|
||||||
|
|
||||||
|
(ert-deftest efinger-test-fetch-serves-plan ()
|
||||||
|
"A responding server delivers its .plan into the buffer, CRs stripped."
|
||||||
|
(let* ((received (make-vector 1 nil))
|
||||||
|
(server (efinger-test--make-server efinger-test-plan received))
|
||||||
|
(port (process-contact server :service))
|
||||||
|
(buffer (generate-new-buffer " *efinger-test-plan*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(with-current-buffer buffer (efinger-plan-mode))
|
||||||
|
(efinger--start
|
||||||
|
buffer (list :label "test" :user "johnc"
|
||||||
|
:host "127.0.0.1" :port port))
|
||||||
|
(should (efinger-test--pump
|
||||||
|
5 (lambda ()
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(string-match-p "status update" (buffer-string))))))
|
||||||
|
;; The bare username reached the server.
|
||||||
|
(should (equal (string-trim (aref received 0)) "johnc"))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(should (string-match-p "John Carmack's .plan" (buffer-string)))
|
||||||
|
;; Header title is present and CRs are gone.
|
||||||
|
(should (string-prefix-p "johnc@127.0.0.1" (buffer-string)))
|
||||||
|
(should-not (string-match-p "\r" (buffer-string)))
|
||||||
|
;; The transient status placeholder was replaced.
|
||||||
|
(should-not (string-match-p "Fingering" (buffer-string)))))
|
||||||
|
(when (process-live-p server) (delete-process server))
|
||||||
|
(kill-buffer buffer))))
|
||||||
|
|
||||||
|
(ert-deftest efinger-test-fetch-connection-refused ()
|
||||||
|
"A refused connection is reported gracefully, without a backtrace."
|
||||||
|
(let ((buffer (generate-new-buffer " *efinger-test-refused*"))
|
||||||
|
(port (efinger-test--closed-port))
|
||||||
|
(efinger-connection-timeout 3))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(with-current-buffer buffer (efinger-plan-mode))
|
||||||
|
(efinger--start
|
||||||
|
buffer (list :label "x" :user "x" :host "127.0.0.1" :port port))
|
||||||
|
(should (efinger-test--pump
|
||||||
|
5 (lambda ()
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(string-match-p "Could not connect\\|timed out"
|
||||||
|
(buffer-string))))))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(should (string-match-p "Could not connect" (buffer-string)))))
|
||||||
|
(kill-buffer buffer))))
|
||||||
|
|
||||||
|
(ert-deftest efinger-test-fetch-returns-immediately ()
|
||||||
|
"The async fetch returns control at once, whatever the host does."
|
||||||
|
(let ((buffer (generate-new-buffer " *efinger-test-immediate*"))
|
||||||
|
(port (efinger-test--closed-port))
|
||||||
|
(efinger-connection-timeout 10))
|
||||||
|
(unwind-protect
|
||||||
|
(let* ((start (float-time))
|
||||||
|
(_ (with-current-buffer buffer (efinger-plan-mode)))
|
||||||
|
(proc (efinger--start
|
||||||
|
buffer
|
||||||
|
(list :label "x" :user "x" :host "127.0.0.1" :port port)))
|
||||||
|
(elapsed (- (float-time) start)))
|
||||||
|
(should (< elapsed 1.0))
|
||||||
|
(should (eq (process-status proc) 'connect)))
|
||||||
|
(kill-buffer buffer))))
|
||||||
|
|
||||||
|
(ert-deftest efinger-test-timeout ()
|
||||||
|
"A silent host is abandoned after `efinger-connection-timeout' seconds."
|
||||||
|
(skip-unless (getenv "EFINGER_NETWORK_TESTS"))
|
||||||
|
(let ((buffer (generate-new-buffer " *efinger-test-timeout*"))
|
||||||
|
(efinger-connection-timeout 2))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(with-current-buffer buffer (efinger-plan-mode))
|
||||||
|
(efinger--start
|
||||||
|
buffer (list :label "bh" :user "x" :host "10.255.255.1" :port 79))
|
||||||
|
(should (efinger-test--pump
|
||||||
|
6 (lambda ()
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(string-match-p "timed out" (buffer-string)))))))
|
||||||
|
(kill-buffer buffer))))
|
||||||
|
|
||||||
|
(ert-deftest efinger-test-live-happynetbox ()
|
||||||
|
"Optional live check against a real public finger server."
|
||||||
|
(skip-unless (getenv "EFINGER_NETWORK_TESTS"))
|
||||||
|
(let ((buffer (generate-new-buffer " *efinger-test-live*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(with-current-buffer buffer (efinger-plan-mode))
|
||||||
|
(efinger--start
|
||||||
|
buffer (efinger--parse-account "random@happynetbox.com"))
|
||||||
|
(should (efinger-test--pump
|
||||||
|
15 (lambda ()
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(> (- (point-max) (point-min)) 60))))))
|
||||||
|
(kill-buffer buffer))))
|
||||||
|
|
||||||
|
(provide 'efinger-test)
|
||||||
|
;;; efinger-test.el ends here
|
||||||
Loading…
Add table
Reference in a new issue