Full Code of alphapapa/ement.el for AI

master 790aee7d8fdd cached
22 files
733.4 KB
166.4k tokens
1 requests
Download .txt
Showing preview only (755K chars total). Download the full file or copy to clipboard to get everything.
Repository: alphapapa/ement.el
Branch: master
Commit: 790aee7d8fdd
Files: 22
Total size: 733.4 KB

Directory structure:
gitextract_pmij6jm2/

├── .dir-locals.el
├── .elpaignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── config.yml
│   └── workflows/
│       └── test.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.org
├── ement-api.el
├── ement-directory.el
├── ement-lib.el
├── ement-macros.el
├── ement-notifications.el
├── ement-notify.el
├── ement-room-list.el
├── ement-room.el
├── ement-structs.el
├── ement-tabulated-room-list.el
├── ement.el
├── makem.sh
└── tests/
    └── ement-tests.el

================================================
FILE CONTENTS
================================================

================================================
FILE: .dir-locals.el
================================================
;;; Directory Local Variables
;;; For more information see (info "(emacs) Directory Variables")

((emacs-lisp-mode . ((fill-column . 90)
		     (indent-tabs-mode . nil))))


================================================
FILE: .elpaignore
================================================
.github/
images/
LICENSE
Makefile
makem.sh
NOTES.org
screenshots/


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: File a bug report
labels: ["bug"]
assignees:
  - alphapapa
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to fill out this bug report!
  - type: input
    id: os-platform
    attributes:
      label: OS/platform
      description: What operating system or platform are you running Emacs on?
    validations:
      required: true
  - type: textarea
    id: emacs-provenance
    attributes:
      label: Emacs version and provenance
      description: What version of Emacs are you using, where did you acquire it, and how did you install it?
    validations:
      required: true
  - type: input
    id: emacs-command
    attributes:
      label: Emacs command
      description: By what method did you run Emacs?  (i.e. what command did you run?)
    validations:
      required: true
  - type: input
    id: emacs-frame
    attributes:
      label: Emacs frame type
      description: Did the problem happen on a GUI or tty Emacs frame?
    validations:
      required: true
  - type: input
    id: package-provenance
    attributes:
      label: Ement package version and provenance
      description: What version of Ement.el are you using, where did you acquire it, and how did you install it?
    validations:
      required: true
  - type: textarea
    id: actions
    attributes:
      label: Actions taken
      description: What actions did you take, step-by-step, in order, before the problem was noticed?
    validations:
      required: true
  - type: textarea
    id: results
    attributes:
      label: Observed results
      description: What behavior did you observe that seemed wrong?
    validations:
      required: true
  - type: textarea
    id: expected
    attributes:
      label: Expected results
      description: What behavior did you expect to observe?
    validations:
      required: true
  - type: textarea
    id: backtrace
    attributes:
      label: Backtrace
      description: If an error was signaled, please use `M-x toggle-debug-on-error RET` and cause the error to happen again, then paste the contents of the `*Backtrace*` buffer here.
      render: elisp
  - type: textarea
    id: etc
    attributes:
      label: Etc.
      description: Any other information that seems relevant



================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true


================================================
FILE: .github/workflows/test.yml
================================================
# * test.yml --- Test Emacs packages using makem.sh on GitHub Actions

# URL: https://github.com/alphapapa/makem.sh
# Version: 0.4.2

# * Commentary:

# Based on Steve Purcell's examples at
# <https://github.com/purcell/setup-emacs/blob/master/.github/workflows/test.yml>,
# <https://github.com/purcell/package-lint/blob/master/.github/workflows/test.yml>.

# * 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/>.

# * Code:

name: "CI"
on:
  pull_request:
  push:
    # Comment out this section to enable testing of all branches.
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        emacs_version:
          - 27.2
          - 28.2
          - 29.1
          - snapshot
    steps:
    - uses: purcell/setup-emacs@master
      with:
        version: ${{ matrix.emacs_version }}

    - uses: actions/checkout@v2

    - name: Install Ispell
      run: |
        sudo apt-get install ispell

    - name: Initialize sandbox
      run: |
        SANDBOX_DIR=$(mktemp -d) || exit 1
        echo "SANDBOX_DIR=$SANDBOX_DIR" >> $GITHUB_ENV
        ./makem.sh -vv --sandbox=$SANDBOX_DIR --install-deps --install-linters

    # The "all" rule is not used, because it treats compilation warnings
    # as failures, so linting and testing are run as separate steps.

    - name: Lint
      # NOTE: Uncomment this line to treat lint failures as passing
      #       so the job doesn't show failure.
      # continue-on-error: true
      run: ./makem.sh -vv --sandbox=$SANDBOX_DIR lint

    - name: Test
      if: always()  # Run test even if linting fails.
      run: ./makem.sh -vv --sandbox=$SANDBOX_DIR test

# Local Variables:
# eval: (outline-minor-mode)
# End:


================================================
FILE: .gitignore
================================================
/.sandbox/
*.elc
/worktrees/
/.#*


================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU 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 <http://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
<http://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
<http://www.gnu.org/philosophy/why-not-lgpl.html>.


================================================
FILE: Makefile
================================================
# * makem.sh/Makefile --- Script to aid building and testing Emacs Lisp packages

# URL: https://github.com/alphapapa/makem.sh
# Version: 0.5

# * Arguments

# For consistency, we use only var=val options, not hyphen-prefixed options.

# NOTE: I don't like duplicating the arguments here and in makem.sh,
# but I haven't been able to find a way to pass arguments which
# conflict with Make's own arguments through Make to the script.
# Using -- doesn't seem to do it.

ifdef install-deps
	INSTALL_DEPS = "--install-deps"
endif
ifdef install-linters
	INSTALL_LINTERS = "--install-linters"
endif

ifdef sandbox
	ifeq ($(sandbox), t)
		SANDBOX = --sandbox
	else
		SANDBOX = --sandbox=$(sandbox)
	endif
endif

ifdef debug
	DEBUG = "--debug"
endif

# ** Verbosity

# Since the "-v" in "make -v" gets intercepted by Make itself, we have
# to use a variable.

verbose = $(v)

ifneq (,$(findstring vvv,$(verbose)))
	VERBOSE = "-vvv"
else ifneq (,$(findstring vv,$(verbose)))
	VERBOSE = "-vv"
else ifneq (,$(findstring v,$(verbose)))
	VERBOSE = "-v"
endif

# * Rules

# TODO: Handle cases in which "test" or "tests" are called and a
# directory by that name exists, which can confuse Make.

%:
	@./makem.sh $(DEBUG) $(VERBOSE) $(SANDBOX) $(INSTALL_DEPS) $(INSTALL_LINTERS) $(@)

.DEFAULT: init
init:
	@./makem.sh $(DEBUG) $(VERBOSE) $(SANDBOX) $(INSTALL_DEPS) $(INSTALL_LINTERS)


================================================
FILE: README.org
================================================
#+TITLE: Ement.el

#+PROPERTY: LOGGING nil

# Export options.
#+OPTIONS: broken-links:t *:t num:1 toc:1

# Info export options.
#+EXPORT_FILE_NAME: ement.texi
#+TEXINFO_DIR_CATEGORY: Emacs
#+TEXINFO_DIR_TITLE: Ement: (ement)
#+TEXINFO_DIR_DESC: Matrix client for Emacs

# Note: This readme works with the org-make-toc <https://github.com/alphapapa/org-make-toc> package, which automatically updates the table of contents.

#+HTML: <img src="images/logo-128px.png" align="right">

# ELPA badge image.
[[https://elpa.gnu.org/packages/ement.html][https://elpa.gnu.org/packages/ement.svg]]

Ement.el is a [[http://www.matrix.org/][Matrix]] client for [[https://www.gnu.org/software/emacs/][GNU Emacs]].  It aims to be simple, fast, featureful, and reliable, while integrating naturally with Emacs.

Feel free to join us in the chat room: [[https://matrix.to/#/#ement.el:matrix.org][https://img.shields.io/matrix/ement.el:matrix.org.svg?label=%23ement.el:matrix.org]]

* Contents                                                         :noexport:
:PROPERTIES:
:TOC:      :include siblings
:END:
:CONTENTS:
- [[#installation][Installation]]
- [[#usage][Usage]]
  - [[#bindings][Bindings]]
  - [[#tips][Tips]]
  - [[#encrypted-room-support-through-pantalaimon][Encrypted room support through Pantalaimon]]
- [[#changelog][Changelog]]
- [[#development][Development]]
:END:

* Screenshots                                                      :noexport:
:PROPERTIES:
:ID:       d818f690-5f22-4eb0-83e1-4d8ce16c9e5b
:END:

The default formatting style resembles IRC clients, with each message being prefixed by the username (which enables powerful Emacs features, like using Occur to show all messages from or mentioning a user).  Alternative, built-in styles include an Element-like one with usernames above groups of messages, as well as a classic, no-margins IRC style.  Messages may be optionally displayed with unique colors for each user (with customizable contrast), making it easier to follow conversations.  Timestamp headers are optionally displayed where a certain amount of time passes between events, as well as where the date changes.

[[images/ement-for-twim.png]]

/Two rooms shown in side-by-side buffers, showing inline images, reactions, date/time headings, room avatars, and messages colored by user (using the modus-vivendi Emacs theme)./

[[images/emacs-with-fully-read-line.png]]

/#emacs:libera.chat showing colored text from IRC users, replies with quoted parts, messages colored by user, addressed usernames colored by their user color, highlighted mentions, and the fully-read marker line (using the modus-vivendi Emacs theme)./

[[images/screenshot5.png]]

/Four rooms shown at once, with messages colored by user, in the default Emacs theme./

[[images/screenshot2.png]]

/A room at the top in the "Elemental" display style, with sender names displayed over groups of messages, and only self-messages in an alternate color.  The lower window shows an earlier version of the rooms list./

[[images/reactions.png]]

/Reactions displayed as color emojis (may need [[#displaying-symbols-and-emojis][proper Emacs configuration]])./

* Installation
:PROPERTIES:
:TOC:      :depth 0
:END:

** GNU ELPA

Ement.el is published in [[http://elpa.gnu.org/][GNU ELPA]] as [[https://elpa.gnu.org/packages/ement.html][ement]], so it may be installed in Emacs with the command ~M-x package-install RET ement RET~.  This is the recommended way to install Ement.el, as it will install the current stable release.

The latest development build may be installed from [[https://elpa.gnu.org/devel/ement.html][ELPA-devel]] or from Git (see below).

** GNU Guix

Ement.el is available in [[https://guix.gnu.org/][GNU Guix]] as [[https://packages.guix.gnu.org/packages/emacs-ement/][emacs-ement]].

** Debian, Ubuntu

Ement.el is available in [[https://packages.debian.org/elpa-ement][Debian as elpa-ement]] and in [[https://packages.ubuntu.com/search?suite=default&section=all&arch=any&keywords=elpa-ement&searchon=names][Ubuntu as elpa-ement]].

** Nix

Ement.el is available in [[https://nixos.org/][NixOS]] as [[https://search.nixos.org/packages?channel=23.05&show=emacsPackages.ement&from=0&size=50&sort=relevance&type=packages&query=ement][emacsPackages.ement]].

** Other distributions

Ement.el is also available in some other distributions.  See [[https://repology.org/project/emacs:ement/related][Repology]] for details.

** Git master

The ~master~ branch of the Git repository is intended to be usable at all times; only minor bugs are expected to be found in it before a new stable release is made.

To install, it is recommended to use [[https://github.com/quelpa/quelpa-use-package][quelpa-use-package]], like this (using [[https://github.com/alphapapa/unpackaged.el#upgrade-a-quelpa-use-package-forms-package][this helpful command]] for upgrading versions):

#+BEGIN_SRC elisp
  ;; Install and load `quelpa-use-package'.
  (package-install 'quelpa-use-package)
  (require 'quelpa-use-package)

  ;; Install Ement.
  (use-package ement
    :quelpa (ement :fetcher github :repo "alphapapa/ement.el"))
#+END_SRC

One might also use systems like [[https://github.com/progfolio/elpaca][Elpaca]] or [[https://github.com/radian-software/straight.el][Straight]] (which is also used by [[https://github.com/doomemacs/doomemacs][DOOM]]), but the author cannot offer support for them.

** Manual

Ement.el is intended to be installed with Emacs's package system, which will ensure that the required autoloads are generated, etc.  If you choose to install it manually, you're on your own.

* Usage
:PROPERTIES:
:TOC:      :include descendants :depth 1
:END:
:CONTENTS:
- [[#bindings][Bindings]]
- [[#tips][Tips]]
- [[#encrypted-room-support-through-pantalaimon][Encrypted room support through Pantalaimon]]
:END:

1. Call command ~ement-connect~ to connect.  Multiple sessions are supported: call the command again with a ~C-u~ universal prefix to connect to another account.
2. Wait for initial sync to complete (which can take a few moments--initial sync JSON responses can be large).
3. Use these commands (room-related commands may be called with universal prefix to prompt for the room):
   - ~ement-list-rooms~ to view the list of joined rooms.
   - ~ement-view-room~ to view a room's buffer, selected with completion.
   - ~ement-create-room~ to create a new room.
   - ~ement-create-space~ to create a space.
   - ~ement-invite-user~ to invite a user to a room.
   - ~ement-join-room~ to join a room.
   - ~ement-leave-room~ to leave a room.
   - ~ement-forget-room~ to forget a room.
   - ~ement-tag-room~ to toggle a tag on a room (including favorite/low-priority status).
   - ~ement-list-members~ to list members in a room.
   - ~ement-send-direct-message~ to send a direct message to a user (in an existing direct room, or creating a new one automatically).
   - ~ement-room-edit-message~ to edit a message at point.
   - ~ement-room-send-file~ to send a file.
   - ~ement-room-send-image~ to send an image.
   - ~ement-room-set-topic~ to set a room's topic.
   - ~ement-room-occur~ to search in a room's known events.
   - ~ement-room-override-name~ to override a room's display name.
   - ~ement-ignore-user~ to ignore a user (or with interactive prefix, un-ignore).
   - ~ement-room-set-message-format~ to set a room's message format buffer-locally.
   - ~ement-room-toggle-space~ to toggle a room's membership in a space (a way to group rooms in Matrix).
   - ~ement-directory~ to view a room directory.
   - ~ement-directory-search~ to search a room directory.
4. Use these special buffers to see events from multiple rooms (you can also reply to messages from these buffers!):
   - See all new events that mention you in the =*Ement Mentions*= buffer.
   - See all new events in rooms that have open buffers in the =*Ement Notifications*= buffer.

** Bindings

These bindings are common to all of the following buffer types:

+ Switch to a room buffer: ~M-g M-r~
+ Switch to the room list buffer: ~M-g M-l~
+ Switch to the mentions buffer: ~M-g M-m~
+ Switch to the notifications buffer: ~M-g M-n~

*** Room buffers

Note that if global minor mode ~ement-room-self-insert-mode~ is enabled (by default it is disabled), typing any of the common printable ascii characters (such as letters) in a room buffer will start a new message, and most of the following bindings are instead accessed via a prefix key.  See the minor mode docstring for details.  (The ~?~ binding is an exception; by default it opens the command menu regardless of this minor mode.)

+ Show command menu: ~?~

[[images/transient.png]]

*Movement*

+ Next event: ~n~
+ Previous event: ~p~
+ End of buffer: ~N~
+ Scroll up and mark read: ~SPC~
+ Scroll down: ~S-SPC~
+ Jump to fully-read marker: ~M-g M-p~
+ Move read markers to point: ~m~
+ Load older messages: at top of buffer, scroll contents up (i.e. ~S-SPC~, ~M-v~ or ~mwheel-scroll~)

*Switching*

+ List rooms: ~M-g M-l~
+ Switch to other room: ~M-g M-r~
+ Switch to mentions buffer: ~M-g M-m~
+ Switch to notifications buffer: ~M-g M-n~
+ Quit window: ~q~

*Messages*

+ Write message: ~RET~
+ Compose message in buffer: ~M-RET~ (while writing in minibuffer: ~C-c '‍~).  Customize the option ~ement-room-compose-method~ to make ~RET~ and the other message bindings use a compose buffer by default.  Use command ~ement-room-compose-org~ to activate Org mode in the compose buffer.
+ Write reply to event at point: ~S-<return>~
+ Edit message: ~<insert>~
+ Delete message: ~C-k~
+ Send reaction to event at point, or send same reaction at point: ~s r~
+ Send emote: ~s e~
+ Send file: ~s f~
+ Send image: ~s i~
+ View event source: ~v~
+ Complete members and rooms at point: ~C-M-i~ (standard ~completion-at-point~ command).  (Type an ~@~ prefix for a member mention, a ~#~ prefix for a room alias, or a ~!~ prefix for a room ID.)

*Images*

+ Toggle scale of image (between fit-to-window and thumbnail): ~mouse-1~
+ Show image in new buffer at full size: ~double-mouse-1~

*Users*

+ Send direct message: ~u RET~
+ Invite user: ~u i~
+ Ignore user: ~u I~

*Room*

+ Occur search in room: ~M-s o~
+ List members: ~r m~
+ Set topic: ~r t~
+ Set message format: ~r f~
+ Set notification rules: ~r n~
+ Override display name: ~r N~
+ Tag/untag room: ~r T~

*Room membership*

+ Create room: ~R c~
+ Join room: ~R j~
+ Leave room: ~R l~
+ Forget room: ~R F~
+ Toggle room's spaces: ~R s~

*Other*

+ Sync new messages (not necessary if auto sync is enabled; with prefix to force new sync): ~g~

*** Room list buffer

+ Show buffer of room at point: ~RET~
+ Show buffer of next unread room: ~SPC~
+ Move between room names: ~TAB~ / ~<backtab>~

+ Kill room's buffer: ~k~
+ Toggle room's membership in a space: ~s~

*** Directory buffers

+ View/join a room: ~RET~ / ~mouse-1~
+ Load next batch of rooms: ~+~

*** Mentions/notifications buffers

+ Move between events: ~TAB~ / ~<backtab>~
+ Go to event at point in its room buffer: ~RET~
+ Write reply to event at point (shows the event in its room while writing): ~S-<return>~

** Tips

# TODO: Show sending messages in Org format.

+ Desktop notifications are enabled by default for events that mention the local user.  They can also be shown for all events in rooms with open buffers.
+ Send messages in Org mode format by customizing the option ~ement-room-send-message-filter~ (which enables Org format by default), or by calling ~ement-room-compose-org~ in a compose buffer (which enables it for a single message).  Then Org-formatted messages are automatically converted and sent as HTML-formatted messages (with the Org syntax as the plain-text fallback).  You can send syntax such as:
  - Bold, italic, underline, strikethrough
  - Links
  - Tables
  - Source blocks (including results with ~:exports both~)
  - Footnotes (okay, that might be pushing it, but you can!)
  - And, generally, anything that Org can export to HTML
  - Note that the default ~org-export-preserve-breaks~ value causes singular line breaks to be exported as spaces.  To preserve the line breaks, indentation, and blank lines in a region, but otherwise use normal formatting, you can use the ~verse~ block type.  Refer to ~(info "(org) Paragraphs")~ and ~(info "(org) Structure Templates")~ for details.
+ Starting in the room list buffer, by pressing ~SPC~ repeatedly, you can cycle through and read all rooms with unread buffers.  (If a room doesn't have a buffer, it will not be included.)
+ Room buffers and the room-list buffer can be bookmarked in Emacs, i.e. using =C-x r m=.  This is especially useful with [[https://github.com/alphapapa/burly.el][Burly]]: you can arrange an Emacs frame with several room buffers displayed at once, use =burly-bookmark-windows= to bookmark the layout, and then you can restore that layout and all of the room buffers by opening the bookmark, rather than having to manually arrange them every time you start Emacs or change the window configuration.
+ Images and other files can be uploaded to rooms using drag-and-drop.
+ Mention members by typing a ~@~ followed by their displayname or Matrix ID.  (Members' names and rooms' aliases/IDs may be completed with ~completion-at-point~ commands.)
+ Customize ~ement-room-use-variable-pitch~ to render messages using proportional fonts.
+ You can customize settings in the ~ement~ group.
  - *Note:* ~setq~ should not be used for certain options, because it will not call the associated setter function.  Users who have an aversion to the customization system may experience problems.

*** Displaying symbols and emojis

Emacs may not display certain symbols and emojis well by default.  Based on [[https://emacs.stackexchange.com/questions/62049/override-the-default-font-for-emoji-characters][this question and answer]], you may find that the simplest way to fix this is to install an appropriate font, like [[https://www.google.com/get/noto/#emoji-zsye][Noto Emoji]], and then use this Elisp code:

#+BEGIN_SRC elisp
  (setf use-default-font-for-symbols nil)
  (set-fontset-font t 'unicode "Noto Emoji" nil 'append)
#+END_SRC

** Encrypted room support through Pantalaimon

Ement.el doesn't support encrypted rooms natively, but it can be used transparently with the E2EE-aware reverse proxy daemon [[https://github.com/matrix-org/pantalaimon/][Pantalaimon]].  After configuring it according to its documentation, call ~ement-connect~ with the appropriate hostname and port, like:

#+BEGIN_SRC elisp
  (ement-connect :uri-prefix "http://localhost:8009")
#+END_SRC

* Changelog
:PROPERTIES:
:TOC:      :depth 0
:END:

** 0.18-pre

Nothing new yet.

** 0.17

*Additions*

+ Command ~ement-room-download-file~, which downloads the file in the event at point (for image, audio, video, and file messages).  ([[https://github.com/alphapapa/ement.el/pull/323][#323]].  Thanks to [[https://github.com/viiru-][Arto Jantunen]].)
+ Customization groups for faces.  (Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Option ~ement-room-hide-redacted-message-content~, which hides the content of redacted messages by default.  It may be disabled to keep redacted content visible with a strikethrough face, which may be useful for room moderators, but users should keep in mind that doing so will leave unpleasant content visible in the current session, even after being redacted by moderators.
+ Option ~ement-room-list-avatar-generation~: if disabled, SVG-based room avatars are not generated.  This option automatically tests whether SVG support is available in Emacs, and should allow use with builds of Emacs that lack =librsvg= support. 

*Changes*

+ Disable underline for faces ~ement-room-list-direct~ and ~ement-room-list-name~ (in case a face they inherit from enables it, e.g. when themed).

*Fixes*

+ Call ~eww-browse-url~ instead of ~browse-url~ in ~ement-room-browse-mxc~ (because the latter is not useful for authenticated media if the user has configured it to use a different browser).  ([[https://github.com/alphapapa/ement.el/pull/323][#323]].  Thanks to [[https://github.com/viiru-][Arto Jantunen]].)
+ Workaround change in ~magit-section~ that broke fontification in room-list and directory buffers.  (See [[https://github.com/alphapapa/ement.el/issues/331][#331]].)
+ Handle non-symbol commands in ~command-history~.  ([[https://github.com/alphapapa/ement.el/issues/330][#330]].  Thanks to [[https://github.com/stsquad][Alex Bennée]] for reporting.)

** 0.16

*Compatibility*

+ Use authenticated media requests (part of Matrix 1.11; see [[https://github.com/matrix-org/matrix-spec-proposals/pull/3916][MSC3916]] and [[https://matrix.org/blog/2024/06/26/sunsetting-unauthenticated-media/][matrix.org's sunsetting unauthenticated media]]).

*Additions*

+ When option ~ement-room-images~ is disabled (preventing automatic download and display of images), individual images may be shown by clicking the button in their events.

*Changes*

+ Option ~ement-room-coalesce-events~ may now be set to (and defaults to) a maximum number of events to coalesce together.  (This avoids potential performance problems in rare cases.  See [[https://github.com/alphapapa/ement.el/issues/247][#247]].  Thanks to [[https://github.com/viiru-][Arto Jantunen]] for reporting and [[https://github.com/sergiodj][Sergio Durigan Junior]] for testing.)

*Fixes*
+ Replies to edited messages are correctly sent to the original event (whereas previously they were sent to the edit, which caused reactions to not be shown).  ([[https://github.com/alphapapa/ement.el/issues/230][#230]], [[https://github.com/alphapapa/ement.el/issues/277][#277]].  Thanks to [[https://github.com/phil-s][Phil Sainty]] for suggesting, and to [[https://github.com/dionisos2][dionisos]] for reporting.)
+ Set ~filter-buffer-substring-function~ in room buffers to prevent undesired text properties from being included in copied text.  ([[https://github.com/alphapapa/ement.el/pull/278][#278]].  Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Command ~ement-disconnect~ no longer shows an error message.  ([[https://github.com/alphapapa/ement.el/issues/208][#208]].)
+ Retrieval of earlier events in a just-joined room.  ([[https://github.com/alphapapa/ement.el/issues/148][#148]].  Thanks to [[https://github.com/MagicRB][Richard Brežák]] for reporting, and to [[https://github.com/phil-s][Phil Sainty]] for testing.)
+ Cache computed displaynames in rooms (avoiding unnecessary reiteration and recalculation).  ([[https://github.com/alphapapa/ement.el/issues/298][#298]].  Thanks to [[https://github.com/Rutherther][Rutherther]] for reporting and testing, and to [[https://github.com/phil-s][Phil Sainty]].)
+ Customization group for options ~ement-room-mode-hook~ and ~ement-room-self-insert-mode~.  (Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Inheritance for some faces.  ([[https://github.com/alphapapa/ement.el/pull/303][#303]].  Thanks to [[https://github.com/tarsius][Jonas Bernoulli]].) 

** 0.15.1

*Fixes*
+ Handle unnamed rooms in ~ement-directory~ list.  (See [[https://github.com/alphapapa/ement.el/issues/248][#248]].  Thanks to [[https://github.com/hjozwiak][Hunter Jozwiak]] and [[https://github.com/bmp][Bharath Palavalli]] for reporting.)
+ Don't use ~cl-type~ ~pcase~ form in Emacs versions before 28.  ([[https://github.com/alphapapa/ement.el/issues/279][#279]].  Thanks to [[https://github.com/AdamBark][Adam Bark]] for reporting.)

** 0.15
:PROPERTIES:
:ID:       81b48364-56a7-4903-b354-b79905edb039
:END:

*Additions*

+ Configurable emoji picker for sending reactions.  ([[https://github.com/alphapapa/ement.el/issues/199][#199]], [[https://github.com/alphapapa/ement.el/pull/201][#201]].  Thanks to [[https://github.com/oantolin][Omar Antolín Camarena]].) ::
  - Option ~ement-room-reaction-picker~ sets the default picker.  Within that, the user may press ~C-g~ to choose a different one with a key bound in ~ement-room-reaction-map~.

+ A variety of enhancements for using compose buffers.  ([[https://github.com/alphapapa/ement.el/issues/140][#140]].  Thanks to [[https://github.com/phil-s][Phil Sainty]].) :: Chiefly, messages can now be composed in small windows below room windows, rather than in the minibuffer or a full-sized window.  A variety of options and commands are available related to these features.  See [[#compose-buffer-enhancements][compose buffer enhancements]].

+ Global minor mode ~ement-room-self-insert-mode~ enables "just typing" to start a message.  (Thanks to [[https://github.com/phil-s][Phil Sainty]].) :: See [[#ement-room-self-insert-mode][ement-room-self-insert-mode]].

+ Options affecting how images are displayed in room buffers. :: See [[#image-display][image display]].

*Changes*

+ Improve prompt used when viewing a room that is not joined.  ([[https://github.com/alphapapa/ement.el/issues/241][#241]].  Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Format "was kicked and rejoined" membership event pairs.
+ Enclose reasons for membership events in quotes for clarity.
+ Improve default room list grouping.
+ When editing or replying to a message in a compose buffer, the related room event is highlighted persistently until the compose buffer is killed.  (Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ In compose buffers ~dabbrev~ will prioritise firstly the associated room, and secondly all other rooms, before looking to other buffers for completions.  (Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Aborted messages are now added to ~ement-room-message-history~ rather than the kill-ring.  (Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Prefix bindings in ~ement-room-mode-map~ now have named labels in ~which-key~ and similar.  (Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Option: ~ement-room-use-variable-pitch~ (previously named ~ement-room-shr-use-fonts~) enables variable-pitch fonts for all message types.  (This option previously supported formatted messages, but now works for plain text messages as well.)  Note: users who have customized the ~ement-room-message-text~ face to be variable-pitch should revert that change, as it causes problems for formatted messages, and is no longer necessary.  ([[https://github.com/alphapapa/ement.el/issues/174][#174]].  Thanks to [[https://github.com/phil-s][Phil Sainty]].)

*Fixes*

+ Edits to previous edit events are correctly sent to the server as edits to the original message event.  ([[https://github.com/alphapapa/ement.el/issues/230][#230]].  Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Completion at point works more reliably in compose buffers.  (Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Toggling images to fill the window body no longer triggers unintended scrolling.  (Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Recognition of mentions after a newline.  ([[https://github.com/alphapapa/ement.el/issues/267][#267]].  Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Newlines in ~ement-room-message-format-spec~ are considered when calculating the wrap-prefix.  (Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Weight of face ~ement-room-list-direct~ (now correctly bold in room list heading).

*** Compose buffer enhancements
:PROPERTIES:
:CUSTOM_ID: compose-buffer-enhancements
:END:

- Option ~ement-room-compose-buffer-display-action~ declares how and where a new compose buffer window should be displayed.  (By default, in a new window below the associated room buffer.)
- Option ~ement-room-compose-buffer-window-dedicated~ determines whether compose buffers will have dedicated windows.
- Option ~ement-room-compose-buffer-window-auto-height~ causes dynamic scaling of the compose buffer window height so that the full message is visible at all times.
- Option ~ement-room-compose-buffer-window-auto-height-min~ specifies the minimum window height when ~ement-room-compose-buffer-window-auto-height~ is enabled.
- Option ~ement-room-compose-buffer-window-auto-height-max~ specifies the maximum window height when ~ement-room-compose-buffer-window-auto-height~ is enabled.
- Option ~ement-room-compose-method~ chooses between minibuffer-centric or compose-buffer-centric behaviour.
- Command ~ement-room-dispatch-new-message~ starts writing a new message using your chosen ~ement-room-compose-method~.  (Bound to ~RET~ in room buffers.)
- Command ~ement-room-dispatch-new-message-alt~ starts writing a new message using the alternative method.  (Bound to ~M-RET~ in room buffers.)
- Command ~ement-room-dispatch-edit-message~ edits a message using your chosen ~ement-room-compose-method~.  (Bound to ~<insert>~ in room buffers.)
- Command ~ement-room-dispatch-reply-to-message~ replies to a message using your chosen ~ement-room-compose-method~.  (Bound to ~S-<return>~ in room buffers.)
- Command ~ement-room-compose-edit~ edits a message using a compose buffer.
- Command ~ement-room-compose-reply~ replies to a message using a compose buffer.
- Command ~ement-room-compose-send-direct~ sends a message directly from a compose buffer (without the minibuffer).  (Bound to ~C-x C-s~ in compose buffers.)
- Command ~ement-room-compose-abort~ kills the compose buffer and delete its window.  (Bound to ~C-c C-k~ in compose buffers.)
- Command ~ement-room-compose-abort-no-history~ does the same without adding to ~ement-room-message-history~.  (Equivalent to ~C-u C-c C-k~.)
- Command ~ement-room-compose-history-prev-message~ cycles backwards through ~ement-room-message-history~.  (Bound to ~M-p~ in compose buffers.)
- Command ~ement-room-compose-history-next-message~ cycles forwards through ~ement-room-message-history~.  (Bound to ~M-n~ in compose buffers.)
- Command ~ement-room-compose-history-isearch-backward~ initiates an isearch through ~ement-room-message-history~.  (Bound to ~M-r~ in compose buffers; continue searching with ~C-r~ or ~C-s~.)
- Command ~ement-room-compose-history-isearch-backward-regexp~ initiates a regexp isearch through ~ement-room-message-history~.  (Bound to ~C-M-r~ in compose buffers; continue searching with ~C-r~ or ~C-s~.)

*** ~ement-room-self-insert-mode~
:PROPERTIES:
:CUSTOM_ID: ement-room-self-insert-mode
:END:

- Option ~ement-room-self-insert-commands~ determines which commands will start a new message when ~ement-room-self-insert-mode~ is enabled (defaulting to ~self-insert-command~ and ~yank~).
- Option ~ement-room-self-insert-chars~ determines which typed characters will start a new message when ~ement-room-self-insert-mode~ is enabled (regardless of whether they are bound to ~self-insert-command~).
- Option ~ement-room-mode-map-prefix-key~ defines a prefix key for accessing the full ~ement-room-mode-map~ when ~ement-room-self-insert-mode~ is enabled.  (By default this key is ~DEL~.)
 
*** Image display
:PROPERTIES:
:CUSTOM_ID: image-display
:END:

- Option ~ement-room-image-margin~ is the number of pixels of margin around image thumbnails.
- Option ~ement-room-image-relief~ is the number of pixels of shadow rectangle around image thumbnails.
- Option ~ement-room-image-thumbnail-height~ is the window body height multiple to use when toggling full-sized images to thumbnails (by default, 0.2).
- Option ~ement-room-image-thumbnail-height-min~ is the minimum pixel height for thumbnail images (by default, 30 pixels).



** 0.14

*Additions*

+ Audio events are rendered as a link to the audio file.  (Thanks to [[https://github.com/viiru-][Arto Jantunen]].)
+ Customization group ~ement-room-list~.
+ Option ~ement-room-list-space-prefix~ is applied to space names in the room list (e.g. set to empty string for cleaner appearance).
+ Option ~ement-room-reaction-names-limit~ sets how many senders of a reaction are shown in the buffer (more than that many are shown in the tooltip).

*Changes*

+ Bind ~TAB~ / ~BACKTAB~ to move between links in room and like buffers.  ([[https://github.com/alphapapa/ement.el/issues/113][#113]].  Thanks to [[https://github.com/ericsfraga][Eric S. Fraga]] for suggesting.)

*Fixes*

+ Insertion of sender headers (when using "Elemental" message format).  (Refactoring contributed by [[https://github.com/Stebalien][Steven Allen]].)
+ Some room event data was being unintentionally serialized to disk when caching the room list visibility state. ([[https://github.com/alphapapa/ement.el/issues/256][#256]])
+ Notifications buffer restores properly when bookmarked.
+ Command ~ement-room-send-reaction~ checks for an event at point.  (Thanks to [[https://github.com/phil-s][Phil Sainty]].)

** 0.13

*Additions*

+ Group joined direct rooms in directory buffers.
+ Command ~end-of-buffer~ is bound to ~N~ in room buffers.

*Changes*

+ Command ~ement-room-image-show~ use frame parameters to maximize the frame, making it easier for users to override.  ([[https://github.com/alphapapa/ement.el/issues/223][#223]].  Thanks to [[https://github.com/progfolio][Nicholas Vollmer]].)

*Fixes*

+ Name for direct rooms in directory buffers.
+ Editing a message from the compose buffer would be sent as a reply to the edited message.  (Fixes [[https://github.com/alphapapa/ement.el/issues/189][#189]].  Thanks to [[https://github.com/phil-s][Phil Sainty]] for reporting.)
+ Editing an already-edited message.  ([[https://github.com/alphapapa/ement.el/issues/226][#226]].  Thanks to [[https://github.com/phil-s][Phil Sainty]] for reporting.)
+ Replying to an already-edited message.  ([[https://github.com/alphapapa/ement.el/issues/227][#227]].  Thanks to [[https://github.com/phil-s][Phil Sainty]] for reporting.)
+ Rendering redactions of edited messages.  ([[https://github.com/alphapapa/ement.el/issues/228][#228]].  Thanks to [[https://github.com/phil-s][Phil Sainty]] for reporting.)
+ Redacting an edited message.  ([[https://github.com/alphapapa/ement.el/issues/228][#228]].  Thanks to [[https://github.com/phil-s][Phil Sainty]] for reporting.)
+ Command ~ement-room-flush-colors~ maintains point position.

** 0.12

*Additions*

+ Command ~ement-notifications~ shows recent notifications, similar to the pane in the Element client.  (This new command fetches recent notifications from the server and allows scrolling up to retrieve older ones.  Newly received notifications, as configured in the ~ement-notify~ options, are displayed in the same buffer.  This functionality will be consolidated in the future.)
+ Face ~ement-room-quote~, applied to quoted parts of replies.

*Changes*
+ Commands ~ement-room-goto-next~ and ~ement-room-goto-prev~ work more usefully at the end of a room buffer.  (Now pressing ~n~ on the last event moves point to the end of the buffer so it will scroll automatically for new messages, and then pressing ~p~ skips over any read marker to the last event.)
+ Room buffer bindings:
  + ~ement-room-goto-next~ and ~ement-room-goto-prev~ are bound to ~n~ and ~p~, respectively.
  + ~ement-room-goto-fully-read-marker~ is bound to ~M-g M-p~ (the mnemonic being "go to previously read").
+ The quoted part of a reply now omits the face applied to the rest of the message, helping to distinguish them.
+ Commands that read a string from the minibuffer in ~ement-room~ buffers and ~ement-connect~ user ID prompts use separate history list variables.
+ Use Emacs's Jansson-based JSON-parsing functions when available.  (This results in a 3-5x speed improvement for parsing JSON responses, which can be significant for large initial sync responses.  Thanks to [[https://github.com/rrix/][Ryan Rix]] for discovering this!)

*Fixes*

+ File event formatter assumed that file size metadata would be present (a malformed, e.g. spam, event might not have it).
+ Send correct file size when sending files/images.
+ Underscores are no longer interpreted as denoting subscripts when sending messages in Org format.  (Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Add workaround for ~savehist-mode~'s serializing of the ~command-history~ variable's arguments.  (For ~ement-~ commands, that may include large data structures, like ~ement-session~ structs, which should never be serialized or reused, and ~savehist~'s doing so could cause noticeable delays for users who enabled it).  (See [[https://github.com/alphapapa/ement.el/issues/216][#216]].  Thanks to [[https://github.com/phil-s][Phil Sainty]] and other users who helped to discover this problem.)

** 0.11

*Additions*
+ Commands ~ement-room-image-show~ and ~ement-room-image-scale~ (bound to ~RET~ and ~M-RET~ when point is at an image) view and scale images.  (Thanks to [[https://github.com/Stebalien][Steven Allen]] for these and other image-related improvements.)
+ Command ~ement-room-image-show-mouse~ is used to show an image with the mouse.

*Changes*
+ Enable ~image-mode~ when showing images in a new buffer.  (Thanks to [[https://github.com/Stebalien][Steven Allen]].)
+ Command ~ement-room-image-show~ is not used for mouse events.
+ Show useful message in SSO login page.

*Fixes*
+ Allow editing of already-edited events.
+ Push rules' actions may be listed in any order.  (Fixes compatibility with [[https://spec.matrix.org/v1.7/client-server-api/#actions][v1.7 of the spec]].  Thanks to [[https://github.com/Stebalien][Steven Allen]].)
+ Call external browser for SSO login page.  (JavaScript is usually required, which EWW doesn't support, and loading the page twice seems to change state on the server that causes the SSO login to fail, so it's best to load the page in the external browser directly).
+ Clean up SSO server process after two minutes in case SSO login fails.
+ Don't stop syncing if an error is signaled while sending a notification.
+ Command ~ement-room-list-next-unread~ could enter an infinite loop.  (Thanks to [[https://github.com/vizs][Visuwesh]] and ~@mrtnmrtn:matrix.org~.)
+ Events in notifications buffer could appear out-of-order.  ([[https://github.com/alphapapa/ement.el/issues/191][#191]].  Thanks to [[https://github.com/phil-s][Phil Sainty]].)

*Internal*
+ The ~ement-read-receipt-idle-timer~ could be duplicated when using multiple sessions.  ([[https://github.com/alphapapa/ement.el/issues/196][#196]].  Thanks to [[https://github.com/phil-s][Phil Sainty]].)

** 0.10

*Security Fixes*
+ When uploading a GPG-encrypted file (i.e. one whose filename ends in ~.gpg~), if the recipient's private key or the symmetric encryption key were cached by Emacs (or a configured agent, like ~gpg-agent~), Emacs would automatically decrypt the file while reading its contents and then upload the decrypted contents.  (This happened because the function ~insert-file-contents~ was used, which does many things automatically, some of which are not even mentioned in its docstring; refer to its entry in the Elisp Info manual for details.  The fix is to use ~insert-file-contents-literally~ instead.)  Thanks to ~@welkinsl:matrix.org~ for reporting.

*Additions*
+ Support for Single Sign-On (SSO) authentication.  ([[https://github.com/alphapapa/ement.el/issues/24][#24]].  Thanks to [[https://github.com/Necronian][Jeffrey Stoffers]] for development, and to [[https://github.com/phil-s][Phil Sainty]], [[https://github.com/FrostyX][Jakub Kadlčík]], and [[https://github.com/oneingan][Juanjo Presa]] for testing.)
+ Bind ~m~ in room buffers to ~ement-room-mark-read~ (which moves read markers to point).

*Changes*

+ Activating a space in the room list uses ~ement-view-space~ (which shows a directory of rooms in the space) instead of ~ement-view-room~ (which shows events in the space, which is generally not useful).
+ Command ~ement-view-room~, when used for a space, shows a footer explaining that the buffer is showing a space rather than a normal room, with a button to call ~ement-view-space~ for it (which lists rooms in the space).
+ Command ~ement-describe-room~ shows whether a room is a space or a normal room.
+ Command ~ement-view-space~ shows the space's name and alias.
+ Command ~ement-room-scroll-up-mark-read~ moves the fully read marker to the top of the window (when the marker's position is within the range of known events), rather than only moving it when at the end of the buffer.  (This eases the process of gradually reading a long backlog of messages.)
+ Improve readme export settings.

*Fixes*
+ Extra indentation of some membership events.  (Thanks to [[https://github.com/Stebalien][Steven Allen]].)
+ Customization group for faces.
+ Don't reinitialize ~ement-room-list-mode~ when room list buffer is refreshed.  ([[https://github.com/alphapapa/ement.el/issues/146][#146]].  Thanks to [[https://github.com/treed][Ted Reed]] for reporting.)
+ Don't fetch old events when scrolling to the bottom of a room buffer (only when scrolling to the top).  (Thanks to [[https://github.com/Stebalien][Steven Allen]].)
+ Minor improvements to auto-detection of homeserver URIs.  (See [[https://github.com/alphapapa/ement.el/issues/24#issuecomment-1569518713][#24]].  Thanks to [[https://github.com/phil-s][Phil Sainty]].)
+ Uploading of certain filetypes (e.g. Emacs would decompress some archives before uploading).  Thanks to ~@welkinsl:matrix.org~ for reporting.
+ Messages edited multiple times sometimes weren't correctly replaced.

** 0.9.3

*Fixes*
+ Another attempt at restoring position in room list when refreshing.
+ Command ~ement-room-list-next-unread~.

** 0.9.2

*Fixes*
+ Restore position in room list when refreshing.
+ Completion in minibuffer.

** 0.9.1

*Fixes*
+ Error in ~ement-room-list~ command upon initial sync.

** 0.9

*Additions*

+ Option ~ement-room-timestamp-header-align~ controls how timestamp headers are aligned in room buffers.
+ Option ~ement-room-view-hook~ runs functions when ~ement-room-view~ is called.  (By default, it refreshes the room list buffer.)
+ In the room list, middle-clicking a room which has a buffer closes its buffer.
+ Basic support for video events.  (Thanks to [[https://github.com/viiru-][Arto Jantunen]].)

*Changes*

+ Using new option ~ement-room-timestamp-header-align~, timestamp headers default to right-aligned.  (With default settings, this keeps them near message timestamps and makes for a cleaner appearance.)

*Fixes*

+ Recognition of certain MXID or displayname forms in outgoing messages when linkifying (aka "pilling") them.
+ Unreadable room avatar images no longer cause errors.  (Fixes [[https://github.com/alphapapa/ement.el/issues/147][#147]].  Thanks to [[https://github.com/jgarte][@jgarte]] for reporting.)
+ Don't error in ~ement-room-list~ when no rooms are joined.  (Fixes [[https://github.com/alphapapa/ement.el/issues/123][#123]].  Thanks to [[https://github.com/Kabouik][@Kabouik]] and [[https://github.com/oantolin][Omar Antolín Camarena]] for reporting.)
+ Enable member/room completion in compose buffers.  (Fixes [[https://github.com/alphapapa/ement.el/issues/115][#115]].  Thanks to Thanks to [[https://github.com/piater][Justus Piater]] and [[https://github.com/chasecaleb][Caleb Chase]] for reporting.)

** 0.8.3

*Fixes*

+ Avoid use of ~pcase~'s ~(map :KEYWORD)~ form.  (This can cause a broken installation on older versions of Emacs that have an older version of the ~map~ library loaded, such as Emacs 27.2 included in Debian 11.  Since there's no way to force Emacs to actually load the version of ~map~ required by this package before installing it (which would naturally happen upon restarting Emacs), we can only avoid using such forms while these versions of Emacs are widely used.)

** 0.8.2

*Fixes*

+ Deduplicate grouped membership events.

** 0.8.1

Added missing changelog entry (of course).

** 0.8

*Additions*
+ Command ~ement-create-space~ creates a new space.
+ Command ~ement-room-toggle-space~ toggles a room's membership in a space (a way to group rooms in Matrix).
+ Visibility of sections in the room list is saved across sessions.
+ Command ~ement-room-list-kill-buffer~ kills a room's buffer from the room list.
+ Set ~device_id~ and ~initial_device_display_name~ upon login (e.g. =Ement.el: username@hostname=).  ([[https://github.com/alphapapa/ement.el/issues/134][#134]].  Thanks to [[https://github.com/viiru-][Arto Jantunen]] for reporting.)

*Changes*

+ Room-related commands may be called interactively with a universal prefix to prompt for the room/session (allowing to send events or change settings in rooms other than the current one).
+ Command ~ement-room-list~ reuses an existing window showing the room list when possible.  ([[https://github.com/alphapapa/ement.el/issues/131][#131]].  Thanks to [[https://github.com/jeffbowman][Jeff Bowman]] for suggesting.)
+ Command ~ement-tag-room~ toggles tags (rather than adding by default and removing when called with a prefix).
+ Default room grouping now groups "spaced" rooms separately.

*Fixes*

+ Message format filter works properly when writing replies.
+ Improve insertion of sender name headers when using the "Elemental" message format.
+ Prompts in commands ~ement-leave-room~ and ~ement-forget-room~.

** 0.7

*Additions*

+ Command ~ement-room-override-name~ sets a local override for a room's display name.  (Especially helpful for 1:1 rooms and bridged rooms.  See [[https://github.com/matrix-org/matrix-spec-proposals/pull/3015#issuecomment-1451017296][MSC3015]].)

*Changes*

+ Improve display of room tombstones (displayed at top and bottom of buffer, and new room ID is linked to join).
+ Use descriptive prompts in ~ement-leave-room~ and ~ement-forget-room~ commands.

*Fixes*

+ Command ~ement-view-space~ when called from a room buffer.  (Thanks to [[https://github.com/MagicRB][Richard Brežák]] for reporting.)
+ Don't call ~display-buffer~ when reverting room list buffer.  (Fixes [[https://github.com/alphapapa/ement.el/issues/121][#121]].  Thanks to [[https://github.com/mekeor][mekeor]] for reporting.)
+ Retry sync for network timeouts.  (Accidentally broken in v0.6.)

*Internal*

+ Function ~ement-put-account-data~ accepts ~:room~ argument to put on a room's account data.

** 0.6

*Additions*
+ Command ~ement-view-space~ to view a space's rooms in a directory buffer.

*Changes*
+ Improve ~ement-describe-room~ command (formatting, bindings).

*Fixes*
+ Retry sync for HTTP 502 "Bad Gateway" errors.
+ Formatting of unban events.
+ Update password authentication according to newer Matrix spec.  (Fixes compatibility with Conduit servers.  [[https://github.com/alphapapa/ement.el/issues/66][#66]].  Thanks to [[https://github.com/tpeacock19][Travis Peacock]], [[https://github.com/viiru-][Arto Jantunen]], and [[https://github.com/scd31][Stephen D]].)
+ Image scaling issues.  (Thanks to [[https://github.com/vizs][Visuwesh]].)

** 0.5.2

*Fixes*
+ Apply ~ement-initial-sync-timeout~ properly (important for when the homeserver is slow to respond).

** 0.5.1

*Fixes*
+ Autoload ~ement-directory~ commands.
+ Faces in ~ement-directory~ listings.

** 0.5

*Additions*
+ Present "joined-and-left" and "rejoined-and-left" membership event pairs as such.
+ Process and show rooms' canonical alias events.

*Changes*
+ The [[https://github.com/alphapapa/taxy.el][taxy.el]]-based room list, with programmable, smart grouping, is now the default ~ement-room-list~.  (The old, ~tabulated-list-mode~-based room list is available as ~ement-tabulated-room-list~.)
+ When selecting a room to view with completion, don't offer spaces.
+ When selecting a room with completion, empty aliases and topics are omitted instead of being displayed as nil.

*Fixes*
+ Use of send-message filter when replying.
+ Replies may be written in compose buffers.

** 0.4.1

*Fixes*
+ Don't show "curl process interrupted" message when updating a read marker's position again.

** 0.4

*Additions*
+ Option ~ement-room-unread-only-counts-notifications~, now enabled by default, causes rooms' unread status to be determined only by their notification counts (which are set by the server and depend on rooms' notification settings).
+ Command ~ement-room-set-notification-state~ sets a room's notification state (imitating Element's user-friendly presets).
+ Room buffers' Transient menus show the room's notification state (imitating Element's user-friendly presets).
+ Command ~ement-set-display-name~ sets the user's global displayname.
+ Command ~ement-room-set-display-name~ sets the user's displayname in a room (which is also now displayed in the room's Transient menu).
+ Column ~Notifications~ in the ~ement-taxy-room-list~ buffer shows rooms' notification state.
+ Option ~ement-interrupted-sync-hook~ allows customization of how sync interruptions are handled.  (Now, by default, a warning is displayed instead of merely a message.)

*Changes*
+ When a room's read receipt is updated, the room's buffer is also marked as unmodified.  (In concert with the new option, this makes rooms' unread status more intuitive.)

*Fixes*
+ Binding of command ~ement-forget-room~ in room buffers.
+ Highlighting of ~@room~ mentions.

** 0.3.1

*Fixes*
+ Room unread status (when the last event in a room is sent by the local user, the room is considered read).

** 0.3

*Additions*
+ Command ~ement-directory~ shows a server's room directory.
+ Command ~ement-directory-search~ searches a server's room directory.
+ Command ~ement-directory-next~ fetches the next batch of rooms in a directory.
+ Command ~ement-leave-room~ accepts a ~FORCE-P~ argument (interactively, with prefix) to leave a room without prompting.
+ Command ~ement-forget-room~ accepts a ~FORCE-P~ argument (interactively, with prefix) to also leave the room, and to forget it without prompting.
+ Option ~ement-notify-mark-frame-urgent-predicates~ marks the frame as urgent when (by default) a message mentions the local user or "@room" and the message's room has an open buffer.

*Changes*
+ Minor improvements to date/time headers.

*Fixes*
+ Command ~ement-describe-room~ for rooms without topics.
+ Improve insertion of old messages around existing timestamp headers.
+ Reduce D-Bus notification system check timeout to 2 seconds (from the default of 25).
+ Compatibility with Emacs 27.

** 0.2.1

*Fixes*
+ Info manual export filename.

** 0.2

*Changes*
+ Read receipts are re-enabled.  (They're now implemented with a global idle timer rather than ~window-scroll-functions~, which sometimes caused a strange race condition that could cause Emacs to become unresponsive or crash.)
+ When determining whether a room is considered unread, non-message events like membership changes, reactions, etc. are ignored.  This fixes a bug that caused certain rooms that had no message events (like some bridged rooms) to appear as unread when they shouldn't have.  But it's unclear whether this is always preferable (e.g. one might want a member leaving a room to cause it to be marked unread), so this is classified as a change rather than simply a fix, and more improvements may be made to this in the future.  (Fixes [[https://github.com/alphapapa/ement.el/issues/97][#97]].  Thanks to [[https://github.com/MrRoy][Julien Roy]] for reporting and testing.)
+ The ~ement-taxy-room-list~ view no longer automatically refreshes the list if the region is active in the buffer.  (This allows the user to operate on multiple rooms without the contents of the buffer changing before completing the process.)

*Fixes*
+ Links to only rooms (as opposed to links to events in rooms) may be activated to join them.
+ Read receipts mark the last completely visible event (rather than one that's only partially displayed).
+ Prevent error when a room avatar image fails to load.

** 0.1.4

*Fixed*
+ Info manual directory headers.

** 0.1.3

*Fixed*
# + Read receipt-sending function was called too many times when scrolling.
# + Send read receipts even when the last receipt is outside the range of retrieved events.
+ Temporarily disable sending of read receipts due to an unusual bug that could cause Emacs to become unresponsive.  (The feature will be re-enabled in a future release.)

** 0.1.2

*Fixed*
+ Function ~ement-room-sync~ correctly updates room-list buffers.  (Thanks to [[https://github.com/vizs][Visuwesh]].)
+ Only send D-Bus notifications when supported.  (Fixes [[https://github.com/alphapapa/ement.el/issues/83][#83]].  Thanks to [[https://github.com/tsdh][Tassilo Horn]].)

** 0.1.1

*Fixed*
+ Function ~ement-room-scroll-up-mark-read~ selects the correct room window.
+ Option ~ement-room-list-avatars~ defaults to what function ~display-images-p~ returns.

** 0.1

After almost two years of development, the first tagged release.  Submitted to GNU ELPA.

* Development
:PROPERTIES:
:TOC:      :include this :ignore descendants
:END:

Bug reports, feature requests, suggestions — /oh my/!

** Copyright Assignment
:PROPERTIES:
:TOC:      :ignore (this)
:END:

Ement.el is published in GNU ELPA and is considered part of GNU Emacs.  Therefore, cumulative contributions of more than 15 lines of code require that the author assign copyright of such contributions to the FSF.  Authors who are interested in doing so may contact [[mailto:assign@gnu.org][assign@gnu.org]] to request the appropriate form.

** Matrix spec in Org format
:PROPERTIES:
:TOC:      :ignore (this)
:END:

An Org-formatted version of the Matrix spec is available in the [[https://github.com/alphapapa/ement.el/tree/meta/spec][meta/spec]] branch.

** Rationale

/This section is preserved for posterity.  As it says, Ement.el has long since surpassed ~matrix-client~, which should no longer be used./

Why write a new Emacs Matrix client when there is already [[https://github.com/alphapapa/matrix-client.el][matrix-client.el]], by the same author, no less?  A few reasons:

- ~matrix-client~ uses an older version of the Matrix spec, r0.3.0, with a few elements of r0.4.0 grafted in.  Bringing it up to date with the current version of the spec, r0.6.1, would be more work than to begin with the current version.  Ement.el targets r0.6.1 from the beginning.
- ~matrix-client~ does not use Matrix's lazy-loading feature (which was added to the specification later), so initial sync requests can take a long time for the server to process and can be large (sometimes tens of megabytes of JSON for the client to process!).  Ement.el uses lazy-loading, which significantly improves performance.
- ~matrix-client~ automatically makes buffers for every room a user has joined, even if the user doesn't currently want to watch a room.  Ement.el opens room buffers on-demand, improving performance by not having to insert events into buffers for rooms the user isn't watching.
- ~matrix-client~ was developed without the intention of publishing it to, e.g. MELPA or ELPA.  It has several dependencies, and its code does not always install or compile cleanly due to macro-expansion issues (apparently depending on the user's Emacs config).  Ement.el is designed to have minimal dependencies outside of Emacs (currently only one, ~plz~, which could be imported into the project), and every file is linted and compiles cleanly using [[https://github.com/alphapapa/makem.sh][makem.sh]].
- ~matrix-client~ uses EIEIO, probably unnecessarily, since few, if any, of the benefits of EIEIO are realized in it.  Ement.el uses structs instead.
- ~matrix-client~ uses bespoke code for inserting messages into buffers, which works pretty well, but has a few minor bugs which are difficult to track down.  Ement.el uses Emacs's built-in (and perhaps little-known) ~ewoc~ library, which makes it much simpler and more reliable to insert and update messages in buffers, and enables the development of advanced UI features more easily.
- ~matrix-client~ was, to a certain extent, designed to imitate other messaging apps.  The result is, at least when used with the ~matrix-client-frame~ command, fairly pleasing to use, but isn't especially "Emacsy."  Ement.el is intended to better fit into Emacs's paradigms.
- ~matrix-client~'s long name makes for long symbol names, which makes for tedious, verbose code.  ~ement~ is easy to type and makes for concise, readable code.
- The author has learned much since writing ~matrix-client~ and hopes to write simpler, more readable, more maintainable code in Ement.el.  It's hoped that this will enable others to contribute more easily.

Note that, while ~matrix-client~ remains usable, and probably will for some time to come, Ement.el has now surpassed it in every way.  The only reason to choose ~matrix-client~ instead is if one is using an older version of Emacs that isn't supported by Ement.el.

* License
:PROPERTIES:
:TOC:      :ignore (this)
:END:

GPLv3

* COMMENT Config                                                   :noexport:
:PROPERTIES:
:TOC:      :ignore (this descendants)
:END:

# NOTE: The #+OPTIONS: and other keywords did not take effect when in this section (perhaps due to file size or to changes in Org), so they were moved to the top of the file.

** File-local variables

# Local Variables:
# eval: (require 'org-make-toc)
# before-save-hook: org-make-toc
# org-export-with-properties: ()
# org-export-with-title: t
# End:


================================================
FILE: ement-api.el
================================================
;;; ement-api.el --- Matrix API library              -*- lexical-binding: t; -*-

;; Copyright (C) 2022-2023  Free Software Foundation, Inc.

;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>

;; 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:

;;

;;; Code:

;;;; Debugging

;; NOTE: Uncomment this form and `emacs-lisp-byte-compile-and-load' the file to enable
;; `ement-debug' messages.  This is commented out by default because, even though the
;; messages are only displayed when `warning-minimum-log-level' is `:debug' at runtime, if
;; that is so at expansion time, the expanded macro calls format the message and check the
;; log level at runtime, which is not zero-cost.

;; (eval-and-compile
;;   (setq-local warning-minimum-log-level nil)
;;   (setq-local warning-minimum-log-level :debug))

;;;; Requirements

(require 'json)
(require 'url-parse)
(require 'url-util)

(require 'plz)

(require 'ement-macros)
(require 'ement-structs)

;;;; Variables


;;;; Customization


;;;; Commands


;;;; Functions

(cl-defun ement-api (session endpoint
                             &key then data params queue
                             (content-type "application/json")
                             (data-type 'text)
                             (else #'ement-api-error) (method 'get)
                             ;; FIXME: What's the right term for the URL part after "/_matrix/"?
                             (endpoint-category "client")
                             (json-read-fn #'json-read)
                             ;; NOTE: Hard to say what the default timeouts
                             ;; should be.  Sometimes the matrix.org homeserver
                             ;; can get slow and respond a minute or two later.
                             (connect-timeout 10) (timeout 60)
                             (version "r0"))
  "Make API request on SESSION to ENDPOINT.
The request automatically uses SESSION's server, URI prefix, and
access token.

These keyword arguments are passed to `plz', which see: THEN,
DATA (passed as BODY), QUEUE (passed to `plz-queue', which see),
DATA-TYPE (passed as BODY-TYPE), ELSE, METHOD,
JSON-READ-FN (passed as AS), CONNECT-TIMEOUT, TIMEOUT.

Other arguments include PARAMS (used as the URL's query
parameters), ENDPOINT-CATEGORY (added to the endpoint URL), and
VERSION (added to the endpoint URL).

Note that most Matrix requests expect JSON-encoded data, so
usually the DATA argument should be passed through
`json-encode'."
  (declare (indent defun))
  (pcase-let* (((cl-struct ement-session server token) session)
               ((cl-struct ement-server uri-prefix) server)
               ((cl-struct url type host portspec) (url-generic-parse-url uri-prefix))
               (path (format "/_matrix/%s/%s/%s" endpoint-category version endpoint))
               (query (url-build-query-string params))
               (filename (concat path "?" query))
               (url (url-recreate-url
                     (url-parse-make-urlobj type nil nil host portspec filename nil data t)))
               (headers (ement-alist "Content-Type" content-type))
               (plz-args))
    (when token
      ;; Almost every request will require a token (only a few, like checking login flows, don't),
      ;; so we simplify the API by using the token automatically when the session has one.
      (push (cons "Authorization" (concat "Bearer " token)) headers))
    (setf plz-args (list method url :headers headers :body data :body-type data-type
                         :as json-read-fn :then then :else else
                         :connect-timeout connect-timeout :timeout timeout :noquery t))
    ;; Omit `then' from debugging because if it's a partially applied
    ;; function on the session object, which may be very large, it
    ;; will take a very long time to print into the warnings buffer.
    ;;  (ement-debug (current-time) method url headers)
    (if queue
        (plz-run
         (apply #'plz-queue queue plz-args))
      (apply #'plz plz-args))))

(define-error 'ement-api-error "Ement API error" 'error)

(defun ement-api-error (plz-error)
  "Signal an Ement API error for PLZ-ERROR."
  ;; This feels a little messy, but it seems to be reasonable.
  (pcase-let* (((cl-struct plz-error response
                           (message plz-message) (curl-error `(,curl-exit-code . ,curl-message)))
                plz-error)
               (status (when (plz-response-p response)
                         (plz-response-status response)))
               (body (when (plz-response-p response)
                       (plz-response-body response)))
               (json-object (when body
                              (ignore-errors
                                (json-read-from-string body))))
               (error-message (format "%S: %s"
                                      (or curl-exit-code status)
                                      (or (when json-object
                                            (alist-get 'error json-object))
                                          curl-message
                                          plz-message))))

    (signal 'ement-api-error (list error-message))))

;;;; Footer

(provide 'ement-api)

;;; ement-api.el ends here


================================================
FILE: ement-directory.el
================================================
;;; ement-directory.el --- Public room directory support                       -*- lexical-binding: t; -*-

;; Copyright (C) 2022-2023  Free Software Foundation, Inc.

;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>

;; 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:

;; This library provides support for viewing and searching public room directories on
;; Matrix homeservers.

;; To make rendering the list flexible and useful, we'll use `taxy-magit-section'.

;;; Code:

;;;; Requirements

(require 'ement)
(require 'ement-room-list)

(require 'taxy)
(require 'taxy-magit-section)

;;;; Variables

(defvar ement-directory-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "RET") #'ement-directory-RET)
    (define-key map [mouse-1] #'ement-directory-mouse-1)
    (define-key map (kbd "+") #'ement-directory-next)
    map))

(defgroup ement-directory nil
  "Options for room directories."
  :group 'ement)

;;;; Mode

(define-derived-mode ement-directory-mode magit-section-mode "Ement-Directory"
  :global nil)

(defvar-local ement-directory-etc nil
  "Alist storing information in `ement-directory' buffers.")

;;;;; Keys

(eval-and-compile
  (taxy-define-key-definer ement-directory-define-key
    ement-directory-keys "ement-directory-key" "FIXME: Docstring."))

;; TODO: Other keys like guest_can_join, world_readable, etc.  (Last-updated time would be
;; nice, but the server doesn't include that in the results.)

(ement-directory-define-key joined-p ()
  (pcase-let (((map ('room_id id)) item)
              ((map session) ement-directory-etc))
    (when (cl-find id (ement-session-rooms session)
                   :key #'ement-room-id :test #'equal)
      "Joined")))

(ement-directory-define-key size (&key < >)
  (pcase-let (((map ('num_joined_members size)) item))
    (cond ((and < (< size <))
           (format "< %s members" <))
          ((and > (> size >))
           (format "> %s members" >)))))

(ement-directory-define-key space-p ()
  "Groups rooms that are themselves spaces."
  (pcase-let (((map ('room_type type)) item))
    (when (equal "m.space" type)
      "Spaces")))

(ement-directory-define-key people-p ()
  (pcase-let (((map ('room_id id) ('room_type type)) item)
              ((map session) ement-directory-etc))
    (pcase type
      ("m.space" nil)
      (_ (when-let ((room (cl-find id (ement-session-rooms session)
                                   :key #'ement-room-id :test #'equal))
                    ((ement--room-direct-p room session)))
           (ement-propertize "People"
             'face 'ement-room-list-direct))))))

(defcustom ement-directory-default-keys
  '((joined-p
     (people-p)
     (and :name "Rooms"
          :keys ((not people-p))))
    (space-p)
    ((size :> 10000))
    ((size :> 1000))
    ((size :> 100))
    ((size :> 10))
    ((size :< 11)))
  "Default keys."
  :type 'sexp)

;;;; Columns

(defvar-local ement-directory-room-avatar-cache (make-hash-table)
  ;; Use a buffer-local variable so that the cache is cleared when the buffer is closed.
  "Hash table caching room avatars for the `ement-directory' room list.")

(eval-and-compile
  (taxy-magit-section-define-column-definer "ement-directory"))

;; TODO: Fetch avatars (with queueing and async updating/insertion?).

(ement-directory-define-column #("✓" 0 1 (help-echo "Joined")) ()
  (pcase-let (((map ('room_id id)) item)
              ((map session) ement-directory-etc))
    (if (cl-find id (ement-session-rooms session)
                 :key #'ement-room-id :test #'equal)
        "✓"
      " ")))

(ement-directory-define-column "Name" (:max-width 25)
  (pcase-let* (((map name ('room_id id) ('room_type type)
                     ('canonical_alias canonical-alias))
                item)
               ((map session) ement-directory-etc)
               (room)
               (face (pcase type
                       ("m.space" 'ement-room-list-space)
                       (_ (if (and (setf room (cl-find id (ement-session-rooms session)
                                                       :key #'ement-room-id :test #'equal))
                                   (ement--room-direct-p room session))
                              'ement-room-list-direct
                            'ement-room-list-name)))))
    ;; NOTE: We can't use `ement--room-display-name' because these aren't room structs,
    ;; and we don't have membership data.
    (ement-propertize (or name canonical-alias "[unnamed]")
      'face face)))

(ement-directory-define-column "Alias" (:max-width 25)
  (pcase-let (((map ('canonical_alias alias)) item))
    (or alias "")))

(ement-directory-define-column "Size" (:align 'right)
  (pcase-let (((map ('num_joined_members size)) item))
    (number-to-string size)))

(ement-directory-define-column "Topic" (:max-width 50)
  (pcase-let (((map topic) item))
    (if topic
        (replace-regexp-in-string "\n" " | " topic nil t)
      "")))

(ement-directory-define-column "ID" ()
  (pcase-let (((map ('room_id id)) item))
    id))

(unless ement-directory-columns
  ;; TODO: Automate this or document it
  (setq-default ement-directory-columns
                '("Name" "Alias" "Size" "Topic" "ID")))

;;;; Commands

;; TODO: Pagination of results.

;;;###autoload
(cl-defun ement-directory (&key server session since (limit 100))
  "View the public room directory on SERVER with SESSION.
Show up to LIMIT rooms.  Interactively, with prefix, prompt for
server and LIMIT.

SINCE may be a next-batch token."
  (interactive (let* ((session (ement-complete-session :prompt "Search on session: "))
                      (server (if current-prefix-arg
                                  (read-string "Search on server: " nil nil
                                               (ement-server-name (ement-session-server session)))
                                (ement-server-name (ement-session-server session))))
                      (args (list :server server :session session)))
                 (when current-prefix-arg
                   (cl-callf plist-put args
                     :limit (read-number "Limit number of rooms: " 100)))
                 args))
  (pcase-let ((revert-function (lambda (&rest _ignore)
                                 (interactive)
                                 (ement-directory :server server :session session :limit limit)))
              (endpoint "publicRooms")
              (params (list (list "limit" limit))))
    (when since
      (cl-callf append params (list (list "since" since))))
    (ement-api session endpoint :params params
      :then (lambda (results)
              (pcase-let (((map ('chunk rooms) ('next_batch next-batch)
                                ('total_room_count_estimate remaining))
                           results))
                (ement-directory--view rooms :append-p since
                  :buffer-name (format "*Ement Directory: %s*" server)
                  :root-section-name (format "Ement Directory: %s" server)
                  :init-fn (lambda ()
                             (setf (alist-get 'server ement-directory-etc) server
                                   (alist-get 'session ement-directory-etc) session
                                   (alist-get 'next-batch ement-directory-etc) next-batch
                                   (alist-get 'limit ement-directory-etc) limit)
                             (setq-local revert-buffer-function revert-function)
                             (when remaining
                               ;; FIXME: The server seems to report all of the rooms on
                               ;; the server as remaining even when searching for a
                               ;; specific term like "emacs".
                               ;; TODO: Display this in a more permanent place (like a
                               ;; header or footer).
                               (message
                                (substitute-command-keys
                                 "%s rooms remaining (use \\[ement-directory-next] to fetch more)")
                                remaining)))))))
    (ement-message "Listing %s rooms on %s..." limit server)))

;;;###autoload
(cl-defun ement-directory-search (query &key server session since (limit 1000))
  "View public rooms on SERVER matching QUERY.
QUERY is a string used to filter results."
  (interactive (let* ((session (ement-complete-session :prompt "Search on session: "))
                      (server (if current-prefix-arg
                                  (read-string "Search on server: " nil nil
                                               (ement-server-name (ement-session-server session)))
                                (ement-server-name (ement-session-server session))))
                      (query (read-string (format "Search for rooms on %s matching: " server)))
                      (args (list query :server server :session session)))
                 (when current-prefix-arg
                   (cl-callf plist-put (cdr args)
                     :limit (read-number "Limit number of rooms: " 1000)))
                 args))
  ;; TODO: Handle "include_all_networks" and "third_party_instance_id".  See § 10.5.4.
  (pcase-let* ((revert-function (lambda (&rest _ignore)
                                  (interactive)
                                  (ement-directory-search query :server server :session session)))
               (endpoint "publicRooms")
               (data (rassq-delete-all nil
                                       (ement-alist "limit" limit
                                                    "filter" (ement-alist "generic_search_term" query)
                                                    "since" since))))
    (ement-api session endpoint :method 'post :data (json-encode data)
      :then (lambda (results)
              (pcase-let (((map ('chunk rooms) ('next_batch next-batch)
                                ('total_room_count_estimate remaining))
                           results))
                (ement-directory--view rooms :append-p since
                  :buffer-name (format "*Ement Directory: \"%s\" on %s*" query server)
                  :root-section-name (format "Ement Directory: \"%s\" on %s" query server)
                  :init-fn (lambda ()
                             (setf (alist-get 'server ement-directory-etc) server
                                   (alist-get 'session ement-directory-etc) session
                                   (alist-get 'next-batch ement-directory-etc) next-batch
                                   (alist-get 'limit ement-directory-etc) limit
                                   (alist-get 'query ement-directory-etc) query)
                             (setq-local revert-buffer-function revert-function)
                             (when remaining
                               (message
                                (substitute-command-keys
                                 "%s rooms remaining (use \\[ement-directory-next] to fetch more)")
                                remaining)))))))
    (ement-message "Searching for %S on %s..." query server)))

(defun ement-directory-next ()
  "Fetch next batch of results in `ement-directory' buffer."
  (interactive)
  (pcase-let (((map next-batch query limit server session) ement-directory-etc))
    (unless next-batch
      (user-error "No more results"))
    (if query
        (ement-directory-search query :server server :session session :limit limit :since next-batch)
      (ement-directory :server server :session session :limit limit :since next-batch))))

(defun ement-directory-mouse-1 (event)
  "Call `ement-directory-RET' at EVENT."
  (interactive "e")
  (mouse-set-point event)
  (call-interactively #'ement-directory-RET))

(defun ement-directory-RET ()
  "View or join room at point, or cycle section at point."
  (interactive)
  (cl-etypecase (oref (magit-current-section) value)
    (null nil)
    (list (pcase-let* (((map ('name name) ('room_id room-id)) (oref (magit-current-section) value))
                       ((map session) ement-directory-etc)
                       (room (cl-find room-id (ement-session-rooms session)
                                      :key #'ement-room-id :test #'equal)))
            (if room
                (ement-view-room room session)
              ;; Room not joined: prompt to join.  (Don't use the alias in the prompt,
              ;; because multiple rooms might have the same alias, e.g. when one is
              ;; upgraded or tombstoned.)
              (when (yes-or-no-p (format "Join room \"%s\" <%s>? " name room-id))
                (ement-join-room room-id session)))))
    (taxy-magit-section (call-interactively #'magit-section-cycle))))

;;;; Functions

(cl-defun ement-directory--view (rooms &key init-fn append-p
                                       (buffer-name "*Ement Directory*")
                                       (root-section-name "Ement Directory")
                                       (keys ement-directory-default-keys)
                                       (display-buffer-action '(display-buffer-same-window)))
  "View ROOMS in an `ement-directory-mode' buffer.
ROOMS should be a list of rooms from an API request.  Calls
INIT-FN immediately after activating major mode.  Sets
BUFFER-NAME and ROOT-SECTION-NAME, and uses
DISPLAY-BUFFER-ACTION.  KEYS are a list of `taxy' keys.  If
APPEND-P, add ROOMS to buffer rather than replacing existing
contents.  To be called by `ement-directory-search'."
  (declare (indent defun))
  (let (column-sizes window-start)
    (cl-labels ((format-item (item)
                  ;; NOTE: We use the buffer-local variable `ement-directory-etc' rather
                  ;; than a closure variable because the taxy-magit-section struct's format
                  ;; table is not stored in it, and we can't reuse closures' variables.
                  ;; (It would be good to store the format table in the taxy-magit-section
                  ;; in the future, to make this cleaner.)
                  (gethash item (alist-get 'format-table ement-directory-etc)))
                ;; NOTE: Since these functions take an "item" (which is a [room session]
                ;; vector), they're prefixed "item-" rather than "room-".
                (size (item)
                  (pcase-let (((map ('num_joined_members size)) item))
                    size))
                (t<nil (a b) (and a (not b)))
                (t>nil (a b) (and (not a) b))
                (make-fn (&rest args)
                  (apply #'make-taxy-magit-section
                         :make #'make-fn
                         :format-fn #'format-item
                         ;; FIXME: Should we reuse `ement-room-list-level-indent' here?
                         :level-indent ement-room-list-level-indent
                         ;; :visibility-fn #'visible-p
                         ;; :heading-indent 2
                         :item-indent 2
                         ;; :heading-face-fn #'heading-face
                         args)))
      (with-current-buffer (get-buffer-create buffer-name)
        (unless (eq 'ement-directory-mode major-mode)
          ;; Don't obliterate buffer-local variables.
          (ement-directory-mode))
        (when init-fn
          (funcall init-fn))
        (pcase-let* ((taxy (if append-p
                               (alist-get 'taxy ement-directory-etc)
                             (make-fn
                              :name root-section-name
                              :take (taxy-make-take-function keys ement-directory-keys))))
                     (taxy-magit-section-insert-indent-items nil)
                     (inhibit-read-only t)
                     (pos (point))
                     (section-ident (when (magit-current-section)
                                      (magit-section-ident (magit-current-section))))
                     (format-cons))
          (setf taxy (thread-last taxy
                                  (taxy-fill (cl-coerce rooms 'list))
                                  (taxy-sort #'> #'size)
                                  (taxy-sort* #'string> #'taxy-name))
                (alist-get 'taxy ement-directory-etc) taxy
                format-cons (taxy-magit-section-format-items
                             ement-directory-columns ement-directory-column-formatters taxy)
                (alist-get 'format-table ement-directory-etc) (car format-cons)
                column-sizes (cdr format-cons)
                header-line-format (taxy-magit-section-format-header
                                    column-sizes ement-directory-column-formatters)
                window-start (if (get-buffer-window buffer-name)
                                 (window-start (get-buffer-window buffer-name))
                               0))
          (delete-all-overlays)
          (erase-buffer)
          (save-excursion
            (taxy-magit-section-insert taxy :items 'first
              ;; :blank-between-depth bufler-taxy-blank-between-depth
              :initial-depth 0))
          (goto-char pos)
          (when (and section-ident (magit-get-section section-ident))
            (goto-char (oref (magit-get-section section-ident) start)))))
      (display-buffer buffer-name display-buffer-action)
      (when (get-buffer-window buffer-name)
        (set-window-start (get-buffer-window buffer-name) window-start))
      ;; NOTE: In order for `bookmark--jump-via' to work properly, the restored buffer
      ;; must be set as the current buffer, so we have to do this explicitly here.
      (set-buffer buffer-name))))

;;;; Spaces

;; Viewing spaces and the rooms in them.

;;;###autoload
(defun ement-view-space (space session)
  ;; TODO: Use this for spaces instead of `ement-view-room' (or something like that).
  ;; TODO: Display space's topic in the header or something.
  "View child rooms in SPACE on SESSION.
SPACE may be a room ID or an `ement-room' struct."
  ;; TODO: "from" query parameter.
  (interactive (ement-complete-room :predicate #'ement--space-p
                 :prompt "Space: "))
  (pcase-let* ((id (cl-typecase space
                     (string space)
                     (ement-room (ement-room-id space))))
               (endpoint (format "rooms/%s/hierarchy" id))
               (revert-function (lambda (&rest _ignore)
                                  (interactive)
                                  (ement-view-space space session))))
    (ement-api session endpoint :version "v1"
      :then (lambda (results)
              (pcase-let (((map rooms ('next_batch next-batch))
                           results))
                (ement-directory--view rooms ;; :append-p since
                  ;; TODO: Use space's alias where possible.
                  :buffer-name (format "*Ement Directory: space %s" (ement--format-room space session))
                  :root-section-name (format "*Ement Directory: rooms in %s %s"
                                             (ement-propertize "space"
                                               'face 'font-lock-type-face)
                                             (ement--format-room space session))
                  :init-fn (lambda ()
                             (setf (alist-get 'session ement-directory-etc) session
                                   (alist-get 'next-batch ement-directory-etc) next-batch
                                   ;; (alist-get 'limit ement-directory-etc) limit
                                   (alist-get 'space ement-directory-etc) space)
                             (setq-local revert-buffer-function revert-function)
                             ;; TODO: Handle next batches.
                             ;; (when remaining
                             ;;   (message
                             ;;    (substitute-command-keys
                             ;;     "%s rooms remaining (use \\[ement-directory-next] to fetch more)")
                             ;;    remaining))
                             )))))))

;;;; Footer

(provide 'ement-directory)
;;; ement-directory.el ends here


================================================
FILE: ement-lib.el
================================================
;;; ement-lib.el --- Library of Ement functions      -*- lexical-binding: t; -*-

;; Copyright (C) 2022-2023  Free Software Foundation, Inc.

;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>

;; 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:

;; This library provides functions used in other Ement libraries.  It exists so they may
;; be required where needed, without causing circular dependencies.

;;; Code:

;;;; Requirements

(eval-when-compile
  (require 'eieio)
  (require 'ewoc)
  (require 'pcase)
  (require 'subr-x)
  
  (require 'taxy-magit-section)

  (require 'ement-macros))

(require 'cl-lib)

(require 'button)
(require 'color)
(require 'map)
(require 'seq)
(require 'xml)

(require 'ement-api)
(require 'ement-structs)

;;;; Variables

(defvar ement-sessions)
(defvar ement-users)
(defvar ement-ewoc)
(defvar ement-room)
(defvar ement-session)

(defvar ement-room-buffer-name-prefix)
(defvar ement-room-buffer-name-suffix)
(defvar ement-room-leave-kill-buffer)
(defvar ement-room-prism)
(defvar ement-room-prism-color-adjustment)
(defvar ement-room-prism-minimum-contrast)
(defvar ement-room-unread-only-counts-notifications)

;;;; Function declarations

;; Instead of using top-level `declare-function' forms (which can easily become obsolete
;; if not kept with the code that needs them), this allows the use of `(declare (function
;; ...))' forms in each function definition, so that if a function is moved or removed,
;; the `declare-function' goes with it.

;; TODO: Propose this upstream.

(eval-and-compile
  (defun ement--byte-run--declare-function (_name _args &rest values)
    "Return a `declare-function' form with VALUES.
Allows the use of a form like:

  (declare (function FN FILE ...))

inside of a function definition, effectively keeping its
`declare-function' form inside the function definition, ensuring
that stray such forms don't remain if the function is removed."
    `(declare-function ,@values))

  (cl-pushnew '(function ement--byte-run--declare-function) defun-declarations-alist :test #'equal)
  (cl-pushnew '(function ement--byte-run--declare-function) macro-declarations-alist :test #'equal))

;;;; Compatibility

;; These workarounds should be removed when they aren't needed.

(defalias 'ement--json-parse-buffer
  ;; For non-libjansson builds (those that do have libjansson will see a 4-5x improvement
  ;; in the time needed to parse JSON responses).

  ;; TODO: Suggest mentioning in manual and docstrings that `json-read', et al do not use
  ;; libjansson, while `json-parse-buffer', et al do.
  (if (fboundp 'json-parse-buffer)
      (lambda ()
        (condition-case err
            (json-parse-buffer :object-type 'alist :null-object nil :false-object :json-false)
          (json-parse-error
           (ement-message "`json-parse-buffer' signaled `json-parse-error'; falling back to `json-read'... (%S)"
                          (error-message-string err))
           (goto-char (point-min))
           (json-read))))
    'json-read))

;;;;; Emacs 28 color features.

;; Copied from Emacs 28.  See <https://github.com/alphapapa/ement.el/issues/99>.

;; TODO(future): Remove these workarounds when dropping support for Emacs <28.

(eval-and-compile
  (unless (boundp 'color-luminance-dark-limit)
    (defconst ement--color-luminance-dark-limit 0.325
      "The relative luminance below which a color is considered \"dark.\"
A \"dark\" color in this sense provides better contrast with
white than with black; see `color-dark-p'.  This value was
determined experimentally.")))

(defalias 'ement--color-dark-p
  (if (fboundp 'color-dark-p)
      'color-dark-p
    (with-suppressed-warnings ((free-vars ement--color-luminance-dark-limit))
      (lambda (rgb)
        "Whether RGB is more readable against white than black.
RGB is a 3-element list (R G B), each component in the range [0,1].
This predicate can be used both for determining a suitable (black or white)
contrast colour with RGB as background and as foreground."
        (unless (<= 0 (apply #'min rgb) (apply #'max rgb) 1)
          (error "RGB components %S not in [0,1]" rgb))
        ;; Compute the relative luminance after gamma-correcting (assuming sRGB),
        ;; and compare to a cut-off value determined experimentally.
        ;; See https://en.wikipedia.org/wiki/Relative_luminance for details.
        (let* ((sr (nth 0 rgb))
               (sg (nth 1 rgb))
               (sb (nth 2 rgb))
               ;; Gamma-correct the RGB components to linear values.
               ;; Use the power 2.2 as an approximation to sRGB gamma;
               ;; it should be good enough for the purpose of this function.
               (r (expt sr 2.2))
               (g (expt sg 2.2))
               (b (expt sb 2.2))
               (y (+ (* r 0.2126) (* g 0.7152) (* b 0.0722))))
          (< y ement--color-luminance-dark-limit))))))

;;;; Functions

;;;;; Commands

(cl-defun ement-create-room
    (session &key name alias topic invite direct-p creation-content
             (then (lambda (data)
                     (message "Created new room: %s" (alist-get 'room_id data))))
             (visibility 'private))
  "Create new room on SESSION.
Then call function THEN with response data.  Optional string
arguments are NAME, ALIAS, and TOPIC.  INVITE may be a list of
user IDs to invite.  If DIRECT-P, set the \"is_direct\" flag in
the request.  CREATION-CONTENT may be an alist of extra keys to
include with the request (see Matrix spec)."
  ;; TODO: Document other arguments.
  ;; SPEC: 10.1.1.
  (declare (indent defun))
  (interactive (list (ement-complete-session)
		     :name (read-string "New room name: ")
		     :alias (read-string "New room alias (e.g. \"foo\" for \"#foo:matrix.org\"): ")
		     :topic (read-string "New room topic: ")
		     :visibility (completing-read "New room visibility: " '(private public))))
  (cl-labels ((given-p (var) (and var (not (string-empty-p var)))))
    (pcase-let* ((endpoint "createRoom")
		 (data (ement-aprog1
			   (ement-alist "visibility" visibility)
			 (when (given-p alias)
			   (push (cons "room_alias_name" alias) it))
			 (when (given-p name)
			   (push (cons "name" name) it))
			 (when (given-p topic)
			   (push (cons "topic" topic) it))
			 (when invite
			   (push (cons "invite" invite) it))
			 (when direct-p
			   (push (cons "is_direct" t) it))
                         (when creation-content
                           (push (cons "creation_content" creation-content) it)))))
      (ement-api session endpoint :method 'post :data (json-encode data)
        :then then))))

(cl-defun ement-create-space
    (session &key name alias topic
             (then (lambda (data)
                     (message "Created new space: %s" (alist-get 'room_id data))))
             (visibility 'private))
  "Create new space on SESSION.
Then call function THEN with response data.  Optional string
arguments are NAME, ALIAS, and TOPIC."
  (declare (indent defun))
  (interactive (list (ement-complete-session)
		     :name (read-string "New space name: ")
		     :alias (read-string "New space alias (e.g. \"foo\" for \"#foo:matrix.org\"): ")
		     :topic (read-string "New space topic: ")
		     :visibility (completing-read "New space visibility: " '(private public))))
  (ement-create-room session :name name :alias alias :topic topic :visibility visibility
    :creation-content (ement-alist "type" "m.space") :then then))

(defun ement-room-leave (room session &optional force-p)
  "Leave ROOM on SESSION.
If FORCE-P, leave without prompting.  ROOM may be an `ement-room'
struct, or a room ID or alias string."
  ;; TODO: Rename `room' argument to `room-or-id'.
  (interactive
   (ement-with-room-and-session
     :prompt-form (ement-complete-room :prompt "Leave room: ")
     (list ement-room ement-session)))
  (cl-etypecase room
    (ement-room)
    (string (setf room (ement-afirst (or (equal room (ement-room-canonical-alias it))
                                         (equal room (ement-room-id it)))
                         (ement-session-rooms session)))))
  (when (or force-p (yes-or-no-p (format "Leave room %s? " (ement--format-room room))))
    (pcase-let* (((cl-struct ement-room id) room)
                 (endpoint (format "rooms/%s/leave" (url-hexify-string id))))
      (ement-api session endpoint :method 'post :data ""
        :then (lambda (_data)
                (when ement-room-leave-kill-buffer
                  ;; NOTE: This generates a symbol and sets its function value to a lambda
                  ;; which removes the symbol from the hook, removing itself from the hook.
                  ;; TODO: When requiring Emacs 27, use `letrec'.
                  (let* ((leave-fn-symbol (gensym (format "ement-leave-%s" room)))
                         (leave-fn (lambda (_session)
                                     (remove-hook 'ement-sync-callback-hook leave-fn-symbol)
                                     ;; FIXME: Probably need to unintern the symbol.
                                     (when-let ((buffer (map-elt (ement-room-local room) 'buffer)))
                                       (when (buffer-live-p buffer)
                                         (kill-buffer buffer))))))
                    (setf (symbol-function leave-fn-symbol) leave-fn)
                    (add-hook 'ement-sync-callback-hook leave-fn-symbol)))
                (ement-message "Left room: %s" (ement--format-room room)))
        :else (lambda (plz-error)
                (pcase-let* (((cl-struct plz-error response) plz-error)
                             ((cl-struct plz-response status body) response)
                             ((map error) (json-read-from-string body)))
                  (pcase status
                    (429 (error "Unable to leave room %s: %s" room error))
                    (_ (error "Unable to leave room %s: %s %S" room status plz-error)))))))))
(defalias 'ement-leave-room #'ement-room-leave)

(defun ement-forget-room (room session &optional force-p)
  "Forget ROOM on SESSION.
If FORCE-P (interactively, with prefix), prompt to leave the room
when necessary, and forget the room without prompting."
  (interactive
   (ement-with-room-and-session
     :prompt-form (ement-complete-room :prompt "Forget room: ")
     (list ement-room ement-session current-prefix-arg)))
  (pcase-let* (((cl-struct ement-room id display-name status) room)
               (endpoint (format "rooms/%s/forget" (url-hexify-string id))))
    (pcase status
      ('join (if (and force-p
                      (yes-or-no-p (format "Leave and forget room %s? (WARNING: You will not be able to rejoin the room to access its content.) "
                                           (ement--format-room room))))
                 (progn
                   ;; TODO: Use `letrec'.
                   (let* ((forget-fn-symbol (gensym (format "ement-forget-%s" room)))
                          (forget-fn (lambda (_session)
                                       (when (equal 'leave (ement-room-status room))
                                         (remove-hook 'ement-sync-callback-hook forget-fn-symbol)
                                         ;; FIXME: Probably need to unintern the symbol.
                                         (ement-forget-room room session 'force)))))
                     (setf (symbol-function forget-fn-symbol) forget-fn)
                     (add-hook 'ement-sync-callback-hook forget-fn-symbol))
                   (ement-leave-room room session 'force))
               (user-error "Room %s is joined (must be left before forgetting)"
                           (ement--format-room room))))
      ('leave (when (or force-p (yes-or-no-p (format "Forget room \"%s\" (%s)? " display-name id)))
                (ement-api session endpoint :method 'post :data ""
                  :then (lambda (_data)
                          ;; NOTE: The spec does not seem to indicate that the action of forgetting
                          ;; a room is synced to other clients, so it seems that we need to remove
                          ;; the room from the session here.
                          (setf (ement-session-rooms session)
                                (cl-remove room (ement-session-rooms session)))
                          ;; TODO: Indicate forgotten in footer in room buffer.
                          (ement-message "Forgot room: %s." (ement--format-room room)))))))))

(defun ement-ignore-user (user-id session &optional unignore-p)
  "Ignore USER-ID on SESSION.
If UNIGNORE-P (interactively, with prefix), un-ignore USER."
  (interactive (list (ement-complete-user-id)
                     (ement-complete-session)
                     current-prefix-arg))
  (pcase-let* (((cl-struct ement-session account-data) session)
               ;; TODO: Store session account-data events in an alist keyed on type.
               ((map ('content (map ('ignored_users ignored-users))))
                (cl-find "m.ignored_user_list" account-data
                         :key (lambda (event) (alist-get 'type event)) :test #'equal)))
    (if unignore-p
        ;; Being map keys, the user IDs have been interned by `json-read'.
        (setf ignored-users (map-delete ignored-users (intern user-id)))
      ;; Empty maps are used to list ignored users.
      (setf (map-elt ignored-users user-id) nil))
    (ement-put-account-data session "m.ignored_user_list" (ement-alist "ignored_users" ignored-users)
      :then (lambda (data)
              (ement-debug "PUT successful" data)
              (message "Ement: User %s %s." user-id (if unignore-p "unignored" "ignored"))))))

(defun ement-invite-user (user-id room session)
  "Invite USER-ID to ROOM on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room."
  ;; SPEC: 10.4.2.1.
  (interactive
   (ement-with-room-and-session
     (list (ement-complete-user-id) ement-room ement-session)))
  (pcase-let* ((endpoint (format "rooms/%s/invite"
                                 (url-hexify-string (ement-room-id room))))
               (data (ement-alist "user_id" user-id) ))
    (ement-api session endpoint :method 'post :data (json-encode data)
      ;; TODO: Handle error codes.
      :then (lambda (_data)
              (message "User %s invited to room \"%s\" (%s)" user-id
                       (ement-room-display-name room)
                       (ement-room-id room))))))

(defun ement-list-members (room session bufferp)
  "Show members of ROOM on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room.  If BUFFERP (interactively, with
prefix), or if there are many members, show in a new buffer;
otherwise show in echo area."
  (interactive
   (ement-with-room-and-session
     (list ement-room ement-session current-prefix-arg)))
  (pcase-let* (((cl-struct ement-room members (local (map fetched-members-p))) room)
               (list-members
                (lambda (&optional _)
                  (cond ((or bufferp (> (hash-table-count members) 51))
                         ;; Show in buffer.
                         (let* ((buffer (get-buffer-create (format "*Ement members: %s*" (ement-room-display-name room))))
                                (members (cl-sort (cl-loop for user being the hash-values of members
                                                           for id = (ement-user-id user)
                                                           for displayname = (ement--user-displayname-in room user)
                                                           collect (cons displayname id))
                                                  (lambda (a b) (string-collate-lessp a b nil t)) :key #'car))
                                (displayname-width (cl-loop for member in members
                                                            maximizing (string-width (car member))))
                                (format-string (format "%%-%ss <%%s>" displayname-width)))
                           (with-current-buffer buffer
                             (erase-buffer)
                             (save-excursion
                               (dolist (member members)
                                 (insert (format format-string (car member) (cdr member)) "\n"))))
                           (pop-to-buffer buffer)))
                        (t
                         ;; Show in echo area.
                         (message "Members of %s (%s): %s" (ement--room-display-name room)
                                  (hash-table-count members)
                                  (string-join (map-apply (lambda (_id user)
                                                            (ement--user-displayname-in room user))
                                                          members)
                                               ", ")))))))
    (if fetched-members-p
        (funcall list-members)
      (ement--get-joined-members room session
        :then list-members))
    (message "Listing members of %s..." (ement--format-room room))))

(defun ement-send-direct-message (session user-id message)
  "Send a direct MESSAGE to USER-ID on SESSION.
Uses the latest existing direct room with the user, or creates a
new one automatically if necessary."
  ;; SPEC: 13.23.2.
  (interactive
   (let* ((session (ement-complete-session))
	  (user-id (ement-complete-user-id))
	  (message (read-string "Message: ")))
     (list session user-id message)))
  (if-let* ((seen-user (gethash user-id ement-users))
	    (existing-direct-room (ement--direct-room-for-user seen-user session)))
      (progn
        (ement-send-message existing-direct-room session :body message)
        (message "Message sent to %s <%s> in room %S <%s>."
                 (ement--user-displayname-in existing-direct-room seen-user)
                 user-id
                 (ement-room-display-name existing-direct-room) (ement-room-id existing-direct-room)))
    ;; No existing room for user: make new one.
    (message "Creating new room for user %s..." user-id)
    (ement-create-room session :direct-p t :invite (list user-id)
      :then (lambda (data)
              (let* ((room-id (alist-get 'room_id data))
	             (room (or (cl-find room-id (ement-session-rooms session)
                                        :key #'ement-room-id)
		               ;; New room hasn't synced yet: make a temporary struct.
		               (make-ement-room :id room-id)))
                     (direct-rooms-account-data-event-content
                      ;; FIXME: Make account-data a map.
                      (alist-get 'content (cl-find-if (lambda (event)
                                                        (equal "m.direct" (alist-get 'type event)))
                                                      (ement-session-account-data session)))))
                ;; Mark new room as direct: add the room to the account-data event, then
                ;; put the new account data to the server.  (See also:
                ;; <https://github.com/matrix-org/matrix-react-sdk/blob/919aab053e5b3bdb5a150fd90855ad406c19e4ab/src/Rooms.ts#L91>).
                (setf (map-elt direct-rooms-account-data-event-content user-id) (vector room-id))
                (ement-put-account-data session "m.direct" direct-rooms-account-data-event-content)
                ;; Send message to new room.
                (ement-send-message room session :body message)
                (message "Room \"%s\" created for user %s.  Sending message..."
	                 room-id user-id))))))

(defun ement-tag-room (tag room session)
  "Toggle TAG for ROOM on SESSION."
  (interactive
   (ement-with-room-and-session
     (let* ((prompt (format "Toggle tag (%s): " (ement--format-room ement-room)))
            (default-tags
             (ement-alist (propertize "Favourite"
                                      'face (when (ement--room-tagged-p "m.favourite" ement-room)
                                              'transient-value))
                          "m.favourite"
                          (propertize "Low-priority"
                                      'face (when (ement--room-tagged-p "m.lowpriority" ement-room)
                                              'transient-value))
                          "m.lowpriority"))
            (input (completing-read prompt default-tags))
            (tag (alist-get input default-tags (concat "u." input) nil #'string=)))
       (list tag ement-room ement-session))))
  (pcase-let* (((cl-struct ement-session user) session)
               ((cl-struct ement-user (id user-id)) user)
               ((cl-struct ement-room (id room-id)) room)
               (endpoint (format "user/%s/rooms/%s/tags/%s"
                                 (url-hexify-string user-id) (url-hexify-string room-id) (url-hexify-string tag)))
               (method (if (ement--room-tagged-p tag room) 'delete 'put)))
    ;; TODO: "order".
    ;; FIXME: Removing a tag on a left room doesn't seem to work (e.g. to unfavorite a room after leaving it, but not forgetting it).
    (ement-api session endpoint :version "v3" :method method :data (pcase method ('put "{}"))
      :then (lambda (_)
              (ement-message "%s tag %S on %s"
                             (pcase method
                               ('delete "Removed")
                               ('put "Added"))
                             tag (ement--format-room room)) ))))

(defun ement-set-display-name (display-name session)
  "Set DISPLAY-NAME for user on SESSION.
Sets global displayname."
  (interactive
   (let* ((session (ement-complete-session))
          (display-name (read-string "Set display-name to: " nil nil
                                     (ement-user-displayname (ement-session-user session)))))
     (list display-name session)))
  (pcase-let* (((cl-struct ement-session user) session)
               ((cl-struct ement-user (id user-id)) user)
               (endpoint (format "profile/%s/displayname" (url-hexify-string user-id))))
    (ement-api session endpoint :method 'put :version "v3"
      :data (json-encode (ement-alist "displayname" display-name))
      :then (lambda (_data)
              (message "Ement: Display name set to %S for <%s>" display-name
                       (ement-user-id (ement-session-user session)))))))

(defun ement-room-set-display-name (display-name room session)
  "Set DISPLAY-NAME for user in ROOM on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room.  Sets the name only in ROOM, not
globally."
  (interactive
   (ement-with-room-and-session
     (let* ((prompt (format "Set display-name in %S to: "
                            (ement--format-room ement-room)))
            (display-name (read-string prompt nil nil
                                       (ement-user-displayname (ement-session-user ement-session)))))
       (list display-name ement-room ement-session))))
  ;; NOTE: This does not seem to be documented in the spec, so we imitate the
  ;; "/myroomnick" command in SlashCommands.tsx from matrix-react-sdk.
  (pcase-let* (((cl-struct ement-room state) room)
               ((cl-struct ement-session user) session)
               ((cl-struct ement-user id) user)
               (member-event (cl-find-if (lambda (event)
                                           (and (equal id (ement-event-state-key event))
                                                (equal "m.room.member" (ement-event-type event))
                                                (equal "join" (alist-get 'membership (ement-event-content event)))))
                                         state)))
    (cl-assert member-event)
    (setf (alist-get 'displayname (ement-event-content member-event)) display-name)
    (ement-put-state room "m.room.member" id (ement-event-content member-event) session
      :then (lambda (_data)
              (message "Ement: Display name set to %S for <%s> in %S" display-name
                       (ement-user-id (ement-session-user session))
                       (ement--format-room room))))))

;;;;;; Describe room

(defvar ement-describe-room-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "q") #'quit-window)
    map)
  "Keymap for `ement-describe-room-mode' buffers.")

(define-derived-mode ement-describe-room-mode read-only-mode
  "Ement-Describe-Room" "Major mode for `ement-describe-room' buffers.")

(defun ement-describe-room (room session)
  "Describe ROOM on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room."
  (interactive (ement-with-room-and-session (list ement-room ement-session)))
  (cl-labels ((heading (string)
                (propertize (or string "") 'face 'font-lock-builtin-face))
              (id (string)
                (propertize (or string "") 'face 'font-lock-constant-face))
              (member<
                (a b) (string-collate-lessp (car a) (car b) nil t)))
    (pcase-let* (((cl-struct ement-room (id room-id) avatar display-name canonical-alias members timeline status topic
                             (local (map fetched-members-p)))
                  room)
                 ((cl-struct ement-session user) session)
                 ((cl-struct ement-user (id user-id)) user)
                 (inhibit-read-only t))
      (if (not fetched-members-p)
          ;; Members not fetched: fetch them and re-call this command.
          (ement--get-joined-members room session
            :then (lambda (_) (ement-room-describe room session)))
        (with-current-buffer (get-buffer-create (format "*Ement room description: %s*" (or display-name canonical-alias room-id)))
          (let ((inhibit-read-only t))
            (erase-buffer)
            ;; We avoid looping twice by doing a bit more work here and
            ;; returning a cons which we destructure.
            (pcase-let* ((`(,member-pairs . ,name-width)
                          (cl-loop for user being the hash-values of members
                                   for formatted = (ement--format-user user room session)
                                   for id = (format "<%s>" (id (ement-user-id user)))
                                   collect (cons formatted id)
                                   into pairs
                                   maximizing (string-width id) into width
                                   finally return (cons (cl-sort pairs #'member<) width)))
                         ;; We put the MXID first, because users may use Unicode characters
                         ;; in their displayname, which `string-width' does not always
                         ;; return perfect results for, and putting it last prevents
                         ;; alignment problems.
                         (spec (format "%%-%ss %%s" name-width)))
              (save-excursion
                (insert "\"" (propertize (or display-name canonical-alias room-id) 'face 'font-lock-doc-face) "\"" " is a "
                        (propertize (if (ement--space-p room)
                                        "space"
                                      "room")
                                    'face 'font-lock-type-face)
                        " "
                        (propertize (pcase status
                                      ('invite "invited")
                                      ('join "joined")
                                      ('leave "left")
                                      (_ (symbol-name status)))
                                    'face 'font-lock-comment-face)
                        " on session <" (id user-id) ">.\n\n"
                        (heading "Avatar: ") (or avatar "") "\n\n"
                        (heading "ID: ") "<" (id room-id) ">" "\n"
                        (heading "Alias: ") "<" (id canonical-alias) ">" "\n\n"
                        (heading "Topic: ") (propertize (or topic "[none]") 'face 'font-lock-comment-face) "\n\n"
                        (heading "Retrieved events: ") (number-to-string (length timeline)) "\n"
                        (heading "  spanning: ")
                        (format-time-string "%Y-%m-%d %H:%M:%S"
                                            (/ (ement-event-origin-server-ts
                                                (car (cl-sort (copy-sequence timeline) #'< :key #'ement-event-origin-server-ts)))
                                               1000))
                        (heading " to ")
                        (format-time-string "%Y-%m-%d %H:%M:%S\n\n"
                                            (/ (ement-event-origin-server-ts
                                                (car (cl-sort (copy-sequence timeline) #'> :key #'ement-event-origin-server-ts)))
                                               1000))
                        (heading "Members") " (" (number-to-string (hash-table-count members)) "):\n")
                (pcase-dolist (`(,formatted . ,id) member-pairs)
                  (insert "  " (format spec id formatted) "\n")))))
          (unless (eq major-mode 'ement-describe-room-mode)
            ;; Without this check, activating the mode again causes a "Cyclic keymap
            ;; inheritance" error.
            (ement-describe-room-mode))
          (pop-to-buffer (current-buffer)))))))

(defalias 'ement-room-describe #'ement-describe-room)

;;;;;; Push rules

;; NOTE: Although v1.4 of the spec is available and describes setting the push rules using
;; the "v3" API endpoint, the Element client continues to use the "r0" endpoint, which is
;; slightly different.  This implementation will follow Element's initially, because the
;; spec is not simple, and imitating Element's requests will make it easier.

(defun ement-room-notification-state (room session)
  "Return notification state for ROOM on SESSION.
Returns one of nil (meaning default rules are used), `all-loud',
`all', `mentions-and-keywords', or `none'."
  ;; Following the implementation of getRoomNotifsState() in RoomNotifs.ts in matrix-react-sdk.

  ;; TODO: Guest support (in which case the state should be `all').
  ;; TODO: Store account data as a hash table of event types.
  (let ((push-rules (cl-find-if (lambda (alist)
                                  (equal "m.push_rules" (alist-get 'type alist)))
                                (ement-session-account-data session))))
    (cl-labels ((override-mute-rule-for-room-p (room)
                  ;; Following findOverrideMuteRule() in RoomNotifs.ts.
                  (when-let ((overrides (map-nested-elt push-rules '(content global override))))
                    (cl-loop for rule in overrides
                             when (and (alist-get 'enabled rule)
                                       (rule-for-room-p rule room))
                             return rule)))
                (rule-for-room-p (rule room)
                  ;; Following isRuleForRoom() in RoomNotifs.ts.
                  (and (/= 1 (length (alist-get 'conditions rule)))
                       (pcase-let* ((condition (elt (alist-get 'conditions rule) 0))
                                    ((map kind key pattern) condition))
                         (and (equal "event_match" kind)
                              (equal "room_id" key)
                              (equal (ement-room-id room) pattern)))))
                (mute-rule-p (rule)
                  (when-let ((actions (alist-get 'actions rule)))
                    (seq-contains-p actions "dont_notify")))
                ;; NOTE: Although v1.7 of the spec says that "dont_notify" is
                ;; obsolete, the latest revision of matrix-react-sdk (released last week
                ;; as v3.77.1) still works as modeled here.
                (tweak-rule-p (type rule)
                  (when-let ((actions (alist-get 'actions rule)))
                    (and (seq-contains-p actions "notify")
                         (seq-contains-p actions `(set_tweak . ,type) 'seq-contains-p)))))
      ;; If none of these match, nil is returned, meaning that the default rule is used
      ;; for the room.
      (if (override-mute-rule-for-room-p room)
          'none
        (when-let ((room-rule (cl-find-if (lambda (rule)
                                            (equal (ement-room-id room) (alist-get 'rule_id rule)))
                                          (map-nested-elt push-rules '(content global room)))))
          (cond ((not (alist-get 'enabled room-rule))
                 ;; NOTE: According to comment in getRoomNotifsState(), this assumes that
                 ;; the default is to notify for all messages, which "will be 'wrong' for
                 ;; one to one rooms because they will notify loudly for all messages."
                 'all)
                ((mute-rule-p room-rule)
                 ;; According to comment, a room-level mute still allows mentions to
                 ;; notify.  NOTE: See note above.
                 'mentions-and-keywords)
                ((tweak-rule-p "sound" room-rule) 'all-loud)))))))

(defun ement-room-set-notification-state (state room session)
  "Set notification STATE for ROOM on SESSION.
Interactively, with prefix, prompt for room and session,
otherwise use current room.  STATE may be nil to set the rules to
default, `all', `mentions-and-keywords', or `none'."
  ;; This merely attempts to reproduce the behavior of Element's simple notification
  ;; options.  It does not attempt to offer all of the features defined in the spec.  And,
  ;; yes, it is rather awkward, having to sometimes* make multiple requests of different
  ;; "kinds" to set the rules for a single room, but that is how the API works.
  ;;
  ;; * It appears that Element only makes multiple requests of different kinds when
  ;; strictly necessary, but coding that logic now would seem likely to be a waste of
  ;; time, given that Element doesn't even use the latest version of the spec yet.  So
  ;; we'll just do the "dumb" thing and always send requests of both "override" and
  ;; "room" kinds, which appears to Just Work™.
  ;;
  ;; TODO: Match rules to these user-friendly notification states for presentation.  See
  ;; <https://github.com/matrix-org/matrix-react-sdk/blob/8c67984f50f985aa481df24778078030efa39001/src/RoomNotifs.ts>.

  ;; TODO: Support `all-loud' ("all_messages_loud").
  (interactive
   (ement-with-room-and-session
     (let* ((prompt (format "Set notification rules for %s: " (ement--format-room ement-room)))
            (available-states (ement-alist "Default" nil
                                           "All messages" 'all
                                           "Mentions and keywords" 'mentions-and-keywords
                                           "None" 'none))
            (selected-rule (completing-read prompt (mapcar #'car available-states) nil t))
            (state (alist-get selected-rule available-states nil nil #'equal)))
       (list state ement-room ement-session))))
  (cl-labels ((set-rule (kind rule queue message-fn)
                (pcase-let* (((cl-struct ement-room (id room-id)) room)
                             (rule-id (url-hexify-string room-id))
                             (endpoint (format "pushrules/global/%s/%s" kind rule-id))
                             (method (if rule 'put 'delete))
                             (then (if rule
                                       ;; Setting rules requires PUTting the rules, then making a second
                                       ;; request to enable them.
                                       (lambda (_data)
                                         (ement-api session (concat endpoint "/enabled") :queue queue :version "r0"
                                           :method 'put :data (json-encode (ement-alist 'enabled t))
                                           :then message-fn))
                                     message-fn)))
                  (ement-api session endpoint :queue queue :method method :version "r0"
                    :data (json-encode rule)
                    :then then
                    :else (lambda (plz-error)
                            (pcase-let* (((cl-struct plz-error response) plz-error)
                                         ((cl-struct plz-response status) response))
                              (pcase status
                                (404 (pcase rule
                                       (`nil
                                        ;; Room already had no rules, so none being found is not an
                                        ;; error.
                                        nil)
                                       (_ ;; Unexpected error: re-signal.
                                        (ement-api-error plz-error))))
                                (_ ;; Unexpected error: re-signal.
                                 (ement-api-error plz-error)))))))))
    (pcase-let* ((available-states
                  (ement-alist
                   nil (ement-alist
                        "override" nil
                        "room" nil)
                   'all (ement-alist
                         "override" nil
                         "room" (ement-alist
                                 'actions (vector "notify" (ement-alist
                                                            'set_tweak "sound"
                                                            'value "default"))))
                   'mentions-and-keywords (ement-alist
                                           "override" nil
                                           "room" (ement-alist
                                                   'actions (vector "dont_notify")))
                   'none (ement-alist
                          "override" (ement-alist
                                      'actions (vector "dont_notify")
                                      'conditions (vector (ement-alist
                                                           'kind "event_match"
                                                           'key "room_id"
                                                           'pattern (ement-room-id room))))
                          "room" nil)))
                 (kinds-and-rules (alist-get state available-states nil nil #'equal)))
      (cl-loop with queue = (make-plz-queue :limit 1)
               with total = (1- (length kinds-and-rules))
               for count from 0
               for message-fn = (if (equal count total)
                                    (lambda (_data)
                                      (message "Set notification rules for room: %s" (ement--format-room room)))
                                  #'ignore)
               for (kind . state) in kinds-and-rules
               do (set-rule kind state queue message-fn)))))

;;;;; Public functions

;; These functions could reasonably be called by code in other packages.

(cl-defun ement-put-state
    (room type key data session
          &key (then (lambda (response-data)
                       (ement-debug "State data put on room" response-data data room session))))
  "Put state event of TYPE with KEY and DATA on ROOM on SESSION.
DATA should be an alist, which will become the JSON request
body."
  (declare (indent defun))
  (pcase-let* ((endpoint (format "rooms/%s/state/%s/%s"
                                 (url-hexify-string (ement-room-id room))
                                 type key)))
    (ement-api session endpoint :method 'put :data (json-encode data)
      ;; TODO: Handle error codes.
      :then then)))

(defun ement-message (format-string &rest args)
  "Call `message' on FORMAT-STRING prefixed with \"Ement: \"."
  ;; TODO: Use this function everywhere we use `message'.
  (apply #'message (concat "Ement: " format-string) args))

(cl-defun ement-upload (session &key data filename then else
                                (content-type "application/octet-stream"))
  "Upload DATA with FILENAME to content repository on SESSION.
THEN and ELSE are passed to `ement-api', which see."
  (declare (indent defun))
  (ement-api session "upload" :method 'post :endpoint-category "media"
    ;; NOTE: Element currently uses "r0" not "v3", so so do we.
    :params (when filename
              (list (list "filename" filename)))
    :content-type content-type :data data :data-type 'binary
    :then then :else else))

(cl-defun ement-complete-session (&key (prompt "Session: "))
  "Return an Ement session selected with completion."
  (pcase (length ement-sessions)
    (0 (user-error "No active sessions.  Call `ement-connect' to log in"))
    (1 (cdar ement-sessions))
    (_ (let* ((ids (mapcar #'car ement-sessions))
              (selected-id (completing-read prompt ids nil t)))
         (alist-get selected-id ement-sessions nil nil #'equal)))))

(declare-function ewoc-locate "ewoc")
(defun ement-complete-user-id ()
  "Return a user-id selected with completion.
Selects from seen users on all sessions.  If point is on an
event, suggests the event's sender as initial input.  Allows
unseen user IDs to be input as well."
  (cl-labels ((format-user (user)
                ;; FIXME: Per-room displaynames are now stored in room structs
                ;; rather than user structs, so to be complete, this needs to
                ;; iterate over all known rooms, looking for the user's
                ;; displayname in that room.
                (format "%s <%s>"
                        (ement-user-displayname user)
			(ement-user-id user))))
    (let* ((display-to-id
	    (cl-loop for key being the hash-keys of ement-users
		     using (hash-values value)
		     collect (cons (format-user value) key)))
           (user-at-point (when (equal major-mode 'ement-room-mode)
                            (when-let ((node (ewoc-locate ement-ewoc)))
                              (when (ement-event-p (ewoc-data node))
                                (format-user (ement-event-sender (ewoc-data node)))))))
	   (selected-user (completing-read "User: " (mapcar #'car display-to-id)
                                           nil nil user-at-point)))
      (or (alist-get selected-user display-to-id nil nil #'equal)
	  selected-user))))

(cl-defun ement-put-account-data
    (session type data &key room
             (then (lambda (received-data)
                     ;; Handle echoed-back account data event (the spec does not explain this,
                     ;; but see <https://github.com/matrix-org/matrix-react-sdk/blob/675b4271e9c6e33be354a93fcd7807253bd27fcd/src/settings/handlers/AccountSettingsHandler.ts#L150>).
                     ;; FIXME: Make session account-data a map instead of a list of events.
                     (if room
                         (push received-data (ement-room-account-data room))
                       (push received-data (ement-session-account-data session)))

                     ;; NOTE: Commenting out this ement-debug form because a bug in Emacs
                     ;; causes this long string to be interpreted as the function's
                     ;; docstring and cause a too-long-docstring warning.

                     ;; (ement-debug "Account data put and received back on session %s:  PUT(json-encoded):%S  RECEIVED:%S"
                     ;;              (ement-user-id (ement-session-user session)) (json-encode data) received-data)
                     )))
  "Put account data of TYPE with DATA on SESSION.
If ROOM, put it on that room's account data.  Also handle the
echoed-back event."
  (declare (indent defun))
  (pcase-let* (((cl-struct ement-session (user (cl-struct ement-user (id user-id)))) session)
               (room-part (if room (format "/rooms/%s" (ement-room-id room)) ""))
               (endpoint (format "user/%s%s/account_data/%s" (url-hexify-string user-id) room-part type)))
    (ement-api session endpoint :method 'put :data (json-encode data)
      :then then)))

(defun ement-redact (event room session &optional reason)
  "Redact EVENT in ROOM on SESSION, optionally for REASON."
  (pcase-let* (((cl-struct ement-event (id event-id)) event)
               ((cl-struct ement-room (id room-id)) room)
               (endpoint (format "rooms/%s/redact/%s/%s"
                                 room-id event-id (ement--update-transaction-id session)))
               (content (ement-alist "reason" reason)))
    (ement-api session endpoint :method 'put :data (json-encode content)
      :then (lambda (_data)
              (message "Event %s redacted." event-id)))))

;;;;; Inline functions

(defsubst ement--user-color (user)
  "Return USER's color, setting it if necessary.
USER is an `ement-user' struct."
  (or (ement-user-color user)
      (setf (ement-user-color user)
            (ement--prism-color (ement-user-id user)))))

;;;;; Private functions

;; These functions aren't expected to be called by code in other packages (but if that
;; were necessary, they could be renamed accordingly).

;; (defun ement--room-routing (room)
;;   "Return a list of servers to route to ROOM through."
;;   ;; See <https://spec.matrix.org/v1.2/appendices/#routing>.
;;   ;; FIXME: Ensure highest power level user is at least level 50.
;;   ;; FIXME: Ignore servers blocked due to server ACLs.
;;   ;; FIXME: Ignore servers which are IP addresses.
;;   (cl-labels ((most-powerful-user-in
;;                (room))
;;               (servers-by-population-in
;;                (room))
;;               (server-of (user)))
;;     (let (first-server-by-power-level)
;;       (delete-dups
;;        (remq nil
;;              (list
;;               ;; 1.
;;               (or (when-let ((user (most-powerful-user-in room)))
;;                     (setf first-server-by-power-level t)
;;                     (server-of user))
;;                   (car (servers-by-population-in room)))
;;               ;; 2.
;;               (if first-server-by-power-level
;;                   (car (servers-by-population-in room))
;;                 (cl-second (servers-by-population-in room)))
;;               ;; 3.
;;               (cl-third (servers-by-population-in room))))))))

(defun ement--space-p (room)
  "Return non-nil if ROOM is a space."
  (equal "m.space" (ement-room-type room)))

(defun ement--room-in-space-p (room space)
  "Return non-nil if ROOM is in SPACE on SESSION."
  ;; We could use `ement---room-spaces', but since that returns rooms by looking them up
  ;; by ID in the session's rooms list, this is more efficient.
  (pcase-let* (((cl-struct ement-room (id parent-id) (local (map children))) space)
               ((cl-struct ement-room (id child-id) (local (map parents))) room))
    (or (member parent-id parents)
        (member child-id children))))

(defun ement--room-spaces (room session)
  "Return list of ROOM's parent spaces on SESSION."
  ;; NOTE: This only looks in the room's parents list; it doesn't look in every space's children
  ;; list.  This should be good enough, assuming we add to the lists correctly elsewhere.
  (pcase-let* (((cl-struct ement-session rooms) session)
               ((cl-struct ement-room (local (map parents))) room))
    (cl-remove-if-not (lambda (session-room-id)
                        (member session-room-id parents))
                      rooms :key #'ement-room-id)))

(cl-defun ement--prism-color (string &key (contrast-with (face-background 'default nil 'default)))
  "Return a computed color for STRING.
The color is adjusted to have sufficient contrast with the color
CONTRAST-WITH (by default, the default face's background).  The
computed color is useful for user messages, generated room
avatars, etc."
  ;; TODO: Use this instead of `ement-room--user-color'.  (Same algorithm ,just takes a
  ;; string as argument.)
  ;; TODO: Try using HSV somehow so we could avoid having so many strings return a
  ;; nearly-black color.
  (cl-labels ((relative-luminance (rgb)
                ;; Copy of `modus-themes-wcag-formula', an elegant
                ;; implementation by Protesilaos Stavrou.  Also see
                ;; <https://en.wikipedia.org/wiki/Relative_luminance> and
                ;; <https://www.w3.org/TR/WCAG20/#relativeluminancedef>.
                (cl-loop for k in '(0.2126 0.7152 0.0722)
                         for x in rgb
                         sum (* k (if (<= x 0.03928)
                                      (/ x 12.92)
                                    (expt (/ (+ x 0.055) 1.055) 2.4)))))
              (contrast-ratio (a b)
                ;; Copy of `modus-themes-contrast'; see above.
                (let ((ct (/ (+ (relative-luminance a) 0.05)
                             (+ (relative-luminance b) 0.05))))
                  (max ct (/ ct))))
              (increase-contrast (color against target toward)
                (let ((gradient (cdr (color-gradient color toward 20)))
                      new-color)
                  (cl-loop do (setf new-color (pop gradient))
                           while new-color
                           until (>= (contrast-ratio new-color against) target)
                           ;; Avoid infinite loop in case of weirdness
                           ;; by returning color as a fallback.
                           finally return (or new-color color)))))
    (let* ((id string)
           (id-hash (float (+ (abs (sxhash id)) ement-room-prism-color-adjustment)))
           ;; TODO: Wrap-around the value to get the color I want.
           (ratio (/ id-hash (float most-positive-fixnum)))
           (color-num (round (* (* 255 255 255) ratio)))
           (color-rgb (list (/ (float (logand color-num 255)) 255)
                            (/ (float (ash (logand color-num 65280) -8)) 255)
                            (/ (float (ash (logand color-num 16711680) -16)) 255)))
           (contrast-with-rgb (color-name-to-rgb contrast-with)))
      (when (< (contrast-ratio color-rgb contrast-with-rgb) ement-room-prism-minimum-contrast)
        (setf color-rgb (increase-contrast color-rgb contrast-with-rgb ement-room-prism-minimum-contrast
                                           (color-name-to-rgb
                                            ;; Ideally we would use the foreground color,
                                            ;; but in some themes, like Solarized Dark,
                                            ;; the foreground color's contrast is too low
                                            ;; to be effective as the value to increase
                                            ;; contrast against, so we use white or black.
                                            (pcase contrast-with
                                              ((or `nil "unspecified-bg")
                                               ;; The `contrast-with' color (i.e. the
                                               ;; default background color) is nil.  This
                                               ;; probably means that we're displaying on
                                               ;; a TTY.
                                               (if (fboundp 'frame--current-backround-mode)
                                                   ;; This function can tell us whether
                                                   ;; the background color is dark or
                                                   ;; light, but it was added in Emacs
                                                   ;; 28.1.
                                                   (pcase (frame--current-backround-mode (selected-frame))
                                                     ('dark "white")
                                                     ('light "black"))
                                                 ;; Pre-28.1: Since faces' colors may be
                                                 ;; "unspecified" on TTY frames, in which
                                                 ;; case we have nothing to compare with, we
                                                 ;; assume that the background color of such
                                                 ;; a frame is black and increase contrast
                                                 ;; toward white.
                                                 "white"))
                                              (_
                                               ;; The `contrast-with` color is usable: test it.
                                               (if (ement--color-dark-p (color-name-to-rgb contrast-with))
                                                   "white" "black")))))))
      (apply #'color-rgb-to-hex (append color-rgb (list 2))))))

(cl-defun ement--format-user (user &optional (room ement-room) (session ement-session))
  "Format `ement-user' USER for ROOM on SESSION.
ROOM defaults to the value of `ement-room'."
  (let ((face (cond ((equal (ement-user-id (ement-session-user session))
                            (ement-user-id user))
                     'ement-room-self)
                    (ement-room-prism
                     `(:inherit ement-room-user :foreground ,(or (ement-user-color user)
                                                                 (setf (ement-user-color user)
                                                                       (ement--prism-color user)))))
                    (t 'ement-room-user))))
    ;; FIXME: If a membership state event has not yet been received, this
    ;; sets the display name in the room to the user ID, and that prevents
    ;; the display name from being used if the state event arrives later.
    (propertize (ement--user-displayname-in room user)
                'face face
                'help-echo (ement-user-id user))))

(cl-defun ement--format-body-mentions
    (body room &key (template "<a href=\"https://matrix.to/#/%s\">%s</a>"))
  "Return string for BODY with mentions in ROOM linkified with TEMPLATE.
TEMPLATE is a format string in which the first \"%s\" is replaced
with the user's MXID and the second with the displayname.  A
mention is qualified by an \"@\"-prefixed displayname or
MXID (optionally suffixed with a colon), or a colon-suffixed
displayname, followed by a blank, question mark, comma, or
period, anywhere in the body."
  ;; Examples:
  ;; "@foo: hi"
  ;; "@foo:matrix.org: hi"
  ;; "foo: hi"
  ;; "@foo and @bar:matrix.org: hi"
  ;; "foo: how about you and @bar ..."
  (declare (indent defun))
  (cl-labels ((members-having-displayname (name members)
                ;; Iterating over the hash table values isn't as efficient as a hash
                ;; lookup, but in most rooms it shouldn't be a problem.
                (cl-loop for user being the hash-values of members
                         when (equal name (ement--user-displayname-in room user))
                         collect user)))
    (pcase-let* (((cl-struct ement-room members) room)
                 (regexp (rx (or bos bow blank "\n")
                             (or (seq (group
                                       ;; Group 1: full @-prefixed MXID.
                                       "@" (group
                                            ;; Group 2: displayname.  (NOTE: Does not work
                                            ;; with displaynames containing spaces.)
                                            (1+ (seq (optional ".") alnum)))
                                       (optional ":" (1+ (seq (optional ".") alnum))))
                                      (or ":" eow eos (syntax punctuation)))
                                 (seq (group
                                       ;; Group 3: MXID username or displayname.
                                       (1+ (not blank)))
                                      ":" (1+ blank)))))
                 (pos 0) (replace-group) (replacement))
      (while (setf pos (string-match regexp body pos))
        (if (setf replacement
                  (or (when-let (member (gethash (match-string 1 body) members))
                        ;; Found user ID: use it as replacement.
                        (setf replace-group 1)
                        (format template (match-string 1 body)
                                (ement--xml-escape-string (ement--user-displayname-in room member))))
                      (when-let* ((name (or (when (match-string 2 body)
                                              (setf replace-group 1)
                                              (match-string 2 body))
                                            (prog1 (match-string 3 body)
                                              (setf replace-group 3))))
                                  (members (members-having-displayname name members))
                                  (member (when (= 1 (length members))
                                            ;; If multiple members are found with the same
                                            ;; displayname, do nothing.
                                            (car members))))
                        ;; Found displayname: use it and MXID as replacement.
                        (format template (ement-user-id member)
                                (ement--xml-escape-string name)))))
            (progn
              ;; Found member: replace and move to end of replacement.
              (setf body (replace-match replacement t t body replace-group))
              (let ((difference (- (length replacement) (length (match-string 0 body)))))
                (setf pos (if (/= 0 difference)
                              ;; Replacement of a different length: adjust POS accordingly.
                              (+ pos difference)
                            (match-end 0)))))
          ;; No replacement: move to end of match.
          (setf pos (match-end 0))))))
  body)

(defun ement--event-mentions-room-p (event &rest _ignore)
  "Return non-nil if EVENT mentions \"@room\"."
  (pcase-let (((cl-struct ement-event (content (map body))) event))
    (when body
      (string-match-p (rx (or space bos) "@room" eow) body))))

(cl-defun ement-complete-room (&key session (predicate #'identity)
                                    (prompt "Room: ") (suggest t))
  "Return a (room session) list selected from SESSION with completion.
If SESSION is nil, select from rooms in all of `ement-sessions'.
When SUGGEST, suggest current buffer's room (or a room at point
in a room list buffer) as initial input (i.e. it should be set to
nil when switching from one room buffer to another).  PROMPT may
override the default prompt.  PREDICATE may be a function to
select which rooms are offered; it is also applied to the
suggested room."
  (declare (indent defun))
  (pcase-let* ((sessions (if session
                             (list session)
                           (mapcar #'cdr ement-sessions)))
               (name-to-room-session
                (cl-loop for session in sessions
                         append (cl-loop for room in (ement-session-rooms session)
                                         when (funcall predicate room)
                                         collect (cons (ement--format-room room 'topic)
                                                       (list room session)))))
               (names (mapcar #'car name-to-room-session))
               (selected-name (completing-read
                               prompt names nil t
                               (when suggest
                                 (when-let ((suggestion (ement--room-at-point)))
                                   (when (or (not predicate)
                                             (funcall predicate suggestion))
                                     (ement--format-room suggestion 'topic)))))))
    (alist-get selected-name name-to-room-session nil nil #'string=)))

(cl-defun ement-send-message (room session
                                   &key body formatted-body replying-to-event filter then)
  "Send message to ROOM on SESSION with BODY and FORMATTED-BODY.
THEN may be a function to call after the event is sent
successfully.  It is called with keyword arguments for ROOM,
SESSION, CONTENT, and DATA.

REPLYING-TO-EVENT may be an event the message is
in reply to; the message will reference it appropriately.

FILTER may be a function through which to pass the message's
content object before sending (see,
e.g. `ement-room-send-org-filter')."
  (declare (indent defun))
  (cl-assert (not (string-empty-p body)))
  (cl-assert (or (not formatted-body) (not (string-empty-p formatted-body))))
  (pcase-let* (((cl-struct ement-room (id room-id)) room)
               (endpoint (format "rooms/%s/send/m.room.message/%s" (url-hexify-string room-id)
                                 (ement--update-transaction-id session)))
               (formatted-body (when formatted-body
                                 (ement--format-body-mentions formatted-body room)))
               (content (ement-aprog1
                            (ement-alist "msgtype" "m.text"
                                         "body" body)
                          (when formatted-body
                            (push (cons "formatted_body" formatted-body) it)
                            (push (cons "format" "org.matrix.custom.html") it))))
               (then (or then #'ignore)))
    (when filter
      (setf content (funcall filter content room)))
    (when replying-to-event
      (setf replying-to-event (ement--original-event-for replying-to-event session)
            content (ement--add-reply content replying-to-event room)))
    (ement-api session endpoint :method 'put :data (json-encode content)
      :then (apply-partially then :room room :session session
                             ;; Data is added when calling back.
                             :content content :data))))

(defalias 'ement--button-buttonize
  ;; This isn't nice, but what can you do.
  (cond ((version<= "29.1" emacs-version) #'buttonize)
        ((version<= "28.1" emacs-version) (with-suppressed-warnings ((obsolete button-buttonize))
                                            #'button-buttonize))
        ((version< emacs-version "28.1")
         ;; FIXME: This doesn't set the mouse-face to highlight, and it doesn't use the
         ;; default-button category.  Neither does `button-buttonize', of course, but why?
         (lambda (string callback &optional data)
           "Make STRING into a button and return it.
When clicked, CALLBACK will be called with the DATA as the
function argument.  If DATA isn't present (or is nil), the button
itself will be used instead as the function argument."
           (propertize string
                       'face 'button
                       'button t
                       'follow-link t
                       'category t
                       'button-data data
                       'keymap button-map
                       'action callback)))))

(defun ement--add-reply (data replying-to-event room)
  "Return DATA adding reply data for REPLYING-TO-EVENT in ROOM.
DATA is an unsent message event's data alist."
  ;; SPEC: <https://matrix.org/docs/spec/client_server/r0.6.1#id351> "13.2.2.6.1  Rich replies"
  ;; FIXME: Rename DATA.
  (pcase-let* (((cl-struct ement-event (id replying-to-event-id)
                           content (sender replying-to-sender))
                replying-to-event)
               ((cl-struct ement-user (id replying-to-sender-id)) replying-to-sender)
               ((map ('body replying-to-body) ('formatted_body replying-to-formatted-body)) content)
               (replying-to-sender-name (ement--user-displayname-in ement-room replying-to-sender))
               (quote-string (format "> <%s> %s\n\n" replying-to-sender-name replying-to-body))
               (reply-body (alist-get "body" data nil nil #'string=))
               (reply-formatted-body (alist-get "formatted_body" data nil nil #'string=))
               (reply-body-with-quote (concat quote-string reply-body))
               (reply-formatted-body-with-quote
                (format "<mx-reply>
  <blockquote>
    <a href=\"https://matrix.to/#/%s/%s\">In reply to</a>
    <a href=\"https://matrix.to/#/%s\">%s</a>
    <br />
    %s
  </blockquote>
</mx-reply>
%s"
                        (ement-room-id room) replying-to-event-id replying-to-sender-id replying-to-sender-name
                        ;; TODO: Encode HTML special characters.  Not as straightforward in Emacs as one
                        ;; might hope: there's `web-mode-html-entities' and `org-entities'.  See also
                        ;; <https://emacs.stackexchange.com/questions/8166/encode-non-html-characters-to-html-equivalent>.
                        (or replying-to-formatted-body replying-to-body)
                        (or reply-formatted-body reply-body))))
    ;; NOTE: map-elt doesn't work with string keys, so we use `alist-get'.
    (setf (alist-get "body" data nil nil #'string=) reply-body-with-quote
          (alist-get "formatted_body" data nil nil #'string=) reply-formatted-body-with-quote
          data (append (ement-alist "m.relates_to"
                                    (ement-alist "m.in_reply_to"
                                                 (ement-alist "event_id" replying-to-event-id))
                                    "format" "org.matrix.custom.html")
                       data))
    data))

(defun ement--direct-room-for-user (user session)
  "Return last-modified direct room with USER on SESSION, if one exists."
  ;; Loosely modeled on the Element function findDMForUser in createRoom.ts.
  (cl-labels ((membership-event-for-p (event user)
                (and (equal "m.room.member" (ement-event-type event))
                     (equal (ement-user-id user) (ement-event-state-key event))))
              (latest-membership-for (user room)
                (when-let ((latest-membership-event
                            (car
                             (cl-sort
                              ;; I guess we need to check both state and timeline events.
                              (append (cl-remove-if-not (lambda (event)
                                                          (membership-event-for-p event user))
                                                        (ement-room-state room))
                                      (cl-remove-if-not (lambda (event)
                                                          (membership-event-for-p event user))
                                                        (ement-room-timeline room)))
                              (lambda (a b)
                                ;; Sort latest first so we can use the car.
                                (> (ement-event-origin-server-ts a)
                                   (ement-event-origin-server-ts b)))))))
                  (alist-get 'membership (ement-event-content latest-membership-event))))
              (latest-event-in (room)
                (car
                 (cl-sort
                  (append (ement-room-state room)
                          (ement-room-timeline room))
                  (lambda (a b)
                    ;; Sort latest first so we can use the car.
                    (> (ement-event-origin-server-ts a)
                       (ement-event-origin-server-ts b)))))))
    (let* ((direct-rooms (cl-remove-if-not
                          (lambda (room)
                            (ement--room-direct-p room session))
                          (ement-session-rooms session)))
           (direct-joined-rooms
            ;; Ensure that the local user is still in each room.
            (cl-remove-if-not
             (lambda (room)
               (equal "join" (latest-membership-for (ement-session-user session) room)))
             direct-rooms))
           ;; Since we don't currently keep a member list for each room, we look in the room's
           ;; join events to see if the user has joined or been invited.
           (direct-rooms-with-user
            (cl-remove-if-not
             (lambda (room)
               (member (latest-membership-for user room) '("invite" "join")))
             direct-joined-rooms)))
      (car (cl-sort direct-rooms-with-user
                    (lambda (a b)
                      (> (latest-event-in a) (latest-event-in b))))))))

(defun ement--event-replaces-p (a b)
  "Return non-nil if event A replaces event B.
That is, if event A replaces B in their
\"m.relates_to\"/\"m.relations\" and \"m.replace\" metadata."
  (pcase-let* (((cl-struct ement-event (id a-id) (origin-server-ts a-ts)
                           (content (map ('m.relates_to
                                          (map ('rel_type a-rel-type)
                                               ('event_id a-replaces-event-id))))))
                a)
               ((cl-struct ement-event (id b-id) (origin-server-ts b-ts)
                           (content (map ('m.relates_to
                                          (map ('rel_type b-rel-type)
                                               ('event_id b-replaces-event-id)))
                                         ('m.relations
                                          (map ('m.replace
                                                (map ('event_id b-replaced-by-event-id))))))))
                b))
    (or (equal a-id b-replaced-by-event-id)
        (and (equal "m.replace" a-rel-type)
             (or (equal a-replaces-event-id b-id)
                 (and (equal "m.replace" b-rel-type)
                      (equal a-replaces-event-id b-replaces-event-id)
                      (>= a-ts b-ts)))))))

(defun ement--events-equal-p (a b)
  "Return non-nil if events A and B are essentially equal.
That is, A and B are either the same event (having the same event
ID), or one event replaces the other (in their m.relates_to and
m.replace metadata)."
  (or (equal (ement-event-id a) (ement-event-id b))
      (ement--event-replaces-p a b)
      (ement--event-replaces-p b a)))

(defun ement--original-event-for (event session)
  "Return the original of EVENT in SESSION.
If EVENT has metadata indicating that it replaces another event,
return the replaced event; otherwise return EVENT.  If a replaced
event can't be found in SESSION's events table, return an ersatz
one that has the expected ID and same sender."
  (pcase-let (((cl-struct ement-event sender
                          (content (map ('m.relates_to
                                         (map ('event_id replaced-event-id)
                                              ('rel_type relation-type))))))
               event))
    (pcase relation-type
      ("m.replace" (or (gethash replaced-event-id (ement-session-events session))
                       (make-ement-event :id replaced-event-id :sender sender)))
      (_ event))))

(defun ement--format-room (room &optional topic)
  "Return ROOM formatted with name, alias, ID, and optionally TOPIC.
Suitable for use in completion, etc."
  (if topic
      (format "%s%s(<%s>)%s"
              (or (ement-room-display-name room)
                  (setf (ement-room-display-name room)
                        (ement--room-display-name room)))
              (if (ement-room-canonical-alias room)
                  (format " <%s> " (ement-room-canonical-alias room))
                " ")
              (ement-room-id room)
              (if (ement-room-topic room)
                  (format ": \"%s\"" (ement-room-topic room))
                ""))
    (format "%s%s(<%s>)"
            (or (ement-room-display-name room)
                (setf (ement-room-display-name room)
                      (ement--room-display-name room)))
            (if (ement-room-canonical-alias room)
                (format " <%s> " (ement-room-canonical-alias room))
              " ")
            (ement-room-id room))))

(defun ement--members-alist (room)
  "Return alist of member displaynames mapped to IDs seen in ROOM."
  ;; We map displaynames to IDs because `ement-room--format-body-mentions' needs to find
  ;; MXIDs from displaynames.
  (pcase-let* (((cl-struct ement-room timeline) room)
               (members-seen (mapcar #'ement-event-sender timeline))
               (members-alist))
    (dolist (member members-seen)
      ;; Testing with `benchmark-run-compiled', it appears that using `cl-pushnew' is
      ;; about 10x faster than using `delete-dups'.
      (cl-pushnew (cons (ement--user-displayname-in room member)
                        (ement-user-id member))
                  members-alist))
    members-alist))

(defun ement--mxc-to-url (uri session)
  "Return HTTPS URL for MXC URI accessed through SESSION."
  (pcase-let* (((cl-struct ement-session server) session)
               ((cl-struct ement-server uri-prefix) server)
               (server-name) (media-id))
    (string-match (rx "mxc://" (group (1+ (not (any "/"))))
                      "/" (group (1+ anything))) uri)
    (setf server-name (match-string 1 uri)
          media-id (match-string 2 uri))
    (format "%s/_matrix/media/r0/download/%s/%s"
            uri-prefix server-name media-id)))

(defun ement--mxc-to-endpoint (uri)
  "Return API endpoint for MXC URI.
Returns string suitable for the ENDPOINT argument to `ement-api'."
  (string-match (rx "mxc://" (group (1+ (not (any "/"))))
                    "/" (group (1+ anything))) uri)
  (let ((server-name (match-string 1 uri))
        (media-id (match-string 2 uri)))
    (format "media/download/%s/%s" server-name media-id)))

(defun ement--remove-face-property (string value)
  "Remove VALUE from STRING's `face' properties.
Used to remove the `button' face from buttons, because that face
can cause undesirable underlining."
  (let ((pos 0))
    (cl-loop for next-face-change-pos = (next-single-property-change pos 'face string)
             for face-at = (get-text-property pos 'face string)
             when face-at
             do (put-text-property pos (or next-face-change-pos (length string))
                                   'face (cl-typecase face-at
                                           (atom (if (equal value face-at)
                                                     nil face-at))
                                           (list (remove value face-at)))
                                   string)
             while next-face-change-pos
             do (setf pos next-face-change-pos))))

(cl-defun ement--text-property-search-forward (property predicate string &key (start 0))
  "Return the position at which PROPERTY in STRING matches PREDICATE.
Return nil if not found.  Searches forward from START."
  (declare (indent defun))
  (cl-loop for pos = start then (next-single-property-change pos property string)
           while pos
           when (funcall predicate (get-text-property pos property string))
           return pos))

(cl-defun ement--text-property-search-backward (property predicate string &key (start 0))
  "Return the position at which PROPERTY in STRING matches PREDICATE.
Return nil if not found.  Searches backward from START."
  (declare (indent defun))
  (cl-loop for pos = start then (previous-single-property-change pos property string)
           while (and pos (> pos 1))
           when (funcall predicate (get-text-property (1- pos) property string))
           return pos))

(defun ement--resize-image (image max-width max-height)
  "Return a copy of IMAGE set to MAX-WIDTH and MAX-HEIGHT.
IMAGE should be one as created by, e.g. `create-image'."
  (declare
   ;; This silences a lint warning on our GitHub CI runs, which use a build of Emacs
   ;; without image support.
   (function image-property "image"))
  ;; It would be nice if the image library had some simple functions to do this sort of thing.
  (let ((new-image (cl-copy-list image)))
    (when (fboundp 'imagemagick-types)
      ;; Only do this when ImageMagick is supported.
      ;; FIXME: When requiring Emacs 27+, remove this (I guess?).
      (setf (image-property new-image :type) 'imagemagick))
    (setf (image-property new-image :max-width) max-width
          (image-property new-image :max-height) max-height)
    new-image))

(defun ement--room-alias (room)
  "Return latest m.room.canonical_alias event in ROOM."
  ;; FIXME: This function probably needs to compare timestamps to ensure that older events
  ;; that are inserted at the head of the events lists aren't used instead of newer ones.
  (or (cl-loop for event in (ement-room-timeline room)
               when (equal "m.room.canonical_alias" (ement-event-type event))
               return (alist-get 'alias (ement-event-content event)))
      (cl-loop for event in (ement-room-state room)
               when (equal "m.room.canonical_alias" (ement-event-type event))
               return (alist-get 'alias (ement-event-content event)))))

(declare-function magit-current-section "magit-section")
(declare-function eieio-oref "eieio-core")
(defu
Download .txt
gitextract_pmij6jm2/

├── .dir-locals.el
├── .elpaignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── config.yml
│   └── workflows/
│       └── test.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.org
├── ement-api.el
├── ement-directory.el
├── ement-lib.el
├── ement-macros.el
├── ement-notifications.el
├── ement-notify.el
├── ement-room-list.el
├── ement-room.el
├── ement-structs.el
├── ement-tabulated-room-list.el
├── ement.el
├── makem.sh
└── tests/
    └── ement-tests.el
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (773K chars).
[
  {
    "path": ".dir-locals.el",
    "chars": 172,
    "preview": ";;; Directory Local Variables\n;;; For more information see (info \"(emacs) Directory Variables\")\n\n((emacs-lisp-mode . ((f"
  },
  {
    "path": ".elpaignore",
    "chars": 66,
    "preview": ".github/\nimages/\nLICENSE\nMakefile\nmakem.sh\nNOTES.org\nscreenshots/\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 2301,
    "preview": "name: Bug Report\ndescription: File a bug report\nlabels: [\"bug\"]\nassignees:\n  - alphapapa\nbody:\n  - type: markdown\n    at"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 27,
    "preview": "blank_issues_enabled: true\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 2323,
    "preview": "# * test.yml --- Test Emacs packages using makem.sh on GitHub Actions\n\n# URL: https://github.com/alphapapa/makem.sh\n# Ve"
  },
  {
    "path": ".gitignore",
    "chars": 34,
    "preview": "/.sandbox/\n*.elc\n/worktrees/\n/.#*\n"
  },
  {
    "path": "LICENSE",
    "chars": 35147,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "Makefile",
    "chars": 1370,
    "preview": "# * makem.sh/Makefile --- Script to aid building and testing Emacs Lisp packages\n\n# URL: https://github.com/alphapapa/ma"
  },
  {
    "path": "README.org",
    "chars": 52856,
    "preview": "#+TITLE: Ement.el\n\n#+PROPERTY: LOGGING nil\n\n# Export options.\n#+OPTIONS: broken-links:t *:t num:1 toc:1\n\n# Info export o"
  },
  {
    "path": "ement-api.el",
    "chars": 5891,
    "preview": ";;; ement-api.el --- Matrix API library              -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2022-2023  Free Softw"
  },
  {
    "path": "ement-directory.el",
    "chars": 20790,
    "preview": ";;; ement-directory.el --- Public room directory support                       -*- lexical-binding: t; -*-\n\n;; Copyright"
  },
  {
    "path": "ement-lib.el",
    "chars": 99673,
    "preview": ";;; ement-lib.el --- Library of Ement functions      -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2022-2023  Free Softw"
  },
  {
    "path": "ement-macros.el",
    "chars": 11978,
    "preview": ";;; ement-macros.el --- Ement macros                 -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2022-2023  Free Softw"
  },
  {
    "path": "ement-notifications.el",
    "chars": 13836,
    "preview": ";;; ement-notifications.el --- Notifications support  -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2023  Free Software "
  },
  {
    "path": "ement-notify.el",
    "chars": 15441,
    "preview": ";;; ement-notify.el --- Notifications for Ement events  -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2022-2023  Free So"
  },
  {
    "path": "ement-room-list.el",
    "chars": 41789,
    "preview": ";;; ement-room-list.el --- List Ement rooms  -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2022-2023  Free Software Foun"
  },
  {
    "path": "ement-room.el",
    "chars": 317618,
    "preview": ";;; ement-room.el --- Ement room buffers             -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2022-2023  Free Softw"
  },
  {
    "path": "ement-structs.el",
    "chars": 3132,
    "preview": ";;; ement-structs.el --- Ement structs               -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2022-2023  Free Softw"
  },
  {
    "path": "ement-tabulated-room-list.el",
    "chars": 23439,
    "preview": ";;; ement-tabulated-room-list.el --- Ement tabulated room list buffer    -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2"
  },
  {
    "path": "ement.el",
    "chars": 59762,
    "preview": ";;; ement.el --- Matrix client                       -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2022-2023  Free Softw"
  },
  {
    "path": "makem.sh",
    "chars": 40520,
    "preview": "#!/usr/bin/env bash\n\n# * makem.sh --- Script to aid building and testing Emacs Lisp packages\n\n# URL: https://github.com/"
  },
  {
    "path": "tests/ement-tests.el",
    "chars": 2789,
    "preview": ";;; ement-tests.el --- Tests for Ement.el                  -*- lexical-binding: t; -*-\n\n;; Copyright (C) 2023  Free Soft"
  }
]

About this extraction

This page contains the full source code of the alphapapa/ement.el GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (733.4 KB), approximately 166.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!